From b9c2c289b38c3a99ae2c3de9270943452bb74a99 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 8 Jun 2023 10:06:07 +0100 Subject: [PATCH 0001/1215] Start working on Spring Boot 3.2 This commit also disables the creation of forward merge issues when merging into main. Forward merge issues will be re-enabled once 3.1.1 has been released. --- README.adoc | 2 +- ci/README.adoc | 2 +- ci/parameters.yml | 2 +- ci/pipeline.yml | 4 +- eclipse/spring-boot-project.setup | 4 +- git/hooks/prepare-forward-merge | 8 +- gradle.properties | 2 +- .../spring-boot-dependencies/build.gradle | 2 +- .../jar-layered-custom/jar/src/layers.xml | 2 +- .../war-layered-custom/war/src/layers.xml | 2 +- .../src/main/xsd/layers-3.2.xsd | 100 ++++++++++++++++++ .../dependencies-layer-no-filter.xml | 2 +- .../src/test/resources/layers.xml | 2 +- .../resources/resource-layer-no-filter.xml | 2 +- 14 files changed, 119 insertions(+), 17 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-3.2.xsd diff --git a/README.adoc b/README.adoc index b0e635135a51..b603722e4e25 100755 --- a/README.adoc +++ b/README.adoc @@ -1,4 +1,4 @@ -= Spring Boot image:https://ci.spring.io/api/v1/teams/spring-boot/pipelines/spring-boot-3.1.x/jobs/build/badge["Build Status", link="https://ci.spring.io/teams/spring-boot/pipelines/spring-boot-3.1.x?groups=Build"] image:https://badges.gitter.im/Join Chat.svg["Chat",link="https://gitter.im/spring-projects/spring-boot?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"] image:https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Gradle Enterprise", link="https://ge.spring.io/scans?&search.rootProjectNames=Spring%20Boot%20Build&search.rootProjectNames=spring-boot-build"] += Spring Boot image:https://ci.spring.io/api/v1/teams/spring-boot/pipelines/spring-boot-3.2.x/jobs/build/badge["Build Status", link="https://ci.spring.io/teams/spring-boot/pipelines/spring-boot-3.2.x?groups=Build"] image:https://badges.gitter.im/Join Chat.svg["Chat",link="https://gitter.im/spring-projects/spring-boot?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"] image:https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Gradle Enterprise", link="https://ge.spring.io/scans?&search.rootProjectNames=Spring%20Boot%20Build&search.rootProjectNames=spring-boot-build"] :docs: https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference :github: https://github.com/spring-projects/spring-boot diff --git a/ci/README.adoc b/ci/README.adoc index 4601e84e0b13..5ef82f3ab88d 100644 --- a/ci/README.adoc +++ b/ci/README.adoc @@ -11,7 +11,7 @@ The pipeline can be deployed using the following command: [source] ---- -$ fly -t spring-boot set-pipeline -p spring-boot-3.1.x -c ci/pipeline.yml -l ci/parameters.yml +$ fly -t spring-boot set-pipeline -p spring-boot-3.2.x -c ci/pipeline.yml -l ci/parameters.yml ---- NOTE: This assumes that you have credhub integration configured with the appropriate diff --git a/ci/parameters.yml b/ci/parameters.yml index 5bc1412be5e6..5821375bd742 100644 --- a/ci/parameters.yml +++ b/ci/parameters.yml @@ -4,7 +4,7 @@ homebrew-tap-repo: "https://github.com/spring-io/homebrew-tap.git" docker-hub-organization: "springci" artifactory-server: "https://repo.spring.io" branch: "main" -milestone: "3.1.x" +milestone: "3.2.x" build-name: "spring-boot" concourse-url: "https://ci.spring.io" task-timeout: 2h00m diff --git a/ci/pipeline.yml b/ci/pipeline.yml index de7c64bc4fb9..15109f6d2e39 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -591,7 +591,7 @@ jobs: <<: *sdkman-task-params RELEASE_TYPE: RELEASE BRANCH: ((branch)) - LATEST_GA: true + LATEST_GA: false - name: update-homebrew-tap serial: true plan: @@ -607,7 +607,7 @@ jobs: image: ci-image file: git-repo/ci/tasks/update-homebrew-tap.yml params: - LATEST_GA: true + LATEST_GA: false - put: homebrew-tap-repo params: repository: updated-homebrew-tap-repo diff --git a/eclipse/spring-boot-project.setup b/eclipse/spring-boot-project.setup index c370697aad4a..f1fa2ed8f26e 100644 --- a/eclipse/spring-boot-project.setup +++ b/eclipse/spring-boot-project.setup @@ -11,8 +11,8 @@ xmlns:setup.workingsets="http://www.eclipse.org/oomph/setup/workingsets/1.0" xmlns:workingsets="http://www.eclipse.org/oomph/workingsets/1.0" xsi:schemaLocation="http://www.eclipse.org/oomph/setup/jdt/1.0 http://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/JDT.ecore http://www.eclipse.org/buildship/oomph/1.0 https://raw.githubusercontent.com/eclipse/buildship/master/org.eclipse.buildship.oomph/model/GradleImport-1.0.ecore http://www.eclipse.org/oomph/predicates/1.0 http://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/Predicates.ecore http://www.eclipse.org/oomph/setup/workingsets/1.0 http://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/SetupWorkingSets.ecore http://www.eclipse.org/oomph/workingsets/1.0 http://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/WorkingSets.ecore" - name="spring.boot.3.1.x" - label="Spring Boot 3.1.x"> + name="spring.boot.3.2.x" + label="Spring Boot 3.2.x"> + https://www.springframework.org/schema/layers/layers-3.2.xsd"> **/application*.* diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/layers.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/layers.xml index 418078fe423e..41e9157bb728 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/layers.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/layers.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/layers/layers-3.2.xsd"> **/application*.* diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-3.2.xsd b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-3.2.xsd new file mode 100644 index 000000000000..20219b9bd8b1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-3.2.xsd @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml index 1398e8320206..b6e9af44d621 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/boot/layers/layers-3.2.xsd"> diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/layers.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/layers.xml index b7427f83a79b..81f10e26311c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/layers.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/layers.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/boot/layers/layers-3.2.xsd"> META-INF/resources/** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml index 0614492c4982..ebfb721fb7c2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/boot/layers/layers-3.2.xsd"> From 214f06083b6d79d4540564064d0dcefdad9d2a73 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 13 Jun 2023 08:14:22 +0200 Subject: [PATCH 0002/1215] Auto-configure OtlpHttpSpanExporter only if property is set - Remove the default value of 'management.otlp.tracing.endpoint' Closes gh-35596 --- .../tracing/otlp/OtlpAutoConfiguration.java | 3 +++ .../autoconfigure/tracing/otlp/OtlpProperties.java | 2 +- .../tracing/otlp/OtlpAutoConfigurationTests.java | 11 +++++++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfiguration.java index c9493f0d0d99..ca2012c20d64 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfiguration.java @@ -30,6 +30,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -45,6 +46,7 @@ * define an {@link OtlpGrpcSpanExporter} and this auto-configuration will back off. * * @author Jonatan Ivanov + * @author Moritz Halbritter * @since 3.1.0 */ @AutoConfiguration @@ -56,6 +58,7 @@ public class OtlpAutoConfiguration { @Bean @ConditionalOnMissingBean(value = OtlpHttpSpanExporter.class, type = "io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter") + @ConditionalOnProperty(prefix = "management.otlp.tracing", name = "endpoint") OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpProperties properties) { OtlpHttpSpanExporterBuilder builder = OtlpHttpSpanExporter.builder() .setEndpoint(properties.getEndpoint()) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpProperties.java index efb1a32f7553..371de8491146 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpProperties.java @@ -34,7 +34,7 @@ public class OtlpProperties { /** * URL to the OTel collector's HTTP API. */ - private String endpoint = "http://localhost:4318/v1/traces"; + private String endpoint; /** * Call timeout for the OTel Collector to process an exported batch of data. This diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationTests.java index 5d75c06b5112..f3b3e5b3a5a9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationTests.java @@ -33,16 +33,23 @@ * Tests for {@link OtlpAutoConfiguration}. * * @author Jonatan Ivanov + * @author Moritz Halbritter */ class OtlpAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(OtlpAutoConfiguration.class)); + @Test + void shouldNotSupplyBeansIfPropertyIsNotSet() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(OtlpHttpSpanExporter.class)); + } + @Test void shouldSupplyBeans() { - this.contextRunner.run((context) -> assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class) - .hasSingleBean(SpanExporter.class)); + this.contextRunner.withPropertyValues("management.otlp.tracing.endpoint=http://localhost:4318/v1/traces") + .run((context) -> assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class) + .hasSingleBean(SpanExporter.class)); } @Test From 5b06224af53098a9aa546e51ba6408b2c3ec034a Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 13 Jun 2023 09:28:58 +0200 Subject: [PATCH 0003/1215] Add property for common key/values on observations - Deprecates 'management.metrics.tags.*' Closes gh-33241 --- .../metrics/MetricsProperties.java | 1 + .../ObservationAutoConfiguration.java | 7 +++ .../observation/ObservationProperties.java | 16 +++++ .../PropertiesObservationFilter.java | 51 +++++++++++++++ .../ObservationAutoConfigurationTests.java | 15 +++++ .../PropertiesObservationFilterTests.java | 62 +++++++++++++++++++ .../src/docs/asciidoc/actuator/metrics.adoc | 13 +--- .../docs/asciidoc/actuator/observability.adoc | 20 +++++- 8 files changed, 171 insertions(+), 14 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilter.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilterTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java index f06176bb95dc..e4ad80555230 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java @@ -79,6 +79,7 @@ public Map getEnable() { return this.enable; } + @DeprecatedConfigurationProperty(replacement = "management.observations.key-values") public Map getTags() { return this.tags; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java index 216bc4262408..440fc9bf9656 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java @@ -43,6 +43,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; /** * {@link EnableAutoConfiguration Auto-configuration} for the Micrometer Observation API. @@ -75,6 +76,12 @@ ObservationRegistry observationRegistry() { return ObservationRegistry.create(); } + @Bean + @Order(0) + PropertiesObservationFilter propertiesObservationFilter(ObservationProperties properties) { + return new PropertiesObservationFilter(properties); + } + @Configuration(proxyBeanMethods = false) @ConditionalOnClass(MeterRegistry.class) @ConditionalOnMissingClass("io.micrometer.tracing.Tracer") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java index e4668d7f4b59..92f8e8b0fe2d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java @@ -16,6 +16,9 @@ package org.springframework.boot.actuate.autoconfigure.observation; +import java.util.LinkedHashMap; +import java.util.Map; + import org.springframework.boot.context.properties.ConfigurationProperties; /** @@ -30,10 +33,23 @@ public class ObservationProperties { private final Http http = new Http(); + /** + * Common key-values that are applied to every observation. + */ + private Map keyValues = new LinkedHashMap<>(); + public Http getHttp() { return this.http; } + public Map getKeyValues() { + return this.keyValues; + } + + public void setKeyValues(Map keyValues) { + this.keyValues = keyValues; + } + public static class Http { private final Client client = new Client(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilter.java new file mode 100644 index 000000000000..d1116aa4ddb5 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilter.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.observation; + +import java.util.Map.Entry; + +import io.micrometer.common.KeyValues; +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationFilter; + +/** + * {@link ObservationFilter} to apply settings from {@link ObservationProperties}. + * + * @author Moritz Halbritter + */ +class PropertiesObservationFilter implements ObservationFilter { + + private final ObservationFilter delegate; + + PropertiesObservationFilter(ObservationProperties properties) { + this.delegate = createDelegate(properties); + } + + @Override + public Context map(Context context) { + return this.delegate.map(context); + } + + private static ObservationFilter createDelegate(ObservationProperties properties) { + if (properties.getKeyValues().isEmpty()) { + return (context) -> context; + } + KeyValues keyValues = KeyValues.of(properties.getKeyValues().entrySet(), Entry::getKey, Entry::getValue); + return (context) -> context.addLowCardinalityKeyValues(keyValues); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java index 28e4bd0100e2..186adde6056a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java @@ -189,6 +189,21 @@ void autoConfiguresObservationFilters() { }); } + @Test + void shouldSupplyPropertiesObservationFilterBean() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(PropertiesObservationFilter.class)); + } + + @Test + void shouldApplyCommonKeyValuesToObservations() { + this.contextRunner.withPropertyValues("management.observations.key-values.a=alpha").run((context) -> { + ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); + Observation.start("keyvalues", observationRegistry).stop(); + MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); + assertThat(meterRegistry.get("keyvalues").tag("a", "alpha").timer().count()).isOne(); + }); + } + @Test void autoConfiguresGlobalObservationConventions() { this.contextRunner.withUserConfiguration(CustomGlobalObservationConvention.class).run((context) -> { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilterTests.java new file mode 100644 index 000000000000..b352f9695d1a --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilterTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.observation; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; +import io.micrometer.observation.Observation.Context; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PropertiesObservationFilter}. + * + * @author Moritz Halbritter + */ +class PropertiesObservationFilterTests { + + @Test + void shouldDoNothingIfKeyValuesAreEmpty() { + PropertiesObservationFilter filter = createFilter(); + Context mapped = mapContext(filter, "a", "alpha"); + assertThat(mapped.getLowCardinalityKeyValues()).containsExactly(KeyValue.of("a", "alpha")); + } + + @Test + void shouldAddKeyValues() { + PropertiesObservationFilter filter = createFilter("b", "beta"); + Context mapped = mapContext(filter, "a", "alpha"); + assertThat(mapped.getLowCardinalityKeyValues()).containsExactly(KeyValue.of("a", "alpha"), + KeyValue.of("b", "beta")); + } + + private static Context mapContext(PropertiesObservationFilter filter, String... initialKeyValues) { + Context context = new Context(); + context.addLowCardinalityKeyValues(KeyValues.of(initialKeyValues)); + return filter.map(context); + } + + private static PropertiesObservationFilter createFilter(String... keyValues) { + ObservationProperties properties = new ObservationProperties(); + for (int i = 0; i < keyValues.length; i += 2) { + properties.getKeyValues().put(keyValues[i], keyValues[i + 1]); + } + return new PropertiesObservationFilter(properties); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc index 2f9ce53ec614..96d671570bc2 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc @@ -1100,19 +1100,8 @@ These use the global registry that is not Spring-managed. [[actuator.metrics.customizing.common-tags]] ==== Common Tags -Common tags are generally used for dimensional drill-down on the operating environment, such as host, instance, region, stack, and others. -Commons tags are applied to all meters and can be configured, as the following example shows: -[source,yaml,indent=0,subs="verbatim",configprops,configblocks] ----- - management: - metrics: - tags: - region: "us-east-1" - stack: "prod" ----- - -The preceding example adds `region` and `stack` tags to all meters with a value of `us-east-1` and `prod`, respectively. +You can configure common tags using the <>. NOTE: The order of common tags is important if you use Graphite. As the order of common tags cannot be guaranteed by using this approach, Graphite users are advised to define a custom `MeterFilter` instead. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc index b7db70d50219..f077a1409c23 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc @@ -9,9 +9,9 @@ To create your own observations (which will lead to metrics and traces), you can include::code:MyCustomObservation[] -NOTE: Low cardinality tags will be added to metrics and traces, while high cardinality tags will only be added to traces. +NOTE: Low cardinality key-values will be added to metrics and traces, while high cardinality key-values will only be added to traces. -Beans of type `ObservationPredicate`, `GlobalObservationConvention` and `ObservationHandler` will be automatically registered on the `ObservationRegistry`. +Beans of type `ObservationPredicate`, `GlobalObservationConvention`, `ObservationFilter` and `ObservationHandler` will be automatically registered on the `ObservationRegistry`. You can additionally register any number of `ObservationRegistryCustomizer` beans to further configure the registry. For more details please see the https://micrometer.io/docs/observation[Micrometer Observation documentation]. @@ -21,4 +21,20 @@ For JDBC, the https://github.com/jdbc-observations/datasource-micrometer[Datasou Read more about it https://jdbc-observations.github.io/datasource-micrometer/docs/current/docs/html/[in the reference documentation]. For R2DBC, the https://github.com/spring-projects-experimental/r2dbc-micrometer-spring-boot[Spring Boot Auto Configuration for R2DBC Observation] creates observations for R2DBC query invocations. +[[actuator.observability.common-key-values]] +=== Common Key-Values +Common key-values are generally used for dimensional drill-down on the operating environment, such as host, instance, region, stack, and others. +Commons key-values are applied to all observations as low cardinality key-values and can be configured, as the following example shows: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + observations: + key-values: + region: "us-east-1" + stack: "prod" +---- + +The preceding example adds `region` and `stack` key-values to all observations with a value of `us-east-1` and `prod`, respectively. + The next sections will provide more details about logging, metrics and traces. From 491e12ab5e8a0cec272ac7e88e7b3edaee0130ba Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 13 Jun 2023 10:42:52 +0200 Subject: [PATCH 0004/1215] Add property to disable Spring Security observations Setting 'management.observations.spring-security.enabled' installs an ObservationPredicate, which prevents all observations starting with 'spring.security.' to be created. Closes gh-34802 --- .../ObservationAutoConfiguration.java | 7 ++++ ...itional-spring-configuration-metadata.json | 6 ++++ .../ObservationAutoConfigurationTests.java | 22 +++++++++++++ .../docs/asciidoc/actuator/observability.adoc | 12 +++++++ .../MyObservationPredicate.java | 32 +++++++++++++++++++ 5 files changed, 79 insertions(+) create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/observability/preventingobservations/MyObservationPredicate.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java index 440fc9bf9656..5e88d1fcc615 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java @@ -40,6 +40,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -82,6 +83,12 @@ PropertiesObservationFilter propertiesObservationFilter(ObservationProperties pr return new PropertiesObservationFilter(properties); } + @Bean + @ConditionalOnProperty(name = "management.observations.spring-security.enabled", havingValue = "false") + ObservationPredicate springSecurityObservationsDisabler() { + return (name, context) -> !name.startsWith("spring.security."); + } + @Configuration(proxyBeanMethods = false) @ConditionalOnClass(MeterRegistry.class) @ConditionalOnMissingClass("io.micrometer.tracing.Tracer") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 9a15c30b96cf..14f14f7c924c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -2038,6 +2038,12 @@ "level": "error" } }, + { + "name": "management.observations.spring-security.enabled", + "description": "Whether to enable observations for Spring Security", + "type": "java.lang.Boolean", + "defaultValue": true + }, { "name": "management.otlp.tracing.compression", "defaultValue": "none" diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java index 186adde6056a..fd7199af72aa 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java @@ -291,6 +291,28 @@ void autoConfiguresObservationHandlerWhenTracingIsActive() { }); } + @Test + void shouldNotDisableSpringSecurityObservationsByDefault() { + this.contextRunner.run((context) -> { + ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); + Observation.start("spring.security.filterchains", observationRegistry).stop(); + MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); + assertThat(meterRegistry.get("spring.security.filterchains").timer().count()).isOne(); + }); + } + + @Test + void shouldDisableSpringSecurityObservationsIfPropertyIsSet() { + this.contextRunner.withPropertyValues("management.observations.spring-security.enabled=false") + .run((context) -> { + ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); + Observation.start("spring.security.filterchains", observationRegistry).stop(); + MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); + assertThatThrownBy(() -> meterRegistry.get("spring.security.filterchains").timer()) + .isInstanceOf(MeterNotFoundException.class); + }); + } + @Configuration(proxyBeanMethods = false) static class ObservationPredicates { diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc index f077a1409c23..10d8ec732207 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc @@ -37,4 +37,16 @@ Commons key-values are applied to all observations as low cardinality key-values The preceding example adds `region` and `stack` key-values to all observations with a value of `us-east-1` and `prod`, respectively. +[[actuator.observability.preventing-observations]] +=== Preventing Observations + +If you'd like to prevent some observations from being reported, you can register beans of type `ObservationPredicate`. +Observations are only reported if all the `ObservationPredicate` beans return `true` for that observation. + +include::code:MyObservationPredicate[] + +The preceding example will prevent all observations with a name starting with "denied.prefix.". + +TIP: If you want to prevent Spring Security from reporting observations, set the property configprop:management.observations.spring-security.enabled[] to `false`. + The next sections will provide more details about logging, metrics and traces. diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/observability/preventingobservations/MyObservationPredicate.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/observability/preventingobservations/MyObservationPredicate.java new file mode 100644 index 000000000000..17a5543dc34b --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/observability/preventingobservations/MyObservationPredicate.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2023 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.docs.actuator.observability.preventingobservations; + +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationPredicate; + +import org.springframework.stereotype.Component; + +@Component +class MyObservationPredicate implements ObservationPredicate { + + @Override + public boolean test(String name, Context context) { + return !name.startsWith("denied.prefix."); + } + +} From 7b90fbb0b2ef73871ca776d9f999e9cdc1cb7e6d Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 13 Jun 2023 12:01:39 +0200 Subject: [PATCH 0005/1215] Add property to specify the order of ServerHttpObservationFilter The property is named 'management.observations.http.server.filter.order' Closes gh-35067 --- .../observation/ObservationProperties.java | 24 ++++++++++ .../OrderedServerHttpObservationFilter.java | 46 +++++++++++++++++++ .../WebFluxObservationAutoConfiguration.java | 10 ++-- ...FluxObservationAutoConfigurationTests.java | 10 ++++ 4 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/OrderedServerHttpObservationFilter.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java index 92f8e8b0fe2d..27c2473cc635 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java @@ -20,6 +20,7 @@ import java.util.Map; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.Ordered; /** * {@link ConfigurationProperties @ConfigurationProperties} for configuring Micrometer @@ -96,10 +97,16 @@ public static class Server { private final ServerRequests requests = new ServerRequests(); + private final Filter filter = new Filter(); + public ServerRequests getRequests() { return this.requests; } + public Filter getFilter() { + return this.filter; + } + public static class ServerRequests { /** @@ -118,6 +125,23 @@ public void setName(String name) { } + public static class Filter { + + /** + * Order of the filter that creates the observations. + */ + private int order = Ordered.HIGHEST_PRECEDENCE + 1; + + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + + } + } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/OrderedServerHttpObservationFilter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/OrderedServerHttpObservationFilter.java new file mode 100644 index 000000000000..9d3146f1dbee --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/OrderedServerHttpObservationFilter.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.observation.web.reactive; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.boot.web.reactive.filter.OrderedWebFilter; +import org.springframework.core.Ordered; +import org.springframework.http.server.reactive.observation.ServerRequestObservationConvention; +import org.springframework.web.filter.reactive.ServerHttpObservationFilter; + +/** + * {@link ServerHttpObservationFilter} that also implements {@link Ordered}. + * + * @author Moritz Halbritter + */ +class OrderedServerHttpObservationFilter extends ServerHttpObservationFilter implements OrderedWebFilter { + + private final int order; + + OrderedServerHttpObservationFilter(ObservationRegistry observationRegistry, + ServerRequestObservationConvention observationConvention, int order) { + super(observationRegistry, observationConvention); + this.order = order; + } + + @Override + public int getOrder() { + return this.order; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java index a998e6b5d24b..83c67a60736e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java @@ -42,7 +42,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention; import org.springframework.http.server.reactive.observation.ServerRequestObservationConvention; @@ -55,6 +54,7 @@ * @author Brian Clozel * @author Jon Schneider * @author Dmytro Nosan + * @author Moritz Halbritter * @since 3.0.0 */ @AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, @@ -77,9 +77,8 @@ public WebFluxObservationAutoConfiguration(MetricsProperties metricsProperties, } @Bean - @ConditionalOnMissingBean - @Order(Ordered.HIGHEST_PRECEDENCE + 1) - public ServerHttpObservationFilter webfluxObservationFilter(ObservationRegistry registry, + @ConditionalOnMissingBean(ServerHttpObservationFilter.class) + public OrderedServerHttpObservationFilter webfluxObservationFilter(ObservationRegistry registry, ObjectProvider customConvention, ObjectProvider tagConfigurer, ObjectProvider contributorsProvider) { @@ -90,7 +89,8 @@ public ServerHttpObservationFilter webfluxObservationFilter(ObservationRegistry List tagsContributors = contributorsProvider.orderedStream().toList(); ServerRequestObservationConvention convention = createConvention(customConvention.getIfAvailable(), name, tagsProvider, tagsContributors); - return new ServerHttpObservationFilter(registry, convention); + int order = this.observationProperties.getHttp().getServer().getFilter().getOrder(); + return new OrderedServerHttpObservationFilter(registry, convention, order); } private static ServerRequestObservationConvention createConvention( diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java index 88609e23f686..d8197198eb47 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java @@ -57,6 +57,7 @@ * @author Brian Clozel * @author Dmytro Nosan * @author Madhura Bhave + * @author Moritz Halbritter */ @ExtendWith(OutputCaptureExtension.class) @SuppressWarnings("removal") @@ -169,6 +170,15 @@ void shouldNotDenyNorLogIfMaxUrisIsNotReached(CapturedOutput output) { }); } + @Test + void shouldUsePropertyForServerHttpObservationFilterOrder() { + this.contextRunner.withPropertyValues("management.observations.http.server.filter.order=1000") + .run((context) -> { + OrderedServerHttpObservationFilter bean = context.getBean(OrderedServerHttpObservationFilter.class); + assertThat(bean.getOrder()).isEqualTo(1000); + }); + } + private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicationContext context) throws Exception { return getInitializedMeterRegistry(context, "/test0", "/test1", "/test2"); From f562ced79a0e854fe055266bfef02a02f1440045 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Jun 2023 09:52:37 +0100 Subject: [PATCH 0006/1215] Add CI on JDK 21 --- ci/images/ci-image-jdk21/Dockerfile | 11 ++++ ci/images/get-jdk-url.sh | 3 + ci/pipeline.yml | 89 ++++++++++++++++++++++++++++- 3 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 ci/images/ci-image-jdk21/Dockerfile diff --git a/ci/images/ci-image-jdk21/Dockerfile b/ci/images/ci-image-jdk21/Dockerfile new file mode 100644 index 000000000000..6636b2c19d24 --- /dev/null +++ b/ci/images/ci-image-jdk21/Dockerfile @@ -0,0 +1,11 @@ +FROM ubuntu:jammy-20230425 + +ADD setup.sh /setup.sh +ADD get-jdk-url.sh /get-jdk-url.sh +ADD get-docker-url.sh /get-docker-url.sh +ADD get-docker-compose-url.sh /get-docker-compose-url.sh +RUN ./setup.sh java17 java21 + +ENV JAVA_HOME /opt/openjdk +ENV PATH $JAVA_HOME/bin:$PATH +ADD docker-lib.sh /docker-lib.sh diff --git a/ci/images/get-jdk-url.sh b/ci/images/get-jdk-url.sh index ec2505e5b13d..6dc041e1d64e 100755 --- a/ci/images/get-jdk-url.sh +++ b/ci/images/get-jdk-url.sh @@ -8,6 +8,9 @@ case "$1" in java20) echo "https://github.com/bell-sw/Liberica/releases/download/20.0.1+10/bellsoft-jdk20.0.1+10-linux-amd64.tar.gz" ;; + java21) + echo "https://download.java.net/java/early_access/jdk21/25/GPL/openjdk-21-ea+25_linux-x64_bin.tar.gz" + ;; *) echo $"Unknown java version" exit 1 diff --git a/ci/pipeline.yml b/ci/pipeline.yml index 15109f6d2e39..14c4b2d87e11 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -175,6 +175,12 @@ resources: source: <<: *ci-registry-image-resource-source repository: ((docker-hub-organization))/spring-boot-ci-jdk20 +- name: ci-image-jdk21 + type: registry-image + icon: docker + source: + <<: *ci-registry-image-resource-source + repository: ((docker-hub-organization))/spring-boot-ci-jdk21 - name: paketo-builder-base-image type: registry-image icon: docker @@ -207,6 +213,14 @@ resources: access_token: ((github-ci-status-token)) branch: ((branch)) context: jdk20-build +- name: repo-status-jdk21-build + type: github-status-resource + icon: eye-check-outline + source: + repository: ((github-repo-name)) + access_token: ((github-ci-status-token)) + branch: ((branch)) + context: jdk21-build - name: slack-alert type: slack-notification icon: slack @@ -249,6 +263,13 @@ jobs: image: ci-image-jdk20 vars: ci-image-name: ci-image-jdk20 + - task: build-ci-image-jdk21 + privileged: true + file: git-repo/ci/tasks/build-ci-image.yml + output_mapping: + image: ci-image-jdk21 + vars: + ci-image-name: ci-image-jdk21 - in_parallel: - put: ci-image params: @@ -256,6 +277,9 @@ jobs: - put: ci-image-jdk20 params: image: ci-image-jdk20/image.tar + - put: ci-image-jdk21 + params: + image: ci-image-jdk21/image.tar - name: detect-jdk-updates plan: - get: git-repo @@ -366,6 +390,38 @@ jobs: - put: slack-alert params: <<: *slack-success-params +- name: jdk21-build + serial: true + public: true + plan: + - get: ci-image-jdk21 + - get: git-repo + trigger: true + - put: repo-status-jdk21-build + params: { state: "pending", commit: "git-repo" } + - do: + - task: build-project + image: ci-image-jdk21 + privileged: true + timeout: ((task-timeout)) + file: git-repo/ci/tasks/build-project.yml + params: + BRANCH: ((branch)) + TOOLCHAIN_JAVA_VERSION: 21 + <<: *gradle-enterprise-task-params + <<: *docker-hub-task-params + on_failure: + do: + - put: repo-status-jdk21-build + params: { state: "failure", commit: "git-repo" } + - put: slack-alert + params: + <<: *slack-fail-params + - put: repo-status-jdk21-build + params: { state: "success", commit: "git-repo" } + - put: slack-alert + params: + <<: *slack-success-params - name: windows-build serial: true plan: @@ -662,13 +718,42 @@ jobs: - put: slack-alert params: <<: *slack-success-params +- name: jdk21-run-system-tests + serial: true + public: true + plan: + - get: ci-image-jdk21 + - get: git-repo + - get: paketo-builder-base-image + trigger: true + - get: daily + trigger: true + - do: + - task: run-system-tests + image: ci-image-jdk21 + privileged: true + timeout: ((task-timeout)) + file: git-repo/ci/tasks/run-system-tests.yml + params: + BRANCH: ((branch)) + TOOLCHAIN_JAVA_VERSION: 21 + <<: *gradle-enterprise-task-params + <<: *docker-hub-task-params + on_failure: + do: + - put: slack-alert + params: + <<: *slack-fail-params + - put: slack-alert + params: + <<: *slack-success-params groups: - name: "builds" - jobs: ["build", "jdk20-build", "windows-build"] + jobs: ["build", "jdk20-build", "jdk21-build", "windows-build"] - name: "releases" jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone", "promote-rc", "promote-release", "create-github-release", "publish-gradle-plugin", "publish-to-sdkman", "update-homebrew-tap"] - name: "system-tests" - jobs: ["run-system-tests", "jdk20-run-system-tests"] + jobs: ["run-system-tests", "jdk20-run-system-tests", "jdk21-run-system-tests"] - name: "ci-images" jobs: ["build-ci-images", "detect-docker-updates", "detect-jdk-updates", "detect-ubuntu-image-updates"] From c73315b4a31da90ab60cacb0da46f82155aab7a7 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 13 Jun 2023 13:59:38 +0200 Subject: [PATCH 0007/1215] Add property to prevent observations starting with a prefix For example, setting management.observations.enable.denied.prefix=false will prevent all observations starting with 'denied.prefix' Closes gh-34802 --- .../ObservationAutoConfiguration.java | 11 +- .../observation/ObservationProperties.java | 15 +++ .../PropertiesObservationFilter.java | 51 --------- .../PropertiesObservationFilterPredicate.java | 83 +++++++++++++++ ...itional-spring-configuration-metadata.json | 6 -- .../ObservationAutoConfigurationTests.java | 18 ++-- ...ertiesObservationFilterPredicateTests.java | 100 ++++++++++++++++++ .../PropertiesObservationFilterTests.java | 62 ----------- .../docs/asciidoc/actuator/observability.adoc | 24 ++++- .../MyObservationPredicate.java | 2 +- 10 files changed, 230 insertions(+), 142 deletions(-) delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilter.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilterPredicate.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilterPredicateTests.java delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilterTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java index 5e88d1fcc615..a164afa63e84 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java @@ -40,7 +40,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -79,14 +78,8 @@ ObservationRegistry observationRegistry() { @Bean @Order(0) - PropertiesObservationFilter propertiesObservationFilter(ObservationProperties properties) { - return new PropertiesObservationFilter(properties); - } - - @Bean - @ConditionalOnProperty(name = "management.observations.spring-security.enabled", havingValue = "false") - ObservationPredicate springSecurityObservationsDisabler() { - return (name, context) -> !name.startsWith("spring.security."); + PropertiesObservationFilterPredicate propertiesObservationFilter(ObservationProperties properties) { + return new PropertiesObservationFilterPredicate(properties); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java index 27c2473cc635..a60bdb700762 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java @@ -27,6 +27,7 @@ * observations. * * @author Brian Clozel + * @author Moritz Halbritter * @since 3.0.0 */ @ConfigurationProperties("management.observations") @@ -39,6 +40,20 @@ public class ObservationProperties { */ private Map keyValues = new LinkedHashMap<>(); + /** + * Whether observations starting with the specified name should be enabled. The + * longest match wins, the key 'all' can also be used to configure all observations. + */ + private Map enable = new LinkedHashMap<>(); + + public Map getEnable() { + return this.enable; + } + + public void setEnable(Map enable) { + this.enable = enable; + } + public Http getHttp() { return this.http; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilter.java deleted file mode 100644 index d1116aa4ddb5..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilter.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2012-2023 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.actuate.autoconfigure.observation; - -import java.util.Map.Entry; - -import io.micrometer.common.KeyValues; -import io.micrometer.observation.Observation.Context; -import io.micrometer.observation.ObservationFilter; - -/** - * {@link ObservationFilter} to apply settings from {@link ObservationProperties}. - * - * @author Moritz Halbritter - */ -class PropertiesObservationFilter implements ObservationFilter { - - private final ObservationFilter delegate; - - PropertiesObservationFilter(ObservationProperties properties) { - this.delegate = createDelegate(properties); - } - - @Override - public Context map(Context context) { - return this.delegate.map(context); - } - - private static ObservationFilter createDelegate(ObservationProperties properties) { - if (properties.getKeyValues().isEmpty()) { - return (context) -> context; - } - KeyValues keyValues = KeyValues.of(properties.getKeyValues().entrySet(), Entry::getKey, Entry::getValue); - return (context) -> context.addLowCardinalityKeyValues(keyValues); - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilterPredicate.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilterPredicate.java new file mode 100644 index 000000000000..1154668798af --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilterPredicate.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.observation; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Supplier; + +import io.micrometer.common.KeyValues; +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationFilter; +import io.micrometer.observation.ObservationPredicate; + +import org.springframework.util.StringUtils; + +/** + * {@link ObservationFilter} to apply settings from {@link ObservationProperties}. + * + * @author Moritz Halbritter + */ +class PropertiesObservationFilterPredicate implements ObservationFilter, ObservationPredicate { + + private final ObservationFilter commonKeyValuesFilter; + + private final ObservationProperties properties; + + PropertiesObservationFilterPredicate(ObservationProperties properties) { + this.properties = properties; + this.commonKeyValuesFilter = createCommonKeyValuesFilter(properties); + } + + @Override + public Context map(Context context) { + return this.commonKeyValuesFilter.map(context); + } + + @Override + public boolean test(String name, Context context) { + return lookupWithFallbackToAll(this.properties.getEnable(), name, true); + } + + private static T lookupWithFallbackToAll(Map values, String name, T defaultValue) { + if (values.isEmpty()) { + return defaultValue; + } + return doLookup(values, name, () -> values.getOrDefault("all", defaultValue)); + } + + private static T doLookup(Map values, String name, Supplier defaultValue) { + while (StringUtils.hasLength(name)) { + T result = values.get(name); + if (result != null) { + return result; + } + int lastDot = name.lastIndexOf('.'); + name = (lastDot != -1) ? name.substring(0, lastDot) : ""; + } + return defaultValue.get(); + } + + private static ObservationFilter createCommonKeyValuesFilter(ObservationProperties properties) { + if (properties.getKeyValues().isEmpty()) { + return (context) -> context; + } + KeyValues keyValues = KeyValues.of(properties.getKeyValues().entrySet(), Entry::getKey, Entry::getValue); + return (context) -> context.addLowCardinalityKeyValues(keyValues); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 14f14f7c924c..9a15c30b96cf 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -2038,12 +2038,6 @@ "level": "error" } }, - { - "name": "management.observations.spring-security.enabled", - "description": "Whether to enable observations for Spring Security", - "type": "java.lang.Boolean", - "defaultValue": true - }, { "name": "management.otlp.tracing.compression", "defaultValue": "none" diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java index fd7199af72aa..eb994e6896d1 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java @@ -191,7 +191,8 @@ void autoConfiguresObservationFilters() { @Test void shouldSupplyPropertiesObservationFilterBean() { - this.contextRunner.run((context) -> assertThat(context).hasSingleBean(PropertiesObservationFilter.class)); + this.contextRunner + .run((context) -> assertThat(context).hasSingleBean(PropertiesObservationFilterPredicate.class)); } @Test @@ -303,14 +304,13 @@ void shouldNotDisableSpringSecurityObservationsByDefault() { @Test void shouldDisableSpringSecurityObservationsIfPropertyIsSet() { - this.contextRunner.withPropertyValues("management.observations.spring-security.enabled=false") - .run((context) -> { - ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); - Observation.start("spring.security.filterchains", observationRegistry).stop(); - MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); - assertThatThrownBy(() -> meterRegistry.get("spring.security.filterchains").timer()) - .isInstanceOf(MeterNotFoundException.class); - }); + this.contextRunner.withPropertyValues("management.observations.enable.spring.security=false").run((context) -> { + ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); + Observation.start("spring.security.filterchains", observationRegistry).stop(); + MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); + assertThatThrownBy(() -> meterRegistry.get("spring.security.filterchains").timer()) + .isInstanceOf(MeterNotFoundException.class); + }); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilterPredicateTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilterPredicateTests.java new file mode 100644 index 000000000000..4afd4f601e67 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilterPredicateTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.observation; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; +import io.micrometer.observation.Observation.Context; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PropertiesObservationFilterPredicate}. + * + * @author Moritz Halbritter + */ +class PropertiesObservationFilterPredicateTests { + + @Test + void shouldDoNothingIfKeyValuesAreEmpty() { + PropertiesObservationFilterPredicate filter = createFilter(); + Context mapped = mapContext(filter, "a", "alpha"); + assertThat(mapped.getLowCardinalityKeyValues()).containsExactly(KeyValue.of("a", "alpha")); + } + + @Test + void shouldAddKeyValues() { + PropertiesObservationFilterPredicate filter = createFilter("b", "beta"); + Context mapped = mapContext(filter, "a", "alpha"); + assertThat(mapped.getLowCardinalityKeyValues()).containsExactly(KeyValue.of("a", "alpha"), + KeyValue.of("b", "beta")); + } + + @Test + void shouldFilter() { + PropertiesObservationFilterPredicate predicate = createPredicate("spring.security"); + Context context = new Context(); + assertThat(predicate.test("spring.security.filterchains", context)).isFalse(); + assertThat(predicate.test("spring.security", context)).isFalse(); + assertThat(predicate.test("spring.data", context)).isTrue(); + assertThat(predicate.test("spring", context)).isTrue(); + } + + @Test + void filterShouldFallbackToAll() { + PropertiesObservationFilterPredicate predicate = createPredicate("all"); + Context context = new Context(); + assertThat(predicate.test("spring.security.filterchains", context)).isFalse(); + assertThat(predicate.test("spring.security", context)).isFalse(); + assertThat(predicate.test("spring.data", context)).isFalse(); + assertThat(predicate.test("spring", context)).isFalse(); + } + + @Test + void shouldNotFilterIfDisabledNamesIsEmpty() { + PropertiesObservationFilterPredicate predicate = createPredicate(); + Context context = new Context(); + assertThat(predicate.test("spring.security.filterchains", context)).isTrue(); + assertThat(predicate.test("spring.security", context)).isTrue(); + assertThat(predicate.test("spring.data", context)).isTrue(); + assertThat(predicate.test("spring", context)).isTrue(); + } + + private static Context mapContext(PropertiesObservationFilterPredicate filter, String... initialKeyValues) { + Context context = new Context(); + context.addLowCardinalityKeyValues(KeyValues.of(initialKeyValues)); + return filter.map(context); + } + + private static PropertiesObservationFilterPredicate createFilter(String... keyValues) { + ObservationProperties properties = new ObservationProperties(); + for (int i = 0; i < keyValues.length; i += 2) { + properties.getKeyValues().put(keyValues[i], keyValues[i + 1]); + } + return new PropertiesObservationFilterPredicate(properties); + } + + private static PropertiesObservationFilterPredicate createPredicate(String... disabledNames) { + ObservationProperties properties = new ObservationProperties(); + for (String name : disabledNames) { + properties.getEnable().put(name, false); + } + return new PropertiesObservationFilterPredicate(properties); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilterTests.java deleted file mode 100644 index b352f9695d1a..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/PropertiesObservationFilterTests.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2012-2023 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.actuate.autoconfigure.observation; - -import io.micrometer.common.KeyValue; -import io.micrometer.common.KeyValues; -import io.micrometer.observation.Observation.Context; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link PropertiesObservationFilter}. - * - * @author Moritz Halbritter - */ -class PropertiesObservationFilterTests { - - @Test - void shouldDoNothingIfKeyValuesAreEmpty() { - PropertiesObservationFilter filter = createFilter(); - Context mapped = mapContext(filter, "a", "alpha"); - assertThat(mapped.getLowCardinalityKeyValues()).containsExactly(KeyValue.of("a", "alpha")); - } - - @Test - void shouldAddKeyValues() { - PropertiesObservationFilter filter = createFilter("b", "beta"); - Context mapped = mapContext(filter, "a", "alpha"); - assertThat(mapped.getLowCardinalityKeyValues()).containsExactly(KeyValue.of("a", "alpha"), - KeyValue.of("b", "beta")); - } - - private static Context mapContext(PropertiesObservationFilter filter, String... initialKeyValues) { - Context context = new Context(); - context.addLowCardinalityKeyValues(KeyValues.of(initialKeyValues)); - return filter.map(context); - } - - private static PropertiesObservationFilter createFilter(String... keyValues) { - ObservationProperties properties = new ObservationProperties(); - for (int i = 0; i < keyValues.length; i += 2) { - properties.getKeyValues().put(keyValues[i], keyValues[i + 1]); - } - return new PropertiesObservationFilter(properties); - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc index 10d8ec732207..e8c900d161e9 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc @@ -40,13 +40,29 @@ The preceding example adds `region` and `stack` key-values to all observations w [[actuator.observability.preventing-observations]] === Preventing Observations -If you'd like to prevent some observations from being reported, you can register beans of type `ObservationPredicate`. +If you'd like to prevent some observations from being reported, you can use the configprop:management.observations.enable[] properties: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + observations: + enable: + denied: + prefix: false + another: + denied: + prefix: false +---- + +The preceding example will prevent all observations with a name starting with `denied.prefix` or `another.denied.prefix`. + +TIP: If you want to prevent Spring Security from reporting observations, set the property configprop:management.observations.enable.spring.security[] to `false`. + +If you need greater control over the prevention of observations, you can register beans of type `ObservationPredicate`. Observations are only reported if all the `ObservationPredicate` beans return `true` for that observation. include::code:MyObservationPredicate[] -The preceding example will prevent all observations with a name starting with "denied.prefix.". - -TIP: If you want to prevent Spring Security from reporting observations, set the property configprop:management.observations.spring-security.enabled[] to `false`. +The preceding example will prevent all observations whose name contains "denied". The next sections will provide more details about logging, metrics and traces. diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/observability/preventingobservations/MyObservationPredicate.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/observability/preventingobservations/MyObservationPredicate.java index 17a5543dc34b..f065c8e2ca83 100644 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/observability/preventingobservations/MyObservationPredicate.java +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/actuator/observability/preventingobservations/MyObservationPredicate.java @@ -26,7 +26,7 @@ class MyObservationPredicate implements ObservationPredicate { @Override public boolean test(String name, Context context) { - return !name.startsWith("denied.prefix."); + return !name.contains("denied"); } } From fb64f6744e9c0026e5892d19a3006e9c14b75546 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 6 Jun 2023 09:38:50 +0200 Subject: [PATCH 0008/1215] Add 21 to JavaVersion See gh-35892 --- .../java/org/springframework/boot/system/JavaVersion.java | 8 +++++++- .../org/springframework/boot/system/JavaVersionTests.java | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/system/JavaVersion.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/system/JavaVersion.java index feda02e0099b..3ebcfbf10fc8 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/system/JavaVersion.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/system/JavaVersion.java @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.SortedSet; import java.util.concurrent.Future; import org.springframework.util.ClassUtils; @@ -53,7 +54,12 @@ public enum JavaVersion { /** * Java 20. */ - TWENTY("20", Class.class, "accessFlags"); + TWENTY("20", Class.class, "accessFlags"), + + /** + * Java 21. + */ + TWENTY_ONE("21", SortedSet.class, "getFirst"); private final String name; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/system/JavaVersionTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/system/JavaVersionTests.java index 0b0787b9feab..d87c1230f700 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/system/JavaVersionTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/system/JavaVersionTests.java @@ -103,4 +103,10 @@ void currentJavaVersionTwenty() { assertThat(JavaVersion.getJavaVersion()).isEqualTo(JavaVersion.TWENTY); } + @Test + @EnabledOnJre(JRE.JAVA_21) + void currentJavaVersionTwentyOne() { + assertThat(JavaVersion.getJavaVersion()).isEqualTo(JavaVersion.TWENTY_ONE); + } + } From 6e604ad65c6ab0e0da90eae3ae9994c78b031768 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 6 Jun 2023 09:39:16 +0200 Subject: [PATCH 0009/1215] Implement @ConditionalOnVirtualThreads Closes gh-35892 --- .../DocumentConfigurationProperties.java | 1 + .../ConditionalOnVirtualThreads.java | 42 +++++++++++ ...itional-spring-configuration-metadata.json | 6 ++ .../ConditionalOnVirtualThreadsTests.java | 70 +++++++++++++++++++ 4 files changed, 119 insertions(+) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnVirtualThreads.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnVirtualThreadsTests.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 d7c8710e2cb4..8019eb3a27bd 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 @@ -106,6 +106,7 @@ private void corePrefixes(Config config) { config.accept("spring.reactor"); config.accept("spring.ssl"); config.accept("spring.task"); + config.accept("spring.threads"); config.accept("spring.mandatory-file-encoding"); config.accept("info"); config.accept("spring.output.ansi.enabled"); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnVirtualThreads.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnVirtualThreads.java new file mode 100644 index 000000000000..cbc80beaa653 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnVirtualThreads.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2022 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.condition; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.system.JavaVersion; +import org.springframework.context.annotation.Conditional; + +/** + * {@link Conditional @Conditional} that only matches when virtual threads are available + * and enabled. + * + * @author Moritz Halbritter + * @since 3.2.0 + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ConditionalOnJava(JavaVersion.TWENTY_ONE) +@ConditionalOnProperty(name = "spring.threads.virtual.enabled", havingValue = "true") +public @interface ConditionalOnVirtualThreads { + +} 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 d4c480df6479..dae3a6e6cdf0 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 @@ -2825,6 +2825,12 @@ "name": "spring.sql.init.mode", "defaultValue": "embedded" }, + { + "name": "spring.threads.virtual.enabled", + "type": "java.lang.Boolean", + "description": "Whether to use virtual threads.", + "defaultValue": false + }, { "name": "spring.thymeleaf.prefix", "defaultValue": "classpath:/templates/" diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnVirtualThreadsTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnVirtualThreadsTests.java new file mode 100644 index 000000000000..dd05fa5b94ac --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnVirtualThreadsTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-2019 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.condition; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; + +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConditionalOnVirtualThreads @ConditionalOnVirtualThreads}. + * + * @author Moritz Halbritter + */ +class ConditionalOnVirtualThreadsTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(BasicConfiguration.class); + + @Test + @EnabledForJreRange(max = JRE.JAVA_20) + void isDisabledOnJdkBelow21EvenIfPropertyIsSet() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true") + .run((context) -> assertThat(context).doesNotHaveBean("someBean")); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void isDisabledOnJdk21IfPropertyIsNotSet() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean("someBean")); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void isEnabledOnJdk21IfPropertyIsSet() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true") + .run((context) -> assertThat(context).hasBean("someBean")); + } + + @Configuration(proxyBeanMethods = false) + static class BasicConfiguration { + + @Bean + @ConditionalOnVirtualThreads + String someBean() { + return "someBean"; + } + + } + +} From f81787e65d9e3fd57c8b5da092645d59b95db279 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 14 Jun 2023 16:35:33 +0200 Subject: [PATCH 0010/1215] Enable virtual threads on Tomcat Closes gh-35704 --- ...veManagementChildContextConfiguration.java | 4 +- ...etManagementChildContextConfiguration.java | 6 +- ...verFactoryCustomizerAutoConfiguration.java | 7 ++ ...tualThreadsWebServerFactoryCustomizer.java | 47 ++++++++++++++ .../TomcatWebServerFactoryCustomizer.java | 4 +- ...hreadsWebServerFactoryCustomizerTests.java | 64 +++++++++++++++++++ 6 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatVirtualThreadsWebServerFactoryCustomizer.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatVirtualThreadsWebServerFactoryCustomizerTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementChildContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementChildContextConfiguration.java index 657f9dbda20d..faa2522016ea 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementChildContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementChildContextConfiguration.java @@ -28,6 +28,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.web.embedded.JettyWebServerFactoryCustomizer; import org.springframework.boot.autoconfigure.web.embedded.NettyWebServerFactoryCustomizer; +import org.springframework.boot.autoconfigure.web.embedded.TomcatVirtualThreadsWebServerFactoryCustomizer; import org.springframework.boot.autoconfigure.web.embedded.TomcatWebServerFactoryCustomizer; import org.springframework.boot.autoconfigure.web.embedded.UndertowWebServerFactoryCustomizer; import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryCustomizer; @@ -76,7 +77,8 @@ static class ReactiveManagementWebServerFactoryCustomizer ReactiveManagementWebServerFactoryCustomizer(ListableBeanFactory beanFactory) { super(beanFactory, ReactiveWebServerFactoryCustomizer.class, TomcatWebServerFactoryCustomizer.class, - TomcatReactiveWebServerFactoryCustomizer.class, JettyWebServerFactoryCustomizer.class, + TomcatReactiveWebServerFactoryCustomizer.class, + TomcatVirtualThreadsWebServerFactoryCustomizer.class, JettyWebServerFactoryCustomizer.class, UndertowWebServerFactoryCustomizer.class, NettyWebServerFactoryCustomizer.class); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ServletManagementChildContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ServletManagementChildContextConfiguration.java index 44de3225efc5..bc0b6c42c3b6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ServletManagementChildContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ServletManagementChildContextConfiguration.java @@ -40,6 +40,7 @@ import org.springframework.boot.autoconfigure.condition.SearchStrategy; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.embedded.JettyWebServerFactoryCustomizer; +import org.springframework.boot.autoconfigure.web.embedded.TomcatVirtualThreadsWebServerFactoryCustomizer; import org.springframework.boot.autoconfigure.web.embedded.TomcatWebServerFactoryCustomizer; import org.springframework.boot.autoconfigure.web.embedded.UndertowWebServerFactoryCustomizer; import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryCustomizer; @@ -122,8 +123,9 @@ static class ServletManagementWebServerFactoryCustomizer ServletManagementWebServerFactoryCustomizer(ListableBeanFactory beanFactory) { super(beanFactory, ServletWebServerFactoryCustomizer.class, TomcatServletWebServerFactoryCustomizer.class, - TomcatWebServerFactoryCustomizer.class, JettyWebServerFactoryCustomizer.class, - UndertowServletWebServerFactoryCustomizer.class, UndertowWebServerFactoryCustomizer.class); + TomcatWebServerFactoryCustomizer.class, TomcatVirtualThreadsWebServerFactoryCustomizer.class, + JettyWebServerFactoryCustomizer.class, UndertowServletWebServerFactoryCustomizer.class, + UndertowWebServerFactoryCustomizer.class); } @Override diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java index eca465353db6..300594855e37 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java @@ -29,6 +29,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnNotWarDeployment; +import org.springframework.boot.autoconfigure.condition.ConditionalOnVirtualThreads; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -62,6 +63,12 @@ public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environ return new TomcatWebServerFactoryCustomizer(environment, serverProperties); } + @Bean + @ConditionalOnVirtualThreads + TomcatVirtualThreadsWebServerFactoryCustomizer tomcatVirtualThreadsProtocolHandlerCustomizer() { + return new TomcatVirtualThreadsWebServerFactoryCustomizer(); + } + } /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatVirtualThreadsWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatVirtualThreadsWebServerFactoryCustomizer.java new file mode 100644 index 000000000000..9af929ffaa82 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatVirtualThreadsWebServerFactoryCustomizer.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2023 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.web.embedded; + +import org.apache.coyote.ProtocolHandler; +import org.apache.tomcat.util.threads.VirtualThreadExecutor; + +import org.springframework.boot.web.embedded.tomcat.ConfigurableTomcatWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.core.Ordered; + +/** + * Activates {@link VirtualThreadExecutor} on {@link ProtocolHandler Tomcat's protocol + * handler}. + * + * @author Moritz Halbritter + * @since 3.2.0 + */ +public class TomcatVirtualThreadsWebServerFactoryCustomizer + implements WebServerFactoryCustomizer, Ordered { + + @Override + public void customize(ConfigurableTomcatWebServerFactory factory) { + factory.addProtocolHandlerCustomizers( + (protocolHandler) -> protocolHandler.setExecutor(new VirtualThreadExecutor("tomcat-handler-"))); + } + + @Override + public int getOrder() { + return TomcatWebServerFactoryCustomizer.order + 1; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java index e47b7cccfe59..050edc743384 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java @@ -67,6 +67,8 @@ public class TomcatWebServerFactoryCustomizer implements WebServerFactoryCustomizer, Ordered { + static final int order = 0; + private final Environment environment; private final ServerProperties serverProperties; @@ -78,7 +80,7 @@ public TomcatWebServerFactoryCustomizer(Environment environment, ServerPropertie @Override public int getOrder() { - return 0; + return order; } @Override diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatVirtualThreadsWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatVirtualThreadsWebServerFactoryCustomizerTests.java new file mode 100644 index 000000000000..5fcf72d1f937 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatVirtualThreadsWebServerFactoryCustomizerTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2023 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.web.embedded; + +import java.util.function.Consumer; + +import org.apache.tomcat.util.threads.VirtualThreadExecutor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; + +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.embedded.tomcat.TomcatWebServer; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TomcatVirtualThreadsWebServerFactoryCustomizer}. + * + * @author Moritz Halbritter + */ +class TomcatVirtualThreadsWebServerFactoryCustomizerTests { + + private final TomcatVirtualThreadsWebServerFactoryCustomizer customizer = new TomcatVirtualThreadsWebServerFactoryCustomizer(); + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void shouldSetVirtualThreadExecutor() { + withWebServer((webServer) -> assertThat(webServer.getTomcat().getConnector().getProtocolHandler().getExecutor()) + .isInstanceOf(VirtualThreadExecutor.class)); + } + + private TomcatWebServer getWebServer() { + TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(0); + this.customizer.customize(factory); + return (TomcatWebServer) factory.getWebServer(); + } + + private void withWebServer(Consumer callback) { + TomcatWebServer webServer = getWebServer(); + webServer.start(); + try { + callback.accept(webServer); + } + finally { + webServer.stop(); + } + } + +} From 3e4a9f5204ee2fe989583b17b77d8dab8558282a Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 6 Jun 2023 09:39:33 +0200 Subject: [PATCH 0011/1215] Add property to limit maximum connections for Jetty Closes gh-35899 --- .../boot/autoconfigure/web/ServerProperties.java | 14 ++++++++++++++ .../embedded/JettyWebServerFactoryCustomizer.java | 1 + .../jetty/ConfigurableJettyWebServerFactory.java | 8 ++++++++ .../jetty/JettyReactiveWebServerFactory.java | 12 ++++++++++++ .../jetty/JettyServletWebServerFactory.java | 12 ++++++++++++ .../jetty/JettyReactiveWebServerFactoryTests.java | 13 +++++++++++++ .../jetty/JettyServletWebServerFactoryTests.java | 13 +++++++++++++ 7 files changed, 73 insertions(+) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index a4e5bcf2d1a8..681bd7d8384a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -1123,6 +1123,12 @@ public static class Jetty { */ private DataSize maxHttpResponseHeaderSize = DataSize.ofKilobytes(8); + /** + * Maximum number of connections that the server accepts and processes at any + * given time. + */ + private int maxConnections = -1; + public Accesslog getAccesslog() { return this.accesslog; } @@ -1155,6 +1161,14 @@ public void setMaxHttpResponseHeaderSize(DataSize maxHttpResponseHeaderSize) { this.maxHttpResponseHeaderSize = maxHttpResponseHeaderSize; } + public int getMaxConnections() { + return this.maxConnections; + } + + public void setMaxConnections(int maxConnections) { + this.maxConnections = maxConnections; + } + /** * Jetty access log properties. */ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java index 2248f1a72039..2c9d015cae91 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java @@ -81,6 +81,7 @@ public void customize(ConfigurableJettyWebServerFactory factory) { ServerProperties.Jetty.Threads threadProperties = properties.getThreads(); factory.setThreadPool(determineThreadPool(properties.getThreads())); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getMaxConnections).to(factory::setMaxConnections); map.from(threadProperties::getAcceptors).to(factory::setAcceptors); map.from(threadProperties::getSelectors).to(factory::setSelectors); map.from(this.serverProperties::getMaxHttpRequestHeaderSize) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/ConfigurableJettyWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/ConfigurableJettyWebServerFactory.java index 5f8a927b403a..65cae98947d0 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/ConfigurableJettyWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/ConfigurableJettyWebServerFactory.java @@ -25,6 +25,7 @@ * {@link ConfigurableWebServerFactory} for Jetty-specific features. * * @author Brian Clozel + * @author Moritz Halbritter * @since 2.0.0 * @see JettyServletWebServerFactory * @see JettyReactiveWebServerFactory @@ -63,4 +64,11 @@ public interface ConfigurableJettyWebServerFactory extends ConfigurableWebServer */ void addServerCustomizers(JettyServerCustomizer... customizers); + /** + * Sets the maximum number of concurrent connections. + * @param maxConnections the maximum number of concurrent connections + * @since 3.2.0 + */ + void setMaxConnections(int maxConnections); + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java index 3e8049f4d739..3102dd4aa3d4 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java @@ -29,6 +29,7 @@ import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.ConnectionLimit; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; @@ -55,6 +56,7 @@ * {@link ReactiveWebServerFactory} that can be used to create {@link JettyWebServer}s. * * @author Brian Clozel + * @author Moritz Halbritter * @since 2.0.0 */ public class JettyReactiveWebServerFactory extends AbstractReactiveWebServerFactory @@ -80,6 +82,8 @@ public class JettyReactiveWebServerFactory extends AbstractReactiveWebServerFact private ThreadPool threadPool; + private int maxConnections = -1; + /** * Create a new {@link JettyServletWebServerFactory} instance. */ @@ -118,6 +122,11 @@ public void addServerCustomizers(JettyServerCustomizer... customizers) { this.jettyServerCustomizers.addAll(Arrays.asList(customizers)); } + @Override + public void setMaxConnections(int maxConnections) { + this.maxConnections = maxConnections; + } + /** * Sets {@link JettyServerCustomizer}s that will be applied to the {@link Server} * before it is started. Calling this method will replace any existing customizers. @@ -180,6 +189,9 @@ protected Server createJettyServer(JettyHttpHandlerAdapter servlet) { contextHandler.addServlet(servletHolder, "/"); server.setHandler(addHandlerWrappers(contextHandler)); JettyReactiveWebServerFactory.logger.info("Server initialized with port: " + port); + if (this.maxConnections > -1) { + server.addBean(new ConnectionLimit(this.maxConnections, server)); + } if (Ssl.isEnabled(getSsl())) { customizeSsl(server, address); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java index e060a372f07c..4d93f13367f5 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java @@ -43,6 +43,7 @@ import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.ConnectionLimit; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConfiguration; @@ -101,6 +102,7 @@ * @author Eddú Meléndez * @author Venil Noronha * @author Henri Kerola + * @author Moritz Halbritter * @since 2.0.0 * @see #setPort(int) * @see #setConfigurations(Collection) @@ -129,6 +131,8 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor private ThreadPool threadPool; + private int maxConnections = -1; + /** * Create a new {@link JettyServletWebServerFactory} instance. */ @@ -163,6 +167,9 @@ public WebServer getWebServer(ServletContextInitializer... initializers) { configureWebAppContext(context, initializers); server.setHandler(addHandlerWrappers(context)); this.logger.info("Server initialized with port: " + port); + if (this.maxConnections > -1) { + server.addBean(new ConnectionLimit(this.maxConnections, server)); + } if (Ssl.isEnabled(getSsl())) { customizeSsl(server, address); } @@ -458,6 +465,11 @@ public void setSelectors(int selectors) { this.selectors = selectors; } + @Override + public void setMaxConnections(int maxConnections) { + this.maxConnections = maxConnections; + } + /** * Sets {@link JettyServerCustomizer}s that will be applied to the {@link Server} * before it is started. Calling this method will replace any existing customizers. diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java index 09ecc2c3cef7..cf42ba29d7a4 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java @@ -22,6 +22,7 @@ import java.util.Arrays; import org.awaitility.Awaitility; +import org.eclipse.jetty.server.ConnectionLimit; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -47,6 +48,7 @@ * * @author Brian Clozel * @author Madhura Bhave + * @author Moritz Halbritter */ @Servlet5ClassPathOverrides class JettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactoryTests { @@ -148,4 +150,15 @@ void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() { this.webServer.stop(); } + @Test + void shouldApplyMaxConnections() { + JettyReactiveWebServerFactory factory = getFactory(); + factory.setMaxConnections(1); + this.webServer = factory.getWebServer(new EchoHandler()); + Server server = ((JettyWebServer) this.webServer).getServer(); + ConnectionLimit connectionLimit = server.getBean(ConnectionLimit.class); + assertThat(connectionLimit).isNotNull(); + assertThat(connectionLimit.getMaxConnections()).isOne(); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java index 63fe24022315..f037202ca4e2 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java @@ -40,6 +40,7 @@ import org.apache.hc.core5.http.HttpResponse; import org.apache.jasper.servlet.JspServlet; import org.awaitility.Awaitility; +import org.eclipse.jetty.server.ConnectionLimit; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; @@ -86,6 +87,7 @@ * @author Dave Syer * @author Andy Wilkinson * @author Henri Kerola + * @author Moritz Halbritter */ @Servlet5ClassPathOverrides class JettyServletWebServerFactoryTests extends AbstractServletWebServerFactoryTests { @@ -518,6 +520,17 @@ public void configure(WebAppContext context) throws Exception { assertThat(context.getErrorHandler()).isInstanceOf(CustomErrorHandler.class); } + @Test + void shouldApplyMaxConnections() { + JettyServletWebServerFactory factory = getFactory(); + factory.setMaxConnections(1); + this.webServer = factory.getWebServer(); + Server server = ((JettyWebServer) this.webServer).getServer(); + ConnectionLimit connectionLimit = server.getBean(ConnectionLimit.class); + assertThat(connectionLimit).isNotNull(); + assertThat(connectionLimit.getMaxConnections()).isOne(); + } + private WebAppContext findWebAppContext(JettyWebServer webServer) { return findWebAppContext(webServer.getServer().getHandler()); } From 23979e6ccfefef35fafa95e09d43af275e510e3a Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 15 Jun 2023 10:13:19 +0200 Subject: [PATCH 0012/1215] Enable LoaderIntegrationTests on Java 21 --- .../boot/loader/LoaderIntegrationTests.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java index c01b5f40c343..7b6ce6e7d3d4 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java @@ -44,6 +44,7 @@ * Integration tests loader that supports fat jars. * * @author Phillip Webb + * @author Moritz Halbritter */ @DisabledIfDockerUnavailable @DisabledOnOs(os = { OS.LINUX, OS.MAC }, architecture = "aarch64", @@ -85,6 +86,7 @@ static Stream javaRuntimes() { javaRuntimes.add(JavaRuntime.openJdk(JavaVersion.SEVENTEEN)); javaRuntimes.add(JavaRuntime.openJdk(JavaVersion.TWENTY)); javaRuntimes.add(JavaRuntime.oracleJdk17()); + javaRuntimes.add(JavaRuntime.openJdkEarlyAccess(JavaVersion.TWENTY_ONE)); return javaRuntimes.stream().filter(JavaRuntime::isCompatible); } @@ -115,6 +117,13 @@ public String toString() { return this.name; } + static JavaRuntime openJdkEarlyAccess(JavaVersion version) { + String imageVersion = version.toString(); + DockerImageName image = DockerImageName.parse("openjdk:%s-ea-jdk".formatted(imageVersion)); + return new JavaRuntime("OpenJDK Early Access " + imageVersion, version, + () -> new GenericContainer<>(image)); + } + static JavaRuntime openJdk(JavaVersion version) { String imageVersion = version.toString(); DockerImageName image = DockerImageName.parse("bellsoft/liberica-openjdk-debian:" + imageVersion); From 140c37ceba3db80397c100dd40fac489d0774149 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 6 Jun 2023 09:39:33 +0200 Subject: [PATCH 0013/1215] Enable virtual threads on Jetty Closes gh-35703 --- ...veManagementChildContextConfiguration.java | 4 +- ...etManagementChildContextConfiguration.java | 5 +- ...verFactoryCustomizerAutoConfiguration.java | 7 +++ .../web/embedded/JettyThreadPool.java | 60 +++++++++++++++++++ ...tualThreadsWebServerFactoryCustomizer.java | 56 +++++++++++++++++ .../JettyWebServerFactoryCustomizer.java | 32 ++-------- ...hreadsWebServerFactoryCustomizerTests.java | 54 +++++++++++++++++ 7 files changed, 187 insertions(+), 31 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyThreadPool.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyVirtualThreadsWebServerFactoryCustomizer.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyVirtualThreadsWebServerFactoryCustomizerTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementChildContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementChildContextConfiguration.java index faa2522016ea..c0c618347eb5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementChildContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/reactive/ReactiveManagementChildContextConfiguration.java @@ -26,6 +26,7 @@ import org.springframework.boot.actuate.autoconfigure.web.server.ManagementWebServerFactoryCustomizer; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.autoconfigure.web.embedded.JettyVirtualThreadsWebServerFactoryCustomizer; import org.springframework.boot.autoconfigure.web.embedded.JettyWebServerFactoryCustomizer; import org.springframework.boot.autoconfigure.web.embedded.NettyWebServerFactoryCustomizer; import org.springframework.boot.autoconfigure.web.embedded.TomcatVirtualThreadsWebServerFactoryCustomizer; @@ -79,7 +80,8 @@ static class ReactiveManagementWebServerFactoryCustomizer super(beanFactory, ReactiveWebServerFactoryCustomizer.class, TomcatWebServerFactoryCustomizer.class, TomcatReactiveWebServerFactoryCustomizer.class, TomcatVirtualThreadsWebServerFactoryCustomizer.class, JettyWebServerFactoryCustomizer.class, - UndertowWebServerFactoryCustomizer.class, NettyWebServerFactoryCustomizer.class); + JettyVirtualThreadsWebServerFactoryCustomizer.class, UndertowWebServerFactoryCustomizer.class, + NettyWebServerFactoryCustomizer.class); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ServletManagementChildContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ServletManagementChildContextConfiguration.java index bc0b6c42c3b6..71beda39b6ba 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ServletManagementChildContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ServletManagementChildContextConfiguration.java @@ -39,6 +39,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.condition.SearchStrategy; import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.embedded.JettyVirtualThreadsWebServerFactoryCustomizer; import org.springframework.boot.autoconfigure.web.embedded.JettyWebServerFactoryCustomizer; import org.springframework.boot.autoconfigure.web.embedded.TomcatVirtualThreadsWebServerFactoryCustomizer; import org.springframework.boot.autoconfigure.web.embedded.TomcatWebServerFactoryCustomizer; @@ -124,8 +125,8 @@ static class ServletManagementWebServerFactoryCustomizer ServletManagementWebServerFactoryCustomizer(ListableBeanFactory beanFactory) { super(beanFactory, ServletWebServerFactoryCustomizer.class, TomcatServletWebServerFactoryCustomizer.class, TomcatWebServerFactoryCustomizer.class, TomcatVirtualThreadsWebServerFactoryCustomizer.class, - JettyWebServerFactoryCustomizer.class, UndertowServletWebServerFactoryCustomizer.class, - UndertowWebServerFactoryCustomizer.class); + JettyWebServerFactoryCustomizer.class, JettyVirtualThreadsWebServerFactoryCustomizer.class, + UndertowServletWebServerFactoryCustomizer.class, UndertowWebServerFactoryCustomizer.class); } @Override diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java index 300594855e37..846806696738 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java @@ -84,6 +84,13 @@ public JettyWebServerFactoryCustomizer jettyWebServerFactoryCustomizer(Environme return new JettyWebServerFactoryCustomizer(environment, serverProperties); } + @Bean + @ConditionalOnVirtualThreads + JettyVirtualThreadsWebServerFactoryCustomizer jettyVirtualThreadsWebServerFactoryCustomizer( + ServerProperties serverProperties) { + return new JettyVirtualThreadsWebServerFactoryCustomizer(serverProperties); + } + } /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyThreadPool.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyThreadPool.java new file mode 100644 index 000000000000..56d01c91a232 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyThreadPool.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2023 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.web.embedded; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.SynchronousQueue; + +import org.eclipse.jetty.util.BlockingArrayQueue; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.util.thread.ThreadPool; + +import org.springframework.boot.autoconfigure.web.ServerProperties; + +/** + * Creates a {@link ThreadPool} for Jetty, applying the + * {@link ServerProperties.Jetty.Threads} properties. + * + * @author Moritz Halbritter + */ +final class JettyThreadPool { + + private JettyThreadPool() { + } + + static QueuedThreadPool create(ServerProperties.Jetty.Threads properties) { + BlockingQueue queue = determineBlockingQueue(properties.getMaxQueueCapacity()); + int maxThreadCount = (properties.getMax() > 0) ? properties.getMax() : 200; + int minThreadCount = (properties.getMin() > 0) ? properties.getMin() : 8; + int threadIdleTimeout = (properties.getIdleTimeout() != null) ? (int) properties.getIdleTimeout().toMillis() + : 60000; + return new QueuedThreadPool(maxThreadCount, minThreadCount, threadIdleTimeout, queue); + } + + private static BlockingQueue determineBlockingQueue(Integer maxQueueCapacity) { + if (maxQueueCapacity == null) { + return null; + } + if (maxQueueCapacity == 0) { + return new SynchronousQueue<>(); + } + else { + return new BlockingArrayQueue<>(maxQueueCapacity); + } + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyVirtualThreadsWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyVirtualThreadsWebServerFactoryCustomizer.java new file mode 100644 index 000000000000..05720b3c82dc --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyVirtualThreadsWebServerFactoryCustomizer.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2023 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.web.embedded; + +import org.eclipse.jetty.util.VirtualThreads; +import org.eclipse.jetty.util.thread.QueuedThreadPool; + +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.web.embedded.jetty.ConfigurableJettyWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.core.Ordered; +import org.springframework.util.Assert; + +/** + * Activates virtual threads on the {@link ConfigurableJettyWebServerFactory}. + * + * @author Moritz Halbritter + * @since 3.2.0 + */ +public class JettyVirtualThreadsWebServerFactoryCustomizer + implements WebServerFactoryCustomizer, Ordered { + + private final ServerProperties serverProperties; + + public JettyVirtualThreadsWebServerFactoryCustomizer(ServerProperties serverProperties) { + this.serverProperties = serverProperties; + } + + @Override + public void customize(ConfigurableJettyWebServerFactory factory) { + Assert.state(VirtualThreads.areSupported(), "Virtual threads are not supported"); + QueuedThreadPool threadPool = JettyThreadPool.create(this.serverProperties.getJetty().getThreads()); + threadPool.setVirtualThreadsExecutor(VirtualThreads.getDefaultVirtualThreadsExecutor()); + factory.setThreadPool(threadPool); + } + + @Override + public int getOrder() { + return JettyWebServerFactoryCustomizer.ORDER + 1; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java index 2c9d015cae91..e53118c5d9af 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java @@ -18,8 +18,6 @@ import java.time.Duration; import java.util.Arrays; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.SynchronousQueue; import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.ConnectionFactory; @@ -31,9 +29,6 @@ import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.HandlerCollection; import org.eclipse.jetty.server.handler.HandlerWrapper; -import org.eclipse.jetty.util.BlockingArrayQueue; -import org.eclipse.jetty.util.thread.QueuedThreadPool; -import org.eclipse.jetty.util.thread.ThreadPool; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.cloud.CloudPlatform; @@ -60,6 +55,8 @@ public class JettyWebServerFactoryCustomizer implements WebServerFactoryCustomizer, Ordered { + static final int ORDER = 0; + private final Environment environment; private final ServerProperties serverProperties; @@ -71,7 +68,7 @@ public JettyWebServerFactoryCustomizer(Environment environment, ServerProperties @Override public int getOrder() { - return 0; + return ORDER; } @Override @@ -79,7 +76,7 @@ public void customize(ConfigurableJettyWebServerFactory factory) { ServerProperties.Jetty properties = this.serverProperties.getJetty(); factory.setUseForwardHeaders(getOrDeduceUseForwardHeaders()); ServerProperties.Jetty.Threads threadProperties = properties.getThreads(); - factory.setThreadPool(determineThreadPool(properties.getThreads())); + factory.setThreadPool(JettyThreadPool.create(properties.getThreads())); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(properties::getMaxConnections).to(factory::setMaxConnections); map.from(threadProperties::getAcceptors).to(factory::setAcceptors); @@ -151,27 +148,6 @@ else if (handler instanceof HandlerCollection collection) { }); } - private ThreadPool determineThreadPool(ServerProperties.Jetty.Threads properties) { - BlockingQueue queue = determineBlockingQueue(properties.getMaxQueueCapacity()); - int maxThreadCount = (properties.getMax() > 0) ? properties.getMax() : 200; - int minThreadCount = (properties.getMin() > 0) ? properties.getMin() : 8; - int threadIdleTimeout = (properties.getIdleTimeout() != null) ? (int) properties.getIdleTimeout().toMillis() - : 60000; - return new QueuedThreadPool(maxThreadCount, minThreadCount, threadIdleTimeout, queue); - } - - private BlockingQueue determineBlockingQueue(Integer maxQueueCapacity) { - if (maxQueueCapacity == null) { - return null; - } - if (maxQueueCapacity == 0) { - return new SynchronousQueue<>(); - } - else { - return new BlockingArrayQueue<>(maxQueueCapacity); - } - } - private void customizeAccessLog(ConfigurableJettyWebServerFactory factory, ServerProperties.Jetty.Accesslog properties) { factory.addServerCustomizers((server) -> { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyVirtualThreadsWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyVirtualThreadsWebServerFactoryCustomizerTests.java new file mode 100644 index 000000000000..6f7fb09dd7f3 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyVirtualThreadsWebServerFactoryCustomizerTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2023 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.web.embedded; + +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; + +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.web.embedded.jetty.ConfigurableJettyWebServerFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.assertArg; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link JettyVirtualThreadsWebServerFactoryCustomizer}. + * + * @author Moritz Halbritter + */ +class JettyVirtualThreadsWebServerFactoryCustomizerTests { + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void shouldConfigureVirtualThreads() { + ServerProperties properties = new ServerProperties(); + JettyVirtualThreadsWebServerFactoryCustomizer customizer = new JettyVirtualThreadsWebServerFactoryCustomizer( + properties); + ConfigurableJettyWebServerFactory factory = mock(ConfigurableJettyWebServerFactory.class); + customizer.customize(factory); + then(factory).should().setThreadPool(assertArg((threadPool) -> { + assertThat(threadPool).isInstanceOf(QueuedThreadPool.class); + QueuedThreadPool queuedThreadPool = (QueuedThreadPool) threadPool; + assertThat(queuedThreadPool.getVirtualThreadsExecutor()).isNotNull(); + })); + } + +} From efcc65bc5bb2870868cb43e540252cd87fa42a4a Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 15 Jun 2023 11:45:05 +0200 Subject: [PATCH 0014/1215] Apply filter order to ServerHttpObservationFilter Closes gh-35067 --- .../servlet/WebMvcObservationAutoConfiguration.java | 4 ++-- .../WebMvcObservationAutoConfigurationTests.java | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java index 5a70728ac125..802b8a3e969f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java @@ -44,7 +44,6 @@ import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.server.observation.DefaultServerRequestObservationConvention; import org.springframework.http.server.observation.ServerRequestObservationConvention; @@ -58,6 +57,7 @@ * @author Brian Clozel * @author Jon Schneider * @author Dmytro Nosan + * @author Moritz Halbritter * @since 3.0.0 */ @AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, @@ -90,7 +90,7 @@ public FilterRegistrationBean webMvcObservationFilt customTagsProvider.getIfAvailable(), contributorsProvider.orderedStream().toList()); ServerHttpObservationFilter filter = new ServerHttpObservationFilter(registry, convention); FilterRegistrationBean registration = new FilterRegistrationBean<>(filter); - registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1); + registration.setOrder(this.observationProperties.getHttp().getServer().getFilter().getOrder()); registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC); return registration; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java index a8fe9bf9c9f1..7328d40b2b53 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java @@ -64,6 +64,7 @@ * @author Tadaya Tsuyukubo * @author Madhura Bhave * @author Chanhyeong LEE + * @author Moritz Halbritter */ @ExtendWith(OutputCaptureExtension.class) @SuppressWarnings("removal") @@ -117,6 +118,15 @@ void filterRegistrationHasExpectedDispatcherTypesAndOrder() { }); } + @Test + void filterRegistrationOrderCanBeOverridden() { + this.contextRunner.withPropertyValues("management.observations.http.server.filter.order=1000") + .run((context) -> { + FilterRegistrationBean registration = context.getBean(FilterRegistrationBean.class); + assertThat(registration.getOrder()).isEqualTo(1000); + }); + } + @Test void filterRegistrationBacksOffWithAnotherServerHttpObservationFilterRegistration() { this.contextRunner.withUserConfiguration(TestServerHttpObservationFilterRegistrationConfiguration.class) From 6e86f5c4444fdce9c4446ae41f4c883ba6d29ea4 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 15 Jun 2023 15:01:13 +0200 Subject: [PATCH 0015/1215] Register uncategorized ObservationHandlers after categorized ones Closes gh-34399 --- .../ObservationHandlerGrouping.java | 8 +- .../ObservationAutoConfigurationTests.java | 29 ++-- .../ObservationHandlerGroupingTests.java | 126 ++++++++++++++++++ 3 files changed, 146 insertions(+), 17 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationHandlerGroupingTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationHandlerGrouping.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationHandlerGrouping.java index 163186947e8a..8312bc986c3f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationHandlerGrouping.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationHandlerGrouping.java @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.observation; +import java.util.ArrayList; import java.util.List; import io.micrometer.observation.ObservationHandler; @@ -30,6 +31,7 @@ * Groups {@link ObservationHandler ObservationHandlers} by type. * * @author Andy Wilkinson + * @author Moritz Halbritter */ @SuppressWarnings("rawtypes") class ObservationHandlerGrouping { @@ -46,13 +48,14 @@ class ObservationHandlerGrouping { void apply(List> handlers, ObservationConfig config) { MultiValueMap, ObservationHandler> groupings = new LinkedMultiValueMap<>(); + List> handlersWithoutCategory = new ArrayList<>(); for (ObservationHandler handler : handlers) { Class category = findCategory(handler); if (category != null) { groupings.add(category, handler); } else { - config.observationHandler(handler); + handlersWithoutCategory.add(handler); } } for (Class category : this.categories) { @@ -61,6 +64,9 @@ void apply(List> handlers, ObservationConfig config) { config.observationHandler(new FirstMatchingCompositeObservationHandler(handlerGroup)); } } + for (ObservationHandler observationHandler : handlersWithoutCategory) { + config.observationHandler(observationHandler); + } } private Class findCategory(ObservationHandler handler) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java index eb994e6896d1..8060d268908d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java @@ -223,14 +223,13 @@ void autoConfiguresObservationHandlers() { Observation.start("test-observation", observationRegistry).stop(); assertThat(context).doesNotHaveBean(DefaultMeterObservationHandler.class); assertThat(handlers).hasSize(2); - // Regular handlers are registered first - assertThat(handlers.get(0)).isInstanceOf(CustomObservationHandler.class); // Multiple MeterObservationHandler are wrapped in - // FirstMatchingCompositeObservationHandler, which calls only the first - // one - assertThat(handlers.get(1)).isInstanceOf(CustomMeterObservationHandler.class); - assertThat(((CustomMeterObservationHandler) handlers.get(1)).getName()) + // FirstMatchingCompositeObservationHandler, which calls only the first one + assertThat(handlers.get(0)).isInstanceOf(CustomMeterObservationHandler.class); + assertThat(((CustomMeterObservationHandler) handlers.get(0)).getName()) .isEqualTo("customMeterObservationHandler1"); + // Regular handlers are registered last + assertThat(handlers.get(1)).isInstanceOf(CustomObservationHandler.class); assertThat(context).doesNotHaveBean(DefaultMeterObservationHandler.class); assertThat(context).doesNotHaveBean(TracingAwareMeterObservationHandler.class); }); @@ -273,20 +272,18 @@ void autoConfiguresObservationHandlerWhenTracingIsActive() { List> handlers = context.getBean(CalledHandlers.class).getCalledHandlers(); Observation.start("test-observation", observationRegistry).stop(); assertThat(handlers).hasSize(3); - // Regular handlers are registered first - assertThat(handlers.get(0)).isInstanceOf(CustomObservationHandler.class); // Multiple TracingObservationHandler are wrapped in - // FirstMatchingCompositeObservationHandler, which calls only the first - // one - assertThat(handlers.get(1)).isInstanceOf(CustomTracingObservationHandler.class); - assertThat(((CustomTracingObservationHandler) handlers.get(1)).getName()) + // FirstMatchingCompositeObservationHandler, which calls only the first one + assertThat(handlers.get(0)).isInstanceOf(CustomTracingObservationHandler.class); + assertThat(((CustomTracingObservationHandler) handlers.get(0)).getName()) .isEqualTo("customTracingHandler1"); // Multiple MeterObservationHandler are wrapped in - // FirstMatchingCompositeObservationHandler, which calls only the first - // one - assertThat(handlers.get(2)).isInstanceOf(CustomMeterObservationHandler.class); - assertThat(((CustomMeterObservationHandler) handlers.get(2)).getName()) + // FirstMatchingCompositeObservationHandler, which calls only the first one + assertThat(handlers.get(1)).isInstanceOf(CustomMeterObservationHandler.class); + assertThat(((CustomMeterObservationHandler) handlers.get(1)).getName()) .isEqualTo("customMeterObservationHandler1"); + // Regular handlers are registered last + assertThat(handlers.get(2)).isInstanceOf(CustomObservationHandler.class); assertThat(context).doesNotHaveBean(TracingAwareMeterObservationHandler.class); assertThat(context).doesNotHaveBean(DefaultMeterObservationHandler.class); }); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationHandlerGroupingTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationHandlerGroupingTests.java new file mode 100644 index 000000000000..62ac14d092b1 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationHandlerGroupingTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.observation; + +import java.lang.reflect.Method; +import java.util.List; + +import io.micrometer.observation.Observation; +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationHandler.FirstMatchingCompositeObservationHandler; +import io.micrometer.observation.ObservationRegistry.ObservationConfig; +import org.junit.jupiter.api.Test; + +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ObservationHandlerGrouping}. + * + * @author Moritz Halbritter + */ +class ObservationHandlerGroupingTests { + + @Test + void shouldGroupCategoriesIntoFirstMatchingHandlerAndRespectsCategoryOrder() { + ObservationHandlerGrouping grouping = new ObservationHandlerGrouping( + List.of(ObservationHandlerA.class, ObservationHandlerB.class)); + ObservationConfig config = new ObservationConfig(); + ObservationHandlerA handlerA1 = new ObservationHandlerA("a1"); + ObservationHandlerA handlerA2 = new ObservationHandlerA("a2"); + ObservationHandlerB handlerB1 = new ObservationHandlerB("b1"); + ObservationHandlerB handlerB2 = new ObservationHandlerB("b2"); + grouping.apply(List.of(handlerB1, handlerB2, handlerA1, handlerA2), config); + List> handlers = getObservationHandlers(config); + assertThat(handlers).hasSize(2); + // Category A is first + assertThat(handlers.get(0)).isInstanceOf(FirstMatchingCompositeObservationHandler.class); + FirstMatchingCompositeObservationHandler firstMatching0 = (FirstMatchingCompositeObservationHandler) handlers + .get(0); + assertThat(firstMatching0.getHandlers()).containsExactly(handlerA1, handlerA2); + // Category B is second + assertThat(handlers.get(1)).isInstanceOf(FirstMatchingCompositeObservationHandler.class); + FirstMatchingCompositeObservationHandler firstMatching1 = (FirstMatchingCompositeObservationHandler) handlers + .get(1); + assertThat(firstMatching1.getHandlers()).containsExactly(handlerB1, handlerB2); + } + + @Test + void uncategorizedHandlersShouldBeOrderedAfterCategories() { + ObservationHandlerGrouping grouping = new ObservationHandlerGrouping(ObservationHandlerA.class); + ObservationConfig config = new ObservationConfig(); + ObservationHandlerA handlerA1 = new ObservationHandlerA("a1"); + ObservationHandlerA handlerA2 = new ObservationHandlerA("a2"); + ObservationHandlerB handlerB1 = new ObservationHandlerB("b1"); + grouping.apply(List.of(handlerB1, handlerA1, handlerA2), config); + List> handlers = getObservationHandlers(config); + assertThat(handlers).hasSize(2); + // Category A is first + assertThat(handlers.get(0)).isInstanceOf(FirstMatchingCompositeObservationHandler.class); + FirstMatchingCompositeObservationHandler firstMatching0 = (FirstMatchingCompositeObservationHandler) handlers + .get(0); + // Uncategorized handlers follow + assertThat(firstMatching0.getHandlers()).containsExactly(handlerA1, handlerA2); + assertThat(handlers.get(1)).isEqualTo(handlerB1); + } + + @SuppressWarnings("unchecked") + private static List> getObservationHandlers(ObservationConfig config) { + Method method = ReflectionUtils.findMethod(ObservationConfig.class, "getObservationHandlers"); + ReflectionUtils.makeAccessible(method); + return (List>) ReflectionUtils.invokeMethod(method, config); + } + + private static class NamedObservationHandler implements ObservationHandler { + + private final String name; + + NamedObservationHandler(String name) { + this.name = name; + } + + @Override + public boolean supportsContext(Context context) { + return true; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{name='" + this.name + "'}"; + } + + } + + private static class ObservationHandlerA extends NamedObservationHandler { + + ObservationHandlerA(String name) { + super(name); + } + + } + + private static class ObservationHandlerB extends NamedObservationHandler { + + ObservationHandlerB(String name) { + super(name); + } + + } + +} From d51559956f0682ac40c794d1744da2cfd73069c5 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 16 Jun 2023 09:54:27 +0200 Subject: [PATCH 0016/1215] Support overriding default OTel SpanProcessor Also makes it easier to set the MeterProvider used in the default SpanProcessor. Closes gh-35560 --- .../OpenTelemetryAutoConfiguration.java | 29 +++++--- .../autoconfigure/tracing/SpanProcessors.java | 70 +++++++++++++++++++ .../OpenTelemetryAutoConfigurationTests.java | 50 ++++++++++++- .../tracing/SpanProcessorsTests.java | 52 ++++++++++++++ 4 files changed, 190 insertions(+), 11 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanProcessors.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanProcessorsTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java index 0d86bb9c2cc5..b86f1eeda46a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java @@ -18,6 +18,7 @@ import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import io.micrometer.tracing.SpanCustomizer; import io.micrometer.tracing.exporter.SpanExportingPredicate; @@ -37,6 +38,7 @@ import io.micrometer.tracing.otel.propagation.BaggageTextMapPropagator; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.ContextStorage; import io.opentelemetry.context.propagation.ContextPropagators; @@ -47,6 +49,7 @@ import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; import io.opentelemetry.sdk.trace.SpanProcessor; import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessorBuilder; import io.opentelemetry.sdk.trace.export.SpanExporter; import io.opentelemetry.sdk.trace.samplers.Sampler; import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; @@ -99,13 +102,13 @@ OpenTelemetry openTelemetry(SdkTracerProvider sdkTracerProvider, ContextPropagat @Bean @ConditionalOnMissingBean - SdkTracerProvider otelSdkTracerProvider(Environment environment, ObjectProvider spanProcessors, - Sampler sampler, ObjectProvider customizers) { + SdkTracerProvider otelSdkTracerProvider(Environment environment, SpanProcessors spanProcessors, Sampler sampler, + ObjectProvider customizers) { String applicationName = environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME); SdkTracerProviderBuilder builder = SdkTracerProvider.builder() .setSampler(sampler) .setResource(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, applicationName))); - spanProcessors.orderedStream().forEach(builder::addSpanProcessor); + spanProcessors.forEach(builder::addSpanProcessor); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder.build(); } @@ -124,14 +127,20 @@ Sampler otelSampler() { } @Bean - SpanProcessor otelSpanProcessor(ObjectProvider spanExporters, + @ConditionalOnMissingBean + SpanProcessors spanProcessors(ObjectProvider spanProcessors) { + return () -> spanProcessors.orderedStream().collect(Collectors.toList()); + } + + @Bean + BatchSpanProcessor otelSpanProcessor(ObjectProvider spanExporters, ObjectProvider spanExportingPredicates, ObjectProvider spanReporters, - ObjectProvider spanFilters) { - return BatchSpanProcessor - .builder(new CompositeSpanExporter(spanExporters.orderedStream().toList(), - spanExportingPredicates.orderedStream().toList(), spanReporters.orderedStream().toList(), - spanFilters.orderedStream().toList())) - .build(); + ObjectProvider spanFilters, ObjectProvider meterProvider) { + BatchSpanProcessorBuilder builder = BatchSpanProcessor.builder(new CompositeSpanExporter( + spanExporters.orderedStream().toList(), spanExportingPredicates.orderedStream().toList(), + spanReporters.orderedStream().toList(), spanFilters.orderedStream().toList())); + meterProvider.ifAvailable(builder::setMeterProvider); + return builder.build(); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanProcessors.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanProcessors.java new file mode 100644 index 000000000000..3edc77b0de46 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanProcessors.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.tracing; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Spliterator; + +import io.opentelemetry.sdk.trace.SpanProcessor; + +/** + * A collection of {@link SpanProcessor span processors}. + * + * @author Moritz Halbritter + * @since 3.2.0 + */ +public interface SpanProcessors extends Iterable { + + /** + * Returns the list of {@link SpanProcessor span processors}. + * @return the list of span processors + */ + List getList(); + + @Override + default Iterator iterator() { + return getList().iterator(); + } + + @Override + default Spliterator spliterator() { + return getList().spliterator(); + } + + /** + * Constructs a {@link SpanProcessors} instance with the given list of + * {@link SpanProcessor span processors}. + * @param spanProcessors the list of span processors + * @return the constructed {@link SpanProcessors} instance + */ + static SpanProcessors of(List spanProcessors) { + return () -> spanProcessors; + } + + /** + * Constructs a {@link SpanProcessors} instance with the given {@link SpanProcessor + * span processors}. + * @param spanProcessors the span processors + * @return the constructed {@link SpanProcessors} instance + */ + static SpanProcessors of(SpanProcessor... spanProcessors) { + return of(Arrays.asList(spanProcessors)); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java index 02ff5e562e9f..2bb1b24924b6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java @@ -29,6 +29,7 @@ import io.micrometer.tracing.otel.bridge.Slf4JEventListener; import io.micrometer.tracing.otel.propagation.BaggageTextMapPropagator; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; import io.opentelemetry.context.propagation.ContextPropagators; @@ -41,6 +42,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.FilteredClassLoader; @@ -51,6 +53,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; /** @@ -82,6 +87,7 @@ void shouldSupplyBeans() { assertThat(context).hasSingleBean(OtelPropagator.class); assertThat(context).hasSingleBean(TextMapPropagator.class); assertThat(context).hasSingleBean(OtelSpanCustomizer.class); + assertThat(context).hasSingleBean(SpanProcessors.class); }); } @@ -112,6 +118,7 @@ void shouldNotSupplyBeansIfDependencyIsMissing(String packageName) { assertThat(context).doesNotHaveBean(OtelPropagator.class); assertThat(context).doesNotHaveBean(TextMapPropagator.class); assertThat(context).doesNotHaveBean(OtelSpanCustomizer.class); + assertThat(context).doesNotHaveBean(SpanProcessors.class); }); } @@ -142,14 +149,18 @@ void shouldBackOffOnCustomBeans() { assertThat(context).hasSingleBean(OtelPropagator.class); assertThat(context).hasBean("customSpanCustomizer"); assertThat(context).hasSingleBean(SpanCustomizer.class); + assertThat(context).hasBean("customSpanProcessors"); + assertThat(context).hasSingleBean(SpanProcessors.class); }); } @Test void shouldAllowMultipleSpanProcessors() { - this.contextRunner.withUserConfiguration(CustomConfiguration.class).run((context) -> { + this.contextRunner.withUserConfiguration(AdditionalSpanProcessorConfiguration.class).run((context) -> { assertThat(context.getBeansOfType(SpanProcessor.class)).hasSize(2); assertThat(context).hasBean("customSpanProcessor"); + SpanProcessors spanProcessors = context.getBean(SpanProcessors.class); + assertThat(spanProcessors).hasSize(2); }); } @@ -235,9 +246,46 @@ void shouldCustomizeSdkTracerProvider() { }); } + @Test + void defaultSpanProcessorShouldUseMeterProviderIfAvailable() { + this.contextRunner.withUserConfiguration(MeterProviderConfiguration.class).run((context) -> { + MeterProvider meterProvider = context.getBean(MeterProvider.class); + assertThat(Mockito.mockingDetails(meterProvider).isMock()).isTrue(); + then(meterProvider).should().meterBuilder(anyString()); + }); + } + + @Configuration(proxyBeanMethods = false) + private static class MeterProviderConfiguration { + + @Bean + MeterProvider meterProvider() { + MeterProvider mock = mock(MeterProvider.class); + given(mock.meterBuilder(anyString())) + .willAnswer((invocation) -> MeterProvider.noop().meterBuilder(invocation.getArgument(0, String.class))); + return mock; + } + + } + + @Configuration(proxyBeanMethods = false) + private static class AdditionalSpanProcessorConfiguration { + + @Bean + SpanProcessor customSpanProcessor() { + return mock(SpanProcessor.class); + } + + } + @Configuration(proxyBeanMethods = false) private static class CustomConfiguration { + @Bean + SpanProcessors customSpanProcessors() { + return SpanProcessors.of(mock(SpanProcessor.class)); + } + @Bean io.micrometer.tracing.Tracer customMicrometerTracer() { return mock(io.micrometer.tracing.Tracer.class); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanProcessorsTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanProcessorsTests.java new file mode 100644 index 000000000000..f65a3b07dddd --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanProcessorsTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.tracing; + +import java.util.List; + +import io.opentelemetry.sdk.trace.SpanProcessor; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link SpanProcessors}. + * + * @author Moritz Halbritter + */ +class SpanProcessorsTests { + + @Test + void ofList() { + SpanProcessor spanProcessor1 = mock(SpanProcessor.class); + SpanProcessor spanProcessor2 = mock(SpanProcessor.class); + SpanProcessors spanProcessors = SpanProcessors.of(List.of(spanProcessor1, spanProcessor2)); + assertThat(spanProcessors).containsExactly(spanProcessor1, spanProcessor2); + assertThat(spanProcessors.getList()).containsExactly(spanProcessor1, spanProcessor2); + } + + @Test + void ofArray() { + SpanProcessor spanProcessor1 = mock(SpanProcessor.class); + SpanProcessor spanProcessor2 = mock(SpanProcessor.class); + SpanProcessors spanProcessors = SpanProcessors.of(spanProcessor1, spanProcessor2); + assertThat(spanProcessors).containsExactly(spanProcessor1, spanProcessor2); + assertThat(spanProcessors.getList()).containsExactly(spanProcessor1, spanProcessor2); + } + +} From 929283f4dc2e21e5ddbd046d939aa2a0d7a9077e Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 16 Jun 2023 10:24:45 +0200 Subject: [PATCH 0017/1215] Support overriding OTel SpanExporters See gh-35596 --- .../OpenTelemetryAutoConfiguration.java | 14 +++- .../autoconfigure/tracing/SpanExporters.java | 70 +++++++++++++++++ .../OpenTelemetryAutoConfigurationTests.java | 76 ++++++++++++++++--- .../tracing/SpanExportersTests.java | 52 +++++++++++++ 4 files changed, 199 insertions(+), 13 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExporters.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExportersTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java index b86f1eeda46a..4220d31c0d26 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java @@ -133,16 +133,22 @@ SpanProcessors spanProcessors(ObjectProvider spanProcessors) { } @Bean - BatchSpanProcessor otelSpanProcessor(ObjectProvider spanExporters, + BatchSpanProcessor otelSpanProcessor(SpanExporters spanExporters, ObjectProvider spanExportingPredicates, ObjectProvider spanReporters, ObjectProvider spanFilters, ObjectProvider meterProvider) { - BatchSpanProcessorBuilder builder = BatchSpanProcessor.builder(new CompositeSpanExporter( - spanExporters.orderedStream().toList(), spanExportingPredicates.orderedStream().toList(), - spanReporters.orderedStream().toList(), spanFilters.orderedStream().toList())); + BatchSpanProcessorBuilder builder = BatchSpanProcessor.builder( + new CompositeSpanExporter(spanExporters.getList(), spanExportingPredicates.orderedStream().toList(), + spanReporters.orderedStream().toList(), spanFilters.orderedStream().toList())); meterProvider.ifAvailable(builder::setMeterProvider); return builder.build(); } + @Bean + @ConditionalOnMissingBean + SpanExporters spanExporters(ObjectProvider spanExporters) { + return SpanExporters.of(spanExporters.orderedStream().collect(Collectors.toList())); + } + @Bean @ConditionalOnMissingBean Tracer otelTracer(OpenTelemetry openTelemetry) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExporters.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExporters.java new file mode 100644 index 000000000000..b6f133e57b7a --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExporters.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.tracing; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Spliterator; + +import io.opentelemetry.sdk.trace.export.SpanExporter; + +/** + * A collection of {@link SpanExporter span exporters}. + * + * @author Moritz Halbritter + * @since 3.2.0 + */ +public interface SpanExporters extends Iterable { + + /** + * Returns the list of {@link SpanExporter span exporters}. + * @return the list of span exporters + */ + List getList(); + + @Override + default Iterator iterator() { + return getList().iterator(); + } + + @Override + default Spliterator spliterator() { + return getList().spliterator(); + } + + /** + * Constructs a {@link SpanExporters} instance with the given list of + * {@link SpanExporter span exporters}. + * @param spanExporters the list of span exporters + * @return the constructed {@link SpanExporters} instance + */ + static SpanExporters of(List spanExporters) { + return () -> spanExporters; + } + + /** + * Constructs a {@link SpanExporters} instance with the given {@link SpanExporter span + * exporters}. + * @param spanExporters the span exporters + * @return the constructed {@link SpanExporters} instance + */ + static SpanExporters of(SpanExporter... spanExporters) { + return of(Arrays.asList(spanExporters)); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java index 2bb1b24924b6..e75701765366 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.autoconfigure.tracing; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import io.micrometer.tracing.SpanCustomizer; @@ -35,9 +36,12 @@ import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.extension.trace.propagation.B3Propagator; +import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.SpanLimits; import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; import io.opentelemetry.sdk.trace.samplers.Sampler; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -88,6 +92,7 @@ void shouldSupplyBeans() { assertThat(context).hasSingleBean(TextMapPropagator.class); assertThat(context).hasSingleBean(OtelSpanCustomizer.class); assertThat(context).hasSingleBean(SpanProcessors.class); + assertThat(context).hasSingleBean(SpanExporters.class); }); } @@ -119,6 +124,7 @@ void shouldNotSupplyBeansIfDependencyIsMissing(String packageName) { assertThat(context).doesNotHaveBean(TextMapPropagator.class); assertThat(context).doesNotHaveBean(OtelSpanCustomizer.class); assertThat(context).doesNotHaveBean(SpanProcessors.class); + assertThat(context).doesNotHaveBean(SpanExporters.class); }); } @@ -151,6 +157,8 @@ void shouldBackOffOnCustomBeans() { assertThat(context).hasSingleBean(SpanCustomizer.class); assertThat(context).hasBean("customSpanProcessors"); assertThat(context).hasSingleBean(SpanProcessors.class); + assertThat(context).hasBean("customSpanExporters"); + assertThat(context).hasSingleBean(SpanExporters.class); }); } @@ -164,6 +172,17 @@ void shouldAllowMultipleSpanProcessors() { }); } + @Test + void shouldAllowMultipleSpanExporters() { + this.contextRunner.withUserConfiguration(MultipleSpanExporterConfiguration.class).run((context) -> { + assertThat(context.getBeansOfType(SpanExporter.class)).hasSize(2); + assertThat(context).hasBean("spanExporter1"); + assertThat(context).hasBean("spanExporter2"); + SpanExporters spanExporters = context.getBean(SpanExporters.class); + assertThat(spanExporters).hasSize(2); + }); + } + @Test void shouldAllowMultipleTextMapPropagators() { this.contextRunner.withUserConfiguration(CustomConfiguration.class).run((context) -> { @@ -228,15 +247,6 @@ void shouldSupplyW3CPropagationWithoutBaggageWhenDisabled() { }); } - private List getInjectors(TextMapPropagator propagator) { - assertThat(propagator).as("propagator").isNotNull(); - if (propagator instanceof CompositeTextMapPropagator compositePropagator) { - return compositePropagator.getInjectors().stream().toList(); - } - fail("Expected CompositeTextMapPropagator, found %s".formatted(propagator.getClass())); - throw new AssertionError("Unreachable"); - } - @Test void shouldCustomizeSdkTracerProvider() { this.contextRunner.withUserConfiguration(SdkTracerProviderCustomizationConfiguration.class).run((context) -> { @@ -255,6 +265,15 @@ void defaultSpanProcessorShouldUseMeterProviderIfAvailable() { }); } + private List getInjectors(TextMapPropagator propagator) { + assertThat(propagator).as("propagator").isNotNull(); + if (propagator instanceof CompositeTextMapPropagator compositePropagator) { + return compositePropagator.getInjectors().stream().toList(); + } + fail("Expected CompositeTextMapPropagator, found %s".formatted(propagator.getClass())); + throw new AssertionError("Unreachable"); + } + @Configuration(proxyBeanMethods = false) private static class MeterProviderConfiguration { @@ -278,6 +297,21 @@ SpanProcessor customSpanProcessor() { } + @Configuration(proxyBeanMethods = false) + private static class MultipleSpanExporterConfiguration { + + @Bean + SpanExporter spanExporter1() { + return new DummySpanExporter(); + } + + @Bean + SpanExporter spanExporter2() { + return new DummySpanExporter(); + } + + } + @Configuration(proxyBeanMethods = false) private static class CustomConfiguration { @@ -286,6 +320,11 @@ SpanProcessors customSpanProcessors() { return SpanProcessors.of(mock(SpanProcessor.class)); } + @Bean + SpanExporters customSpanExporters() { + return SpanExporters.of(new DummySpanExporter()); + } + @Bean io.micrometer.tracing.Tracer customMicrometerTracer() { return mock(io.micrometer.tracing.Tracer.class); @@ -381,4 +420,23 @@ SdkTracerProviderBuilderCustomizer sdkTracerProviderBuilderCustomizerTwo() { } + private static class DummySpanExporter implements SpanExporter { + + @Override + public CompletableResultCode export(Collection spans) { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExportersTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExportersTests.java new file mode 100644 index 000000000000..dc883b971863 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExportersTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.tracing; + +import java.util.List; + +import io.opentelemetry.sdk.trace.export.SpanExporter; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link SpanExporters}. + * + * @author Moritz Halbritter + */ +class SpanExportersTests { + + @Test + void ofList() { + SpanExporter spanExporter1 = mock(SpanExporter.class); + SpanExporter spanExporter2 = mock(SpanExporter.class); + SpanExporters spanExporters = SpanExporters.of(List.of(spanExporter1, spanExporter2)); + assertThat(spanExporters).containsExactly(spanExporter1, spanExporter2); + assertThat(spanExporters.getList()).containsExactly(spanExporter1, spanExporter2); + } + + @Test + void ofArray() { + SpanExporter spanExporter1 = mock(SpanExporter.class); + SpanExporter spanExporter2 = mock(SpanExporter.class); + SpanExporters spanExporters = SpanExporters.of(spanExporter1, spanExporter2); + assertThat(spanExporters).containsExactly(spanExporter1, spanExporter2); + assertThat(spanExporters.getList()).containsExactly(spanExporter1, spanExporter2); + } + +} From c25b084391b7a5aae5b4104619aa46af092b7599 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 16 Jun 2023 10:36:33 +0200 Subject: [PATCH 0018/1215] Polish --- .../autoconfigure/tracing/OpenTelemetryAutoConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java index 4220d31c0d26..6a30b9cbc636 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java @@ -129,7 +129,7 @@ Sampler otelSampler() { @Bean @ConditionalOnMissingBean SpanProcessors spanProcessors(ObjectProvider spanProcessors) { - return () -> spanProcessors.orderedStream().collect(Collectors.toList()); + return SpanProcessors.of(spanProcessors.orderedStream().collect(Collectors.toList())); } @Bean From 27add2bbe3095805f4bb159f33e9aefd3292df2e Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 16 Jun 2023 14:09:00 +0200 Subject: [PATCH 0019/1215] Rework @AutoConfigureObservability and tracing auto-configurations @ConditionalOnEnabledTracing is now applied to the minimal amount of beans. The beans which are annotated with it are beans that will lead to span sending to backends. This leaves the majority of the Micrometer Tracing, Brave and OpenTelemetry infrastructure untouched in tests. Closes gh-35354 --- .../tracing/BraveAutoConfiguration.java | 1 - .../MicrometerTracingAutoConfiguration.java | 1 - .../OpenTelemetryAutoConfiguration.java | 1 - .../tracing/otlp/OtlpAutoConfiguration.java | 2 +- .../PrometheusExemplarsAutoConfiguration.java | 2 - .../WavefrontTracingAutoConfiguration.java | 4 +- .../zipkin/ZipkinAutoConfiguration.java | 2 - .../tracing/zipkin/ZipkinConfigurations.java | 3 + .../WavefrontSenderConfiguration.java | 23 +++++ ...intsAutoConfigurationIntegrationTests.java | 5 +- .../tracing/BraveAutoConfigurationTests.java | 6 -- ...crometerTracingAutoConfigurationTests.java | 11 --- .../otlp/OtlpAutoConfigurationTests.java | 10 +++ ...etheusExemplarsAutoConfigurationTests.java | 8 +- ...avefrontTracingAutoConfigurationTests.java | 16 ++-- .../zipkin/ZipkinAutoConfigurationTests.java | 6 -- ...ConfigurationsBraveConfigurationTests.java | 9 ++ ...ationsOpenTelemetryConfigurationTests.java | 9 ++ .../WavefrontSenderConfigurationTests.java | 29 ++++++ ...ObservabilityContextCustomizerFactory.java | 90 ------------------- ...vabilityContextCustomizerFactoryTests.java | 70 --------------- 21 files changed, 100 insertions(+), 208 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfiguration.java index 4001f0e7fad5..393ff7dbfa1a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfiguration.java @@ -76,7 +76,6 @@ @AutoConfiguration(before = MicrometerTracingAutoConfiguration.class) @ConditionalOnClass({ Tracer.class, BraveTracer.class }) @EnableConfigurationProperties(TracingProperties.class) -@ConditionalOnEnabledTracing public class BraveAutoConfiguration { private static final BraveBaggageManager BRAVE_BAGGAGE_MANAGER = new BraveBaggageManager(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java index e91e41a5b057..149141c887f9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java @@ -39,7 +39,6 @@ */ @AutoConfiguration @ConditionalOnClass(Tracer.class) -@ConditionalOnEnabledTracing public class MicrometerTracingAutoConfiguration { /** diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java index 6a30b9cbc636..f2ccd0a5a629 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java @@ -75,7 +75,6 @@ * @since 3.0.0 */ @AutoConfiguration(before = MicrometerTracingAutoConfiguration.class) -@ConditionalOnEnabledTracing @ConditionalOnClass({ OtelTracer.class, SdkTracerProvider.class, OpenTelemetry.class }) @EnableConfigurationProperties(TracingProperties.class) public class OpenTelemetryAutoConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfiguration.java index ca2012c20d64..e86b48ae555e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfiguration.java @@ -50,7 +50,6 @@ * @since 3.1.0 */ @AutoConfiguration -@ConditionalOnEnabledTracing @ConditionalOnClass({ OtelTracer.class, SdkTracerProvider.class, OpenTelemetry.class, OtlpHttpSpanExporter.class }) @EnableConfigurationProperties(OtlpProperties.class) public class OtlpAutoConfiguration { @@ -59,6 +58,7 @@ public class OtlpAutoConfiguration { @ConditionalOnMissingBean(value = OtlpHttpSpanExporter.class, type = "io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter") @ConditionalOnProperty(prefix = "management.otlp.tracing", name = "endpoint") + @ConditionalOnEnabledTracing OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpProperties properties) { OtlpHttpSpanExporterBuilder builder = OtlpHttpSpanExporter.builder() .setEndpoint(properties.getEndpoint()) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfiguration.java index 9032a0712ad5..e669127982c7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfiguration.java @@ -22,7 +22,6 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.metrics.export.prometheus.PrometheusMetricsExportAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.tracing.ConditionalOnEnabledTracing; import org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -43,7 +42,6 @@ after = MicrometerTracingAutoConfiguration.class) @ConditionalOnBean(Tracer.class) @ConditionalOnClass({ Tracer.class, SpanContextSupplier.class }) -@ConditionalOnEnabledTracing public class PrometheusExemplarsAutoConfiguration { @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/wavefront/WavefrontTracingAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/wavefront/WavefrontTracingAutoConfiguration.java index 81a0dd0863c3..f3fab744bcc4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/wavefront/WavefrontTracingAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/wavefront/WavefrontTracingAutoConfiguration.java @@ -52,7 +52,6 @@ @AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, WavefrontAutoConfiguration.class }) @ConditionalOnClass({ WavefrontSender.class, WavefrontSpanHandler.class }) -@ConditionalOnEnabledTracing @EnableConfigurationProperties(WavefrontProperties.class) @Import(WavefrontSenderConfiguration.class) public class WavefrontTracingAutoConfiguration { @@ -60,6 +59,7 @@ public class WavefrontTracingAutoConfiguration { @Bean @ConditionalOnMissingBean @ConditionalOnBean(WavefrontSender.class) + @ConditionalOnEnabledTracing WavefrontSpanHandler wavefrontSpanHandler(WavefrontProperties properties, WavefrontSender wavefrontSender, SpanMetrics spanMetrics, ApplicationTags applicationTags) { return new WavefrontSpanHandler(properties.getSender().getMaxQueueSize(), wavefrontSender, spanMetrics, @@ -96,6 +96,7 @@ static class WavefrontBrave { @Bean @ConditionalOnMissingBean + @ConditionalOnEnabledTracing WavefrontBraveSpanHandler wavefrontBraveSpanHandler(WavefrontSpanHandler wavefrontSpanHandler) { return new WavefrontBraveSpanHandler(wavefrontSpanHandler); } @@ -108,6 +109,7 @@ static class WavefrontOpenTelemetry { @Bean @ConditionalOnMissingBean + @ConditionalOnEnabledTracing WavefrontOtelSpanExporter wavefrontOtelSpanExporter(WavefrontSpanHandler wavefrontSpanHandler) { return new WavefrontOtelSpanExporter(wavefrontSpanHandler); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfiguration.java index 971b9d514ec1..daff635f8631 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfiguration.java @@ -21,7 +21,6 @@ import zipkin2.codec.SpanBytesEncoder; import zipkin2.reporter.Sender; -import org.springframework.boot.actuate.autoconfigure.tracing.ConditionalOnEnabledTracing; import org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinConfigurations.BraveConfiguration; import org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinConfigurations.OpenTelemetryConfiguration; import org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinConfigurations.ReporterConfiguration; @@ -48,7 +47,6 @@ @ConditionalOnClass(Sender.class) @Import({ SenderConfiguration.class, ReporterConfiguration.class, BraveConfiguration.class, OpenTelemetryConfiguration.class }) -@ConditionalOnEnabledTracing @EnableConfigurationProperties(ZipkinProperties.class) public class ZipkinAutoConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurations.java index 722b502befa5..b4a1802b7bc6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurations.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurations.java @@ -26,6 +26,7 @@ import zipkin2.reporter.urlconnection.URLConnectionSender; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.autoconfigure.tracing.ConditionalOnEnabledTracing; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -142,6 +143,7 @@ static class BraveConfiguration { @Bean @ConditionalOnMissingBean @ConditionalOnBean(Reporter.class) + @ConditionalOnEnabledTracing ZipkinSpanHandler zipkinSpanHandler(Reporter spanReporter) { return (ZipkinSpanHandler) ZipkinSpanHandler.newBuilder(spanReporter).build(); } @@ -155,6 +157,7 @@ static class OpenTelemetryConfiguration { @Bean @ConditionalOnMissingBean @ConditionalOnBean(Sender.class) + @ConditionalOnEnabledTracing ZipkinSpanExporter zipkinSpanExporter(BytesEncoder encoder, Sender sender) { return ZipkinSpanExporter.builder().setEncoder(encoder).setSender(sender).build(); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontSenderConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontSenderConfiguration.java index 6cb11ae31df3..1416abc72ec4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontSenderConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontSenderConfiguration.java @@ -21,13 +21,17 @@ import com.wavefront.sdk.common.WavefrontSender; import com.wavefront.sdk.common.clients.WavefrontClient.Builder; +import org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport; import org.springframework.boot.actuate.autoconfigure.metrics.export.wavefront.WavefrontMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.tracing.ConditionalOnEnabledTracing; import org.springframework.boot.actuate.autoconfigure.tracing.wavefront.WavefrontTracingAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.util.unit.DataSize; @@ -46,6 +50,7 @@ public class WavefrontSenderConfiguration { @Bean @ConditionalOnMissingBean + @Conditional(WavefrontTracingOrMetricsCondition.class) public WavefrontSender wavefrontSender(WavefrontProperties properties) { Builder builder = new Builder(properties.getEffectiveUri().toString(), properties.getApiTokenOrThrow()); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); @@ -57,4 +62,22 @@ public WavefrontSender wavefrontSender(WavefrontProperties properties) { return builder.build(); } + static final class WavefrontTracingOrMetricsCondition extends AnyNestedCondition { + + WavefrontTracingOrMetricsCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnEnabledTracing + static class TracingCondition { + + } + + @ConditionalOnEnabledMetricsExport("wavefront") + static class MetricsCondition { + + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebEndpointsAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebEndpointsAutoConfigurationIntegrationTests.java index f2d6c56aca3b..33aa8931dd26 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebEndpointsAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebEndpointsAutoConfigurationIntegrationTests.java @@ -19,6 +19,8 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration; import org.springframework.boot.actuate.health.HealthEndpointWebExtension; import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -80,7 +82,8 @@ private ReactiveWebApplicationContextRunner reactiveWebRunner() { MongoReactiveAutoConfiguration.class, MongoReactiveDataAutoConfiguration.class, RepositoryRestMvcAutoConfiguration.class, HazelcastAutoConfiguration.class, ElasticsearchDataAutoConfiguration.class, RedisAutoConfiguration.class, - RedisRepositoriesAutoConfiguration.class }) + RedisRepositoriesAutoConfiguration.class, BraveAutoConfiguration.class, + OpenTelemetryAutoConfiguration.class }) @SpringBootConfiguration static class WebEndpointTestApplication { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java index e51998741cef..073fd9b96871 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java @@ -151,12 +151,6 @@ void shouldSupplyB3PropagationFactoryViaProperty() { }); } - @Test - void shouldNotSupplyBeansIfTracingIsDisabled() { - this.contextRunner.withPropertyValues("management.tracing.enabled=false") - .run((context) -> assertThat(context).doesNotHaveBean(BraveAutoConfiguration.class)); - } - @Test void shouldNotSupplyCorrelationScopeDecoratorIfBaggageDisabled() { this.contextRunner.withPropertyValues("management.tracing.baggage.enabled=false") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java index 6f3c4a373d06..d9f94f46396a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java @@ -111,17 +111,6 @@ void shouldNotSupplyBeansIfPropagatorIsMissing() { }); } - @Test - void shouldNotSupplyBeansIfTracingIsDisabled() { - this.contextRunner.withUserConfiguration(TracerConfiguration.class, PropagatorConfiguration.class) - .withPropertyValues("management.tracing.enabled=false") - .run((context) -> { - assertThat(context).doesNotHaveBean(DefaultTracingObservationHandler.class); - assertThat(context).doesNotHaveBean(PropagatingReceiverTracingObservationHandler.class); - assertThat(context).doesNotHaveBean(PropagatingSenderTracingObservationHandler.class); - }); - } - @Configuration(proxyBeanMethods = false) private static class TracerConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationTests.java index f3b3e5b3a5a9..0f5fbca25c80 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationTests.java @@ -40,6 +40,9 @@ class OtlpAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(OtlpAutoConfiguration.class)); + private final ApplicationContextRunner tracingDisabledContextRunner = this.contextRunner + .withPropertyValues("management.tracing.enabled=false"); + @Test void shouldNotSupplyBeansIfPropertyIsNotSet() { this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(OtlpHttpSpanExporter.class)); @@ -96,6 +99,13 @@ void shouldBackOffWhenCustomGrpcExporterIsDefined() { .hasSingleBean(SpanExporter.class)); } + @Test + void shouldNotSupplyOtlpHttpSpanExporterIfTracingIsDisabled() { + this.tracingDisabledContextRunner + .withPropertyValues("management.otlp.tracing.endpoint=http://localhost:4318/v1/traces") + .run((context) -> assertThat(context).doesNotHaveBean(OtlpHttpSpanExporter.class)); + } + @Configuration(proxyBeanMethods = false) private static class CustomHttpExporterConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java index 2d2f9d35a530..3bbec4be49f3 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java @@ -40,7 +40,7 @@ /** * Tests for {@link PrometheusExemplarsAutoConfiguration}. * - * * @author Jonatan Ivanov + * @author Jonatan Ivanov */ class PrometheusExemplarsAutoConfigurationTests { @@ -52,12 +52,6 @@ class PrometheusExemplarsAutoConfigurationTests { AutoConfigurations.of(PrometheusExemplarsAutoConfiguration.class, ObservationAutoConfiguration.class, BraveAutoConfiguration.class, MicrometerTracingAutoConfiguration.class)); - @Test - void shouldNotSupplyBeansIfTracingIsDisabled() { - this.contextRunner.withPropertyValues("management.tracing.enabled=false") - .run((context) -> assertThat(context).doesNotHaveBean(SpanContextSupplier.class)); - } - @Test void shouldNotSupplyBeansIfPrometheusSupportIsMissing() { this.contextRunner.withClassLoader(new FilteredClassLoader("io.prometheus.client.exemplars")) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/wavefront/WavefrontTracingAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/wavefront/WavefrontTracingAutoConfigurationTests.java index a75d692772e6..8295ed6930f6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/wavefront/WavefrontTracingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/wavefront/WavefrontTracingAutoConfigurationTests.java @@ -47,6 +47,9 @@ class WavefrontTracingAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( AutoConfigurations.of(WavefrontAutoConfiguration.class, WavefrontTracingAutoConfiguration.class)); + private final ApplicationContextRunner tracingDisabledContextRunner = this.contextRunner + .withPropertyValues("management.tracing.enabled=false"); + @Test void shouldSupplyBeans() { this.contextRunner.withUserConfiguration(WavefrontSenderConfiguration.class).run((context) -> { @@ -83,14 +86,11 @@ void shouldNotSupplyBeansIfMicrometerReporterWavefrontIsMissing() { @Test void shouldNotSupplyBeansIfTracingIsDisabled() { - this.contextRunner.withPropertyValues("management.tracing.enabled=false") - .withUserConfiguration(WavefrontSenderConfiguration.class) - .run((context) -> { - assertThat(context).doesNotHaveBean(WavefrontSpanHandler.class); - assertThat(context).doesNotHaveBean(SpanMetrics.class); - assertThat(context).doesNotHaveBean(WavefrontBraveSpanHandler.class); - assertThat(context).doesNotHaveBean(WavefrontOtelSpanExporter.class); - }); + this.tracingDisabledContextRunner.withUserConfiguration(WavefrontSenderConfiguration.class).run((context) -> { + assertThat(context).doesNotHaveBean(WavefrontSpanHandler.class); + assertThat(context).doesNotHaveBean(WavefrontBraveSpanHandler.class); + assertThat(context).doesNotHaveBean(WavefrontOtelSpanExporter.class); + }); } @Test diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfigurationTests.java index c647bfbfe2ac..1da2e7668a87 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfigurationTests.java @@ -59,12 +59,6 @@ void shouldBackOffOnCustomBeans() { }); } - @Test - void shouldNotSupplyBeansIfTracingIsDisabled() { - this.contextRunner.withPropertyValues("management.tracing.enabled=false") - .run((context) -> assertThat(context).doesNotHaveBean(BytesEncoder.class)); - } - @Test void definesPropertiesBasedConnectionDetailsByDefault() { this.contextRunner.run((context) -> assertThat(context).hasSingleBean(PropertiesZipkinConnectionDetails.class)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsBraveConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsBraveConfigurationTests.java index 735cd00d0c6b..9b488aebe404 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsBraveConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsBraveConfigurationTests.java @@ -42,6 +42,9 @@ class ZipkinConfigurationsBraveConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(BraveConfiguration.class)); + private final ApplicationContextRunner tracingDisabledContextRunner = this.contextRunner + .withPropertyValues("management.tracing.enabled=false"); + @Test void shouldSupplyBeans() { this.contextRunner.withUserConfiguration(ReporterConfiguration.class) @@ -79,6 +82,12 @@ void shouldSupplyZipkinSpanHandlerWithCustomSpanHandler() { }); } + @Test + void shouldNotSupplyZipkinSpanHandlerIfTracingIsDisabled() { + this.tracingDisabledContextRunner.withUserConfiguration(ReporterConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(ZipkinSpanHandler.class)); + } + @Configuration(proxyBeanMethods = false) private static class ReporterConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsOpenTelemetryConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsOpenTelemetryConfigurationTests.java index c3ef5f99c371..5c2a9059c558 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsOpenTelemetryConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurationsOpenTelemetryConfigurationTests.java @@ -43,6 +43,9 @@ class ZipkinConfigurationsOpenTelemetryConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(BaseConfiguration.class, OpenTelemetryConfiguration.class)); + private final ApplicationContextRunner tracingDisabledContextRunner = this.contextRunner + .withPropertyValues("management.tracing.enabled=false"); + @Test void shouldSupplyBeans() { this.contextRunner.withUserConfiguration(SenderConfiguration.class) @@ -70,6 +73,12 @@ void shouldBackOffOnCustomBeans() { }); } + @Test + void shouldNotSupplyZipkinSpanExporterIfTracingIsDisabled() { + this.tracingDisabledContextRunner.withUserConfiguration(SenderConfiguration.class) + .run((context) -> assertThat(context).doesNotHaveBean(ZipkinSpanExporter.class)); + } + @Configuration(proxyBeanMethods = false) private static class SenderConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontSenderConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontSenderConfigurationTests.java index a75203e4fedf..367adebb5c59 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontSenderConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontSenderConfigurationTests.java @@ -42,6 +42,17 @@ class WavefrontSenderConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(WavefrontSenderConfiguration.class)); + private final ApplicationContextRunner tracingDisabledContextRunner = this.contextRunner + .withPropertyValues("management.tracing.enabled=false"); + + private final ApplicationContextRunner metricsDisabledContextRunner = this.contextRunner.withPropertyValues( + "management.defaults.metrics.export.enabled=false", "management.simple.metrics.export.enabled=true"); + + // Both metrics and tracing are disabled + private final ApplicationContextRunner observabilityDisabledContextRunner = this.contextRunner.withPropertyValues( + "management.tracing.enabled=false", "management.defaults.metrics.export.enabled=false", + "management.simple.metrics.export.enabled=true"); + @Test void shouldNotFailIfWavefrontIsMissing() { this.contextRunner.withClassLoader(new FilteredClassLoader("com.wavefront")) @@ -83,6 +94,24 @@ void configureWavefrontSender() { }); } + @Test + void shouldNotSupplyWavefrontSenderIfObservabilityIsDisabled() { + this.observabilityDisabledContextRunner.withPropertyValues("management.wavefront.api-token=abcde") + .run((context) -> assertThat(context).doesNotHaveBean(WavefrontSender.class)); + } + + @Test + void shouldSupplyWavefrontSenderIfOnlyTracingIsDisabled() { + this.tracingDisabledContextRunner.withPropertyValues("management.wavefront.api-token=abcde") + .run((context) -> assertThat(context).hasSingleBean(WavefrontSender.class)); + } + + @Test + void shouldSupplyWavefrontSenderIfOnlyMetricsAreDisabled() { + this.metricsDisabledContextRunner.withPropertyValues("management.wavefront.api-token=abcde") + .run((context) -> assertThat(context).hasSingleBean(WavefrontSender.class)); + } + @Test void allowsWavefrontSenderToBeCustomized() { this.contextRunner.withUserConfiguration(CustomSenderConfiguration.class) diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/observability/ObservabilityContextCustomizerFactory.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/observability/ObservabilityContextCustomizerFactory.java index 918777baf1e7..86a003b6b6e0 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/observability/ObservabilityContextCustomizerFactory.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/observability/ObservabilityContextCustomizerFactory.java @@ -19,39 +19,19 @@ import java.util.List; import java.util.Objects; -import io.micrometer.tracing.Tracer; - -import org.springframework.aot.AotDetector; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.BeanFactoryUtils; -import org.springframework.beans.factory.FactoryBean; -import org.springframework.beans.factory.ListableBeanFactory; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; -import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.ConfigurationClassPostProcessor; -import org.springframework.core.Ordered; import org.springframework.core.env.Environment; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextCustomizerFactory; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.TestContextAnnotationUtils; -import org.springframework.util.ClassUtils; /** * {@link ContextCustomizerFactory} that globally disables metrics export and tracing in * tests. The behaviour can be controlled with {@link AutoConfigureObservability} on the * test class or via the {@value #AUTO_CONFIGURE_PROPERTY} property. - *

- * Registers {@link Tracer#NOOP} if tracing is disabled, micrometer-tracing is on the - * classpath, and the user hasn't supplied their own {@link Tracer}. * * @author Chris Bono * @author Moritz Halbritter @@ -87,7 +67,6 @@ public void customizeContext(ConfigurableApplicationContext context, } if (isTracingDisabled(context.getEnvironment())) { TestPropertyValues.of("management.tracing.enabled=false").applyTo(context); - registerNoopTracer(context); } } @@ -105,25 +84,6 @@ private boolean isTracingDisabled(Environment environment) { return !environment.getProperty(AUTO_CONFIGURE_PROPERTY, Boolean.class, false); } - private void registerNoopTracer(ConfigurableApplicationContext context) { - if (AotDetector.useGeneratedArtifacts()) { - return; - } - if (!ClassUtils.isPresent("io.micrometer.tracing.Tracer", context.getClassLoader())) { - return; - } - ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); - if (beanFactory instanceof BeanDefinitionRegistry registry) { - registerNoopTracer(registry); - } - } - - private void registerNoopTracer(BeanDefinitionRegistry registry) { - RootBeanDefinition definition = new RootBeanDefinition(NoopTracerRegistrar.class); - definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - registry.registerBeanDefinition(NoopTracerRegistrar.class.getName(), definition); - } - @Override public boolean equals(Object o) { if (this == o) { @@ -143,54 +103,4 @@ public int hashCode() { } - /** - * {@link BeanDefinitionRegistryPostProcessor} that runs after the - * {@link ConfigurationClassPostProcessor} and adds a {@link Tracer} bean definition - * when a {@link Tracer} hasn't already been registered. - */ - static class NoopTracerRegistrar implements BeanDefinitionRegistryPostProcessor, Ordered, BeanFactoryAware { - - private BeanFactory beanFactory; - - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.beanFactory = beanFactory; - } - - @Override - public int getOrder() { - return Ordered.LOWEST_PRECEDENCE; - } - - @Override - public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { - if (AotDetector.useGeneratedArtifacts()) { - return; - } - if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors((ListableBeanFactory) this.beanFactory, - Tracer.class, false, false).length == 0) { - registry.registerBeanDefinition("noopTracer", new RootBeanDefinition(NoopTracerFactoryBean.class)); - } - } - - @Override - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { - } - - } - - static class NoopTracerFactoryBean implements FactoryBean { - - @Override - public Tracer getObject() { - return Tracer.NOOP; - } - - @Override - public Class getObjectType() { - return Tracer.class; - } - - } - } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/observability/ObservabilityContextCustomizerFactoryTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/observability/ObservabilityContextCustomizerFactoryTests.java index d8e46424c52e..c8604bf53071 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/observability/ObservabilityContextCustomizerFactoryTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/observability/ObservabilityContextCustomizerFactoryTests.java @@ -18,22 +18,15 @@ import java.util.Collections; -import io.micrometer.tracing.Tracer; import org.junit.jupiter.api.Test; -import org.springframework.boot.context.annotation.UserConfigurations; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; import org.springframework.context.support.GenericApplicationContext; import org.springframework.mock.env.MockEnvironment; import org.springframework.test.context.ContextCustomizer; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; /** * Tests for {@link AutoConfigureObservability} and @@ -82,59 +75,6 @@ void shouldEnableBothWhenAnnotated() { assertThatTracingIsEnabled(context); } - @Test - void shouldRegisterNoopTracerIfTracingIsDisabled() { - ContextCustomizer customizer = createContextCustomizer(NoAnnotation.class); - ConfigurableApplicationContext context = new GenericApplicationContext(); - applyCustomizerToContext(customizer, context); - context.refresh(); - Tracer tracer = context.getBean(Tracer.class); - assertThat(tracer).isNotNull(); - assertThat(tracer.nextSpan().isNoop()).isTrue(); - } - - @Test - void shouldNotRegisterNoopTracerIfTracingIsEnabled() { - ContextCustomizer customizer = createContextCustomizer(WithAnnotation.class); - ConfigurableApplicationContext context = new GenericApplicationContext(); - applyCustomizerToContext(customizer, context); - context.refresh(); - assertThat(context.getBeanProvider(Tracer.class).getIfAvailable()).as("Tracer bean").isNull(); - } - - @Test - void shouldNotRegisterNoopTracerIfMicrometerTracingIsNotPresent() throws Exception { - try (FilteredClassLoader filteredClassLoader = new FilteredClassLoader("io.micrometer.tracing")) { - ContextCustomizer customizer = createContextCustomizer(NoAnnotation.class); - new ApplicationContextRunner().withClassLoader(filteredClassLoader) - .withInitializer(applyCustomizer(customizer)) - .run((context) -> { - assertThat(context).doesNotHaveBean(Tracer.class); - assertThatMetricsAreDisabled(context); - assertThatTracingIsDisabled(context); - }); - } - } - - @Test - void shouldBackOffOnCustomTracer() { - ContextCustomizer customizer = createContextCustomizer(NoAnnotation.class); - new ApplicationContextRunner().withConfiguration(UserConfigurations.of(CustomTracer.class)) - .withInitializer(applyCustomizer(customizer)) - .run((context) -> { - assertThat(context).hasSingleBean(Tracer.class); - assertThat(context).hasBean("customTracer"); - }); - } - - @Test - void shouldNotRunIfAotIsEnabled() { - ContextCustomizer customizer = createContextCustomizer(NoAnnotation.class); - new ApplicationContextRunner().withSystemProperties("spring.aot.enabled:true") - .withInitializer(applyCustomizer(customizer)) - .run((context) -> assertThat(context).doesNotHaveBean(Tracer.class)); - } - @Test void notEquals() { ContextCustomizer customizer1 = createContextCustomizer(OnlyMetrics.class); @@ -256,14 +196,4 @@ static class WithDisabledAnnotation { } - @Configuration(proxyBeanMethods = false) - static class CustomTracer { - - @Bean - Tracer customTracer() { - return mock(Tracer.class); - } - - } - } From 3664df61eb9b46cf14ade3e8322eea8ac369c6e7 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 16 Jun 2023 14:53:40 +0200 Subject: [PATCH 0020/1215] Polish API of SpanExporters and SpanProcessors --- .../tracing/OpenTelemetryAutoConfiguration.java | 6 +++--- .../boot/actuate/autoconfigure/tracing/SpanExporters.java | 6 +++--- .../boot/actuate/autoconfigure/tracing/SpanProcessors.java | 6 +++--- .../actuate/autoconfigure/tracing/SpanExportersTests.java | 4 ++-- .../actuate/autoconfigure/tracing/SpanProcessorsTests.java | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java index f2ccd0a5a629..e24dfc24473d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java @@ -135,9 +135,9 @@ SpanProcessors spanProcessors(ObjectProvider spanProcessors) { BatchSpanProcessor otelSpanProcessor(SpanExporters spanExporters, ObjectProvider spanExportingPredicates, ObjectProvider spanReporters, ObjectProvider spanFilters, ObjectProvider meterProvider) { - BatchSpanProcessorBuilder builder = BatchSpanProcessor.builder( - new CompositeSpanExporter(spanExporters.getList(), spanExportingPredicates.orderedStream().toList(), - spanReporters.orderedStream().toList(), spanFilters.orderedStream().toList())); + BatchSpanProcessorBuilder builder = BatchSpanProcessor + .builder(new CompositeSpanExporter(spanExporters.list(), spanExportingPredicates.orderedStream().toList(), + spanReporters.orderedStream().toList(), spanFilters.orderedStream().toList())); meterProvider.ifAvailable(builder::setMeterProvider); return builder.build(); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExporters.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExporters.java index b6f133e57b7a..d718047a9f23 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExporters.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExporters.java @@ -35,16 +35,16 @@ public interface SpanExporters extends Iterable { * Returns the list of {@link SpanExporter span exporters}. * @return the list of span exporters */ - List getList(); + List list(); @Override default Iterator iterator() { - return getList().iterator(); + return list().iterator(); } @Override default Spliterator spliterator() { - return getList().spliterator(); + return list().spliterator(); } /** diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanProcessors.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanProcessors.java index 3edc77b0de46..183e3bedac5f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanProcessors.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanProcessors.java @@ -35,16 +35,16 @@ public interface SpanProcessors extends Iterable { * Returns the list of {@link SpanProcessor span processors}. * @return the list of span processors */ - List getList(); + List list(); @Override default Iterator iterator() { - return getList().iterator(); + return list().iterator(); } @Override default Spliterator spliterator() { - return getList().spliterator(); + return list().spliterator(); } /** diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExportersTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExportersTests.java index dc883b971863..d15f2d1aceb6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExportersTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExportersTests.java @@ -37,7 +37,7 @@ void ofList() { SpanExporter spanExporter2 = mock(SpanExporter.class); SpanExporters spanExporters = SpanExporters.of(List.of(spanExporter1, spanExporter2)); assertThat(spanExporters).containsExactly(spanExporter1, spanExporter2); - assertThat(spanExporters.getList()).containsExactly(spanExporter1, spanExporter2); + assertThat(spanExporters.list()).containsExactly(spanExporter1, spanExporter2); } @Test @@ -46,7 +46,7 @@ void ofArray() { SpanExporter spanExporter2 = mock(SpanExporter.class); SpanExporters spanExporters = SpanExporters.of(spanExporter1, spanExporter2); assertThat(spanExporters).containsExactly(spanExporter1, spanExporter2); - assertThat(spanExporters.getList()).containsExactly(spanExporter1, spanExporter2); + assertThat(spanExporters.list()).containsExactly(spanExporter1, spanExporter2); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanProcessorsTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanProcessorsTests.java index f65a3b07dddd..8a5fa76868de 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanProcessorsTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanProcessorsTests.java @@ -37,7 +37,7 @@ void ofList() { SpanProcessor spanProcessor2 = mock(SpanProcessor.class); SpanProcessors spanProcessors = SpanProcessors.of(List.of(spanProcessor1, spanProcessor2)); assertThat(spanProcessors).containsExactly(spanProcessor1, spanProcessor2); - assertThat(spanProcessors.getList()).containsExactly(spanProcessor1, spanProcessor2); + assertThat(spanProcessors.list()).containsExactly(spanProcessor1, spanProcessor2); } @Test @@ -46,7 +46,7 @@ void ofArray() { SpanProcessor spanProcessor2 = mock(SpanProcessor.class); SpanProcessors spanProcessors = SpanProcessors.of(spanProcessor1, spanProcessor2); assertThat(spanProcessors).containsExactly(spanProcessor1, spanProcessor2); - assertThat(spanProcessors.getList()).containsExactly(spanProcessor1, spanProcessor2); + assertThat(spanProcessors.list()).containsExactly(spanProcessor1, spanProcessor2); } } From 854b29b8fbdb34df180b890db92c6cc584656a9e Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 16 Jun 2023 14:20:03 -0700 Subject: [PATCH 0021/1215] Polish --- .../OpenTelemetryAutoConfiguration.java | 5 ++-- .../autoconfigure/tracing/SpanExporters.java | 26 ++++++++++++------- .../autoconfigure/tracing/SpanProcessors.java | 26 ++++++++++++------- .../web/embedded/JettyThreadPool.java | 9 +++---- 4 files changed, 38 insertions(+), 28 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java index 93553ead79c4..8a3a4d79f820 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java @@ -18,7 +18,6 @@ import java.util.Collections; import java.util.List; -import java.util.stream.Collectors; import io.micrometer.tracing.SpanCustomizer; import io.micrometer.tracing.exporter.SpanExportingPredicate; @@ -128,7 +127,7 @@ Sampler otelSampler() { @Bean @ConditionalOnMissingBean SpanProcessors spanProcessors(ObjectProvider spanProcessors) { - return SpanProcessors.of(spanProcessors.orderedStream().collect(Collectors.toList())); + return SpanProcessors.of(spanProcessors.orderedStream().toList()); } @Bean @@ -145,7 +144,7 @@ BatchSpanProcessor otelSpanProcessor(SpanExporters spanExporters, @Bean @ConditionalOnMissingBean SpanExporters spanExporters(ObjectProvider spanExporters) { - return SpanExporters.of(spanExporters.orderedStream().collect(Collectors.toList())); + return SpanExporters.of(spanExporters.orderedStream().toList()); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExporters.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExporters.java index d718047a9f23..a44f8ce0e035 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExporters.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanExporters.java @@ -17,18 +17,22 @@ package org.springframework.boot.actuate.autoconfigure.tracing; import java.util.Arrays; +import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Spliterator; import io.opentelemetry.sdk.trace.export.SpanExporter; +import org.springframework.util.Assert; + /** * A collection of {@link SpanExporter span exporters}. * * @author Moritz Halbritter * @since 3.2.0 */ +@FunctionalInterface public interface SpanExporters extends Iterable { /** @@ -47,16 +51,6 @@ default Spliterator spliterator() { return list().spliterator(); } - /** - * Constructs a {@link SpanExporters} instance with the given list of - * {@link SpanExporter span exporters}. - * @param spanExporters the list of span exporters - * @return the constructed {@link SpanExporters} instance - */ - static SpanExporters of(List spanExporters) { - return () -> spanExporters; - } - /** * Constructs a {@link SpanExporters} instance with the given {@link SpanExporter span * exporters}. @@ -67,4 +61,16 @@ static SpanExporters of(SpanExporter... spanExporters) { return of(Arrays.asList(spanExporters)); } + /** + * Constructs a {@link SpanExporters} instance with the given list of + * {@link SpanExporter span exporters}. + * @param spanExporters the list of span exporters + * @return the constructed {@link SpanExporters} instance + */ + static SpanExporters of(Collection spanExporters) { + Assert.notNull(spanExporters, "SpanExporters must not be null"); + List copy = List.copyOf(spanExporters); + return () -> copy; + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanProcessors.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanProcessors.java index 183e3bedac5f..ca8c55498d07 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanProcessors.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/SpanProcessors.java @@ -17,18 +17,22 @@ package org.springframework.boot.actuate.autoconfigure.tracing; import java.util.Arrays; +import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Spliterator; import io.opentelemetry.sdk.trace.SpanProcessor; +import org.springframework.util.Assert; + /** * A collection of {@link SpanProcessor span processors}. * * @author Moritz Halbritter * @since 3.2.0 */ +@FunctionalInterface public interface SpanProcessors extends Iterable { /** @@ -47,16 +51,6 @@ default Spliterator spliterator() { return list().spliterator(); } - /** - * Constructs a {@link SpanProcessors} instance with the given list of - * {@link SpanProcessor span processors}. - * @param spanProcessors the list of span processors - * @return the constructed {@link SpanProcessors} instance - */ - static SpanProcessors of(List spanProcessors) { - return () -> spanProcessors; - } - /** * Constructs a {@link SpanProcessors} instance with the given {@link SpanProcessor * span processors}. @@ -67,4 +61,16 @@ static SpanProcessors of(SpanProcessor... spanProcessors) { return of(Arrays.asList(spanProcessors)); } + /** + * Constructs a {@link SpanProcessors} instance with the given list of + * {@link SpanProcessor span processors}. + * @param spanProcessors the list of span processors + * @return the constructed {@link SpanProcessors} instance + */ + static SpanProcessors of(Collection spanProcessors) { + Assert.notNull(spanProcessors, "SpanProcessors must not be null"); + List copy = List.copyOf(spanProcessors); + return () -> copy; + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyThreadPool.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyThreadPool.java index 56d01c91a232..7c8dadadb908 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyThreadPool.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyThreadPool.java @@ -26,8 +26,9 @@ import org.springframework.boot.autoconfigure.web.ServerProperties; /** - * Creates a {@link ThreadPool} for Jetty, applying the - * {@link ServerProperties.Jetty.Threads} properties. + * Creates a {@link ThreadPool} for Jetty, applying + * {@link org.springframework.boot.autoconfigure.web.ServerProperties.Jetty.Threads + * ServerProperties.Jetty.Threads Jetty thread properties}. * * @author Moritz Halbritter */ @@ -52,9 +53,7 @@ private static BlockingQueue determineBlockingQueue(Integer maxQueueCa if (maxQueueCapacity == 0) { return new SynchronousQueue<>(); } - else { - return new BlockingArrayQueue<>(maxQueueCapacity); - } + return new BlockingArrayQueue<>(maxQueueCapacity); } } From fe3579bd1e0814cbfefd157d1e057480b65d42b4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:54:40 +0100 Subject: [PATCH 0022/1215] Upgrade to Angus Mail 2.0.2 Closes gh-35943 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index dfff8d658703..eea4a1b8c77c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -50,7 +50,7 @@ bom { ] } } - library("Angus Mail", "1.1.0") { + library("Angus Mail", "2.0.2") { group("org.eclipse.angus") { modules = [ "angus-core", From fd2ae822f0d6d7c874115038073005f298863f46 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:54:46 +0100 Subject: [PATCH 0023/1215] Upgrade to Brave 5.16.0 Closes gh-35944 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index eea4a1b8c77c..8278f7b917a9 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -109,7 +109,7 @@ bom { ] } } - library("Brave", "5.15.1") { + library("Brave", "5.16.0") { group("io.zipkin.brave") { imports = [ "brave-bom" From 57a44bf55d02fdf0e2d1ad4c94a06cb8f81b1462 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:54:51 +0100 Subject: [PATCH 0024/1215] Upgrade to Build Helper Maven Plugin 3.4.0 Closes gh-35945 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 8278f7b917a9..dac5a516fe62 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -116,7 +116,7 @@ bom { ] } } - library("Build Helper Maven Plugin", "3.3.0") { + library("Build Helper Maven Plugin", "3.4.0") { group("org.codehaus.mojo") { plugins = [ "build-helper-maven-plugin" From 6b1a1141aaddc160eef23c72d5f9071244ab2c41 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:54:56 +0100 Subject: [PATCH 0025/1215] Upgrade to Cassandra Driver 4.16.0 Closes gh-35946 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index dac5a516fe62..fca2d584008a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -153,7 +153,7 @@ bom { ] } } - library("Cassandra Driver", "4.15.0") { + library("Cassandra Driver", "4.16.0") { group("com.datastax.oss") { imports = [ "java-driver-bom" From 3d336f92044dbf824cadc181312f8e2b992019a1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:55:00 +0100 Subject: [PATCH 0026/1215] Upgrade to Elasticsearch Client 8.8.1 Closes gh-35947 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index fca2d584008a..8d82984d26ae 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -262,7 +262,7 @@ bom { ] } } - library("Elasticsearch Client", "8.7.1") { + library("Elasticsearch Client", "8.8.1") { group("org.elasticsearch.client") { modules = [ "elasticsearch-rest-client" { From 454aae92d674206ff73155c1cdf7fae660db5488 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:55:05 +0100 Subject: [PATCH 0027/1215] Upgrade to Flyway 9.19.4 Closes gh-35948 --- .../autoconfigure/flyway/FlywayAutoConfiguration.java | 2 +- .../flyway/Flyway90AutoConfigurationTests.java | 2 ++ .../flyway/FlywayAutoConfigurationTests.java | 11 ++++++++++- .../autoconfigure/flyway/FlywayPropertiesTests.java | 2 +- .../spring-boot-dependencies/build.gradle | 2 +- 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java index 3b9294ce6fbc..0c5928ed27c3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java @@ -288,7 +288,7 @@ private void configureSqlServerKerberosLoginFile(FluentConfiguration configurati SQLServerConfigurationExtension sqlServerConfigurationExtension = configuration.getPluginRegister() .getPlugin(SQLServerConfigurationExtension.class); Assert.state(sqlServerConfigurationExtension != null, "Flyway SQL Server extension missing"); - sqlServerConfigurationExtension.setKerberosLoginFile(sqlServerKerberosLoginFile); + sqlServerConfigurationExtension.getKerberos().getLogin().setFile(sqlServerKerberosLoginFile); } private void configureCallbacks(FluentConfiguration configuration, List callbacks) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway90AutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway90AutoConfigurationTests.java index 6acaa1851ecb..97fd9ad3e6d9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway90AutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway90AutoConfigurationTests.java @@ -25,6 +25,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; import org.springframework.boot.testsupport.classpath.ClassPathOverrides; import static org.assertj.core.api.Assertions.assertThat; @@ -34,6 +35,7 @@ * * @author Andy Wilkinson */ +@ClassPathExclusions("flyway-*.jar") @ClassPathOverrides("org.flywaydb:flyway-core:9.0.4") class Flyway90AutoConfigurationTests { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java index b2148d973e5d..8aa67ff18922 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java @@ -33,6 +33,7 @@ import org.flywaydb.core.api.callback.Event; import org.flywaydb.core.api.migration.JavaMigration; import org.flywaydb.core.internal.license.FlywayTeamsUpgradeRequiredException; +import org.flywaydb.database.sqlserver.SQLServerConfigurationExtension; import org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform; import org.jooq.DSLContext; import org.jooq.SQLDialect; @@ -700,7 +701,15 @@ void outputQueryResultsIsCorrectlyMapped() { void sqlServerKerberosLoginFileIsCorrectlyMapped() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) .withPropertyValues("spring.flyway.sql-server-kerberos-login-file=/tmp/config") - .run(validateFlywayTeamsPropertyOnly("sqlserver.kerberos.login.file")); + .run((context) -> { + assertThat(context.getBean(Flyway.class) + .getConfiguration() + .getPluginRegister() + .getPlugin(SQLServerConfigurationExtension.class) + .getKerberos() + .getLogin() + .getFile()).isEqualTo("/tmp/config"); + }); } @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java index 98c3454b227f..0c7bb4110a2d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java @@ -116,7 +116,7 @@ void expectedPropertiesAreManaged() { "javaMigrationClassProvider", "pluginRegister", "resourceProvider", "resolvers"); // Properties we don't want to expose ignoreProperties(configuration, "resolversAsClassNames", "callbacksAsClassNames", "driver", "modernConfig", - "currentResolvedEnvironment", "reportFilename"); + "currentResolvedEnvironment", "reportFilename", "reportEnabled"); // Handled by the conversion service ignoreProperties(configuration, "baselineVersionAsString", "encodingAsString", "locationsAsStrings", "targetAsString"); diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 8d82984d26ae..136e6df1cd74 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -279,7 +279,7 @@ bom { ] } } - library("Flyway", "9.16.3") { + library("Flyway", "9.19.4") { group("org.flywaydb") { modules = [ "flyway-core", From e4b207e73b937bc833420124c9866f9e8f38521a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:55:10 +0100 Subject: [PATCH 0028/1215] Upgrade to Git Commit ID Maven Plugin 6.0.0 Closes gh-35949 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 136e6df1cd74..f1f616cf793f 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -299,7 +299,7 @@ bom { ] } } - library("Git Commit ID Maven Plugin", "5.0.1") { + library("Git Commit ID Maven Plugin", "6.0.0") { group("io.github.git-commit-id") { plugins = [ "git-commit-id-maven-plugin" From 9d8caf1133c9c10604dfb392944dde28a39960aa Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:55:15 +0100 Subject: [PATCH 0029/1215] Upgrade to Hazelcast 5.3.1 Closes gh-35950 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f1f616cf793f..569a3727097e 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -357,7 +357,7 @@ bom { ] } } - library("Hazelcast", "5.2.4") { + library("Hazelcast", "5.3.1") { group("com.hazelcast") { modules = [ "hazelcast", From 152fc42d4e467365ac08226202c45d3df51fccf9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:55:20 +0100 Subject: [PATCH 0030/1215] Upgrade to Hibernate 6.2.5.Final Closes gh-35951 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 569a3727097e..9946b8d70fd9 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -365,7 +365,7 @@ bom { ] } } - library("Hibernate", "6.2.4.Final") { + library("Hibernate", "6.2.5.Final") { group("org.hibernate.orm") { modules = [ "hibernate-agroal", From fc269a5c698685ba53567a3699cf493beda953e0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:55:25 +0100 Subject: [PATCH 0031/1215] Upgrade to Infinispan 14.0.11.Final Closes gh-35952 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 9946b8d70fd9..785264a311c3 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -453,7 +453,7 @@ bom { ] } } - library("Infinispan", "14.0.10.Final") { + library("Infinispan", "14.0.11.Final") { group("org.infinispan") { imports = [ "infinispan-bom" From dfe317ef81f14a8507ce092acd7daecf23a8a972 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:55:29 +0100 Subject: [PATCH 0032/1215] Upgrade to Jedis 4.4.3 Closes gh-35953 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 785264a311c3..f04b99c6d32c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -645,7 +645,7 @@ bom { ] } } - library("Jedis", "4.3.2") { + library("Jedis", "4.4.3") { group("redis.clients") { modules = [ "jedis" From e82bd223a036fe0826bdaa95002a3b9145bca826 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:55:34 +0100 Subject: [PATCH 0033/1215] Upgrade to Kafka 3.5.0 Closes gh-35954 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f04b99c6d32c..b649b4b62e8a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -736,7 +736,7 @@ bom { ] } } - library("Kafka", "3.4.1") { + library("Kafka", "3.5.0") { group("org.apache.kafka") { modules = [ "connect", From fdc6f544052f068cb35e2a9ad80a07e84dd0075e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:55:39 +0100 Subject: [PATCH 0034/1215] Upgrade to Kotlin Coroutines 1.7.1 Closes gh-35955 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index b649b4b62e8a..bb8f848edef7 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -787,7 +787,7 @@ bom { ] } } - library("Kotlin Coroutines", "1.6.4") { + library("Kotlin Coroutines", "1.7.1") { group("org.jetbrains.kotlinx") { imports = [ "kotlinx-coroutines-bom" From 1e17d8eeeac091d625b6853f0c517dcd9b218c65 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:55:44 +0100 Subject: [PATCH 0035/1215] Upgrade to Liquibase 4.22.0 Closes gh-35956 --- .../spring-boot-actuator/build.gradle | 1 + .../spring-boot-autoconfigure/build.gradle | 1 + .../flyway/FlywayAutoConfigurationTests.java | 16 +++++++--------- .../spring-boot-dependencies/build.gradle | 2 +- spring-boot-project/spring-boot/build.gradle | 1 + 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator/build.gradle b/spring-boot-project/spring-boot-actuator/build.gradle index c4e393059178..3f7999ca49d4 100644 --- a/spring-boot-project/spring-boot-actuator/build.gradle +++ b/spring-boot-project/spring-boot-actuator/build.gradle @@ -50,6 +50,7 @@ dependencies { optional("org.hibernate.validator:hibernate-validator") optional("org.influxdb:influxdb-java") optional("org.liquibase:liquibase-core") { + exclude group: "javax.activation", module: "javax.activation-api" exclude(group: "javax.xml.bind", module: "jaxb-api") } optional("org.mongodb:mongodb-driver-reactivestreams") diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index db2a24fb5066..03e84c83456a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -130,6 +130,7 @@ dependencies { exclude group: "javax.xml.bind", module: "jaxb-api" } optional("org.liquibase:liquibase-core") { + exclude group: "javax.activation", module: "javax.activation-api" exclude group: "javax.xml.bind", module: "jaxb-api" } optional("org.messaginghub:pooled-jms") { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java index 8aa67ff18922..b2149465d8b2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java @@ -701,15 +701,13 @@ void outputQueryResultsIsCorrectlyMapped() { void sqlServerKerberosLoginFileIsCorrectlyMapped() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) .withPropertyValues("spring.flyway.sql-server-kerberos-login-file=/tmp/config") - .run((context) -> { - assertThat(context.getBean(Flyway.class) - .getConfiguration() - .getPluginRegister() - .getPlugin(SQLServerConfigurationExtension.class) - .getKerberos() - .getLogin() - .getFile()).isEqualTo("/tmp/config"); - }); + .run((context) -> assertThat(context.getBean(Flyway.class) + .getConfiguration() + .getPluginRegister() + .getPlugin(SQLServerConfigurationExtension.class) + .getKerberos() + .getLogin() + .getFile()).isEqualTo("/tmp/config")); } @Test diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index bb8f848edef7..3cab5e9da2ae 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -803,7 +803,7 @@ bom { } library("Liquibase", "4.20.0") { prohibit { - versionRange "[4.21.0,4.21.2)" + versionRange "[4.21.0,4.22.0]" because "https://github.com/liquibase/liquibase/issues/4135" } group("org.liquibase") { diff --git a/spring-boot-project/spring-boot/build.gradle b/spring-boot-project/spring-boot/build.gradle index f0831197439b..3a862fe98b67 100644 --- a/spring-boot-project/spring-boot/build.gradle +++ b/spring-boot-project/spring-boot/build.gradle @@ -75,6 +75,7 @@ dependencies { exclude(group: "javax.xml.bind", module: "jaxb-api") } optional("org.liquibase:liquibase-core") { + exclude group: "javax.activation", module: "javax.activation-api" exclude(group: "javax.xml.bind", module: "jaxb-api") } optional("org.postgresql:postgresql") From 6745ec92240fc847d6eb3be5ce5fbcdc8e67a8e0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:55:49 +0100 Subject: [PATCH 0036/1215] Upgrade to Maven Assembly Plugin 3.6.0 Closes gh-35957 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 3cab5e9da2ae..549c6d1a493d 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -853,7 +853,7 @@ bom { ] } } - library("Maven Assembly Plugin", "3.5.0") { + library("Maven Assembly Plugin", "3.6.0") { group("org.apache.maven.plugins") { plugins = [ "maven-assembly-plugin" From 559989a0adb81fa982f3ea829bd5722988cab872 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:55:55 +0100 Subject: [PATCH 0037/1215] Upgrade to Maven Dependency Plugin 3.6.0 Closes gh-35958 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 549c6d1a493d..217888ccde6a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -874,7 +874,7 @@ bom { ] } } - library("Maven Dependency Plugin", "3.5.0") { + library("Maven Dependency Plugin", "3.6.0") { group("org.apache.maven.plugins") { plugins = [ "maven-dependency-plugin" From 3c482a024c7f95f0cf6f76097529707d3355dde2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:55:59 +0100 Subject: [PATCH 0038/1215] Upgrade to Maven Failsafe Plugin 3.1.2 Closes gh-35959 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 217888ccde6a..1c0b3997017d 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -895,7 +895,7 @@ bom { ] } } - library("Maven Failsafe Plugin", "3.0.0") { + library("Maven Failsafe Plugin", "3.1.2") { group("org.apache.maven.plugins") { plugins = [ "maven-failsafe-plugin" From b167c6e45dce1b261be23f2ea6d5c77b401152d9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:56:05 +0100 Subject: [PATCH 0039/1215] Upgrade to Maven Invoker Plugin 3.6.0 Closes gh-35960 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 1c0b3997017d..eb43f034503c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -916,7 +916,7 @@ bom { ] } } - library("Maven Invoker Plugin", "3.5.1") { + library("Maven Invoker Plugin", "3.6.0") { group("org.apache.maven.plugins") { plugins = [ "maven-invoker-plugin" From 853db91e3125d5c20c9d42b715d96902af53d880 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:56:10 +0100 Subject: [PATCH 0040/1215] Upgrade to Maven Shade Plugin 3.5.0 Closes gh-35961 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index eb43f034503c..973b2216a385 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -944,7 +944,7 @@ bom { ] } } - library("Maven Shade Plugin", "3.4.1") { + library("Maven Shade Plugin", "3.5.0") { group("org.apache.maven.plugins") { plugins = [ "maven-shade-plugin" From 0cac2e26036449cb89ea161a922fec4433ff8fe7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:56:15 +0100 Subject: [PATCH 0041/1215] Upgrade to Maven Source Plugin 3.3.0 Closes gh-35962 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 973b2216a385..caebd11efcf4 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -951,7 +951,7 @@ bom { ] } } - library("Maven Source Plugin", "3.2.1") { + library("Maven Source Plugin", "3.3.0") { group("org.apache.maven.plugins") { plugins = [ "maven-source-plugin" From 045307994bb87797bd1e0acf3430b1c6915c1291 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:56:19 +0100 Subject: [PATCH 0042/1215] Upgrade to Maven Surefire Plugin 3.1.2 Closes gh-35963 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index caebd11efcf4..38c491b33c14 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -958,7 +958,7 @@ bom { ] } } - library("Maven Surefire Plugin", "3.0.0") { + library("Maven Surefire Plugin", "3.1.2") { group("org.apache.maven.plugins") { plugins = [ "maven-surefire-plugin" From a5df44cbcaac8faa28374af854f83f860b2bc15f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:56:24 +0100 Subject: [PATCH 0043/1215] Upgrade to Maven War Plugin 3.4.0 Closes gh-35964 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 38c491b33c14..f49779303979 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -965,7 +965,7 @@ bom { ] } } - library("Maven War Plugin", "3.3.2") { + library("Maven War Plugin", "3.4.0") { group("org.apache.maven.plugins") { plugins = [ "maven-war-plugin" From 9fb9d5518d2d9cb141458f70683bd3e8d12ecb64 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:56:29 +0100 Subject: [PATCH 0044/1215] Upgrade to Mockito 5.4.0 Closes gh-35965 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f49779303979..4c7782f43887 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -991,7 +991,7 @@ bom { ] } } - library("Mockito", "5.3.1") { + library("Mockito", "5.4.0") { group("org.mockito") { imports = [ "mockito-bom" From d5fcfabb10cd5dc89b8f437d93b5e993c0389ddd Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:56:34 +0100 Subject: [PATCH 0045/1215] Upgrade to Native Build Tools Plugin 0.9.23 Closes gh-35966 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 946d095164b4..15ed1df1ccc8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ org.gradle.parallel=true org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 kotlinVersion=1.8.22 -nativeBuildToolsVersion=0.9.22 +nativeBuildToolsVersion=0.9.23 springFrameworkVersion=6.0.10 tomcatVersion=10.1.10 From fe1f675c43f412309a80c0eb1a3b26e8c82f16f8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:56:39 +0100 Subject: [PATCH 0046/1215] Upgrade to Neo4j Java Driver 5.9.0 Closes gh-35967 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 4c7782f43887..682e6ccd77c5 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1044,7 +1044,7 @@ bom { ] } } - library("Neo4j Java Driver", "5.8.0") { + library("Neo4j Java Driver", "5.9.0") { group("org.neo4j.driver") { modules = [ "neo4j-java-driver" From 2ce6458cd491dbccb9b094da970739559315f423 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:56:44 +0100 Subject: [PATCH 0047/1215] Upgrade to OkHttp 4.11.0 Closes gh-35968 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 682e6ccd77c5..6f7a4de9f48e 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1058,7 +1058,7 @@ bom { ] } } - library("OkHttp", "4.10.0") { + library("OkHttp", "4.11.0") { group("com.squareup.okhttp3") { imports = [ "okhttp-bom" From be1eb32ac064b47e4457eb3c81268b8cf1b14eda Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:56:48 +0100 Subject: [PATCH 0048/1215] Upgrade to OpenTelemetry 1.27.0 Closes gh-35969 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 6f7a4de9f48e..c1298bbd5537 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1065,7 +1065,7 @@ bom { ] } } - library("OpenTelemetry", "1.25.0") { + library("OpenTelemetry", "1.27.0") { group("io.opentelemetry") { imports = [ "opentelemetry-bom" From e94f35f85a3016a2bab5c684f3bc5d43c164109b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:56:58 +0100 Subject: [PATCH 0049/1215] Upgrade to Rabbit Stream Client 0.10.0 Closes gh-35971 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c1298bbd5537..5f71b6e91a9c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1188,7 +1188,7 @@ bom { ] } } - library("Rabbit Stream Client", "0.9.0") { + library("Rabbit Stream Client", "0.10.0") { group("com.rabbitmq") { modules = [ "stream-client" From a63cf9dd7f6ea59599987e3be0b9ea2670914b4a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:57:03 +0100 Subject: [PATCH 0050/1215] Upgrade to REST Assured 5.3.1 Closes gh-35972 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5f71b6e91a9c..be9428d7ede1 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1209,7 +1209,7 @@ bom { ] } } - library("REST Assured", "5.3.0") { + library("REST Assured", "5.3.1") { group("io.rest-assured") { imports = [ "rest-assured-bom" From 7053c3e0fcf8b84c8615a6f5419eb80ebbcb05b3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:57:12 +0100 Subject: [PATCH 0051/1215] Upgrade to Selenium 4.10.0 Closes gh-35974 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index be9428d7ede1..d3c0aadaca16 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1316,7 +1316,7 @@ bom { ] } } - library("Selenium", "4.8.3") { + library("Selenium", "4.10.0") { group("org.seleniumhq.selenium") { modules = [ "lift", From 9ab94ef8a3321beff343dfa1f02894bb756c1403 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:57:17 +0100 Subject: [PATCH 0052/1215] Upgrade to Selenium HtmlUnit 4.10.0 Closes gh-35975 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d3c0aadaca16..51f79be01606 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1344,7 +1344,7 @@ bom { ] } } - library("Selenium HtmlUnit", "4.8.3") { + library("Selenium HtmlUnit", "4.10.0") { group("org.seleniumhq.selenium") { modules = [ "htmlunit-driver" From 4dc0b26eeadb7407685dcad668217738a5af3379 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:57:22 +0100 Subject: [PATCH 0053/1215] Upgrade to Spring AMQP 3.0.5 Closes gh-35976 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 51f79be01606..c2af53dad5fa 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1382,7 +1382,7 @@ bom { ] } } - library("Spring AMQP", "3.0.5-SNAPSHOT") { + library("Spring AMQP", "3.0.5") { group("org.springframework.amqp") { modules = [ "spring-amqp", From ca5bd37e81929c340b0aca6e270decb0f7afb360 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:57:27 +0100 Subject: [PATCH 0054/1215] Upgrade to Spring Framework 6.1.0-M1 Closes gh-35977 Closes gh-35980 --- buildSrc/build.gradle | 9 +-- gradle.properties | 2 +- .../OrderedServerHttpObservationFilter.java | 1 + .../test/web/client/TestRestTemplate.java | 3 - .../context/SpringBootContextLoaderTests.java | 2 +- ...pringBootTestContextBootstrapperTests.java | 2 +- .../build.gradle | 5 ++ .../spring-boot-gradle-plugin/build.gradle | 5 ++ .../spring-boot-loader-tools/build.gradle | 11 ++++ .../client/ClientHttpRequestFactories.java | 11 ---- ...lientHttpRequestFactoriesRuntimeHints.java | 1 - .../ClientHttpRequestFactorySettings.java | 59 +++++++++++++------ .../boot/web/client/RestTemplateBuilder.java | 11 ++-- .../HttpWebServiceMessageSenderBuilder.java | 2 +- ...lientHttpRequestFactoriesOkHttp3Tests.java | 7 --- ...lientHttpRequestFactoriesOkHttp4Tests.java | 7 --- ...HttpRequestFactoriesRuntimeHintsTests.java | 9 --- .../ClientHttpRequestFactoriesTests.java | 41 ------------- ...ClientHttpRequestFactorySettingsTests.java | 14 ----- .../spring-boot-image-tests/build.gradle | 5 ++ 20 files changed, 78 insertions(+), 129 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 2968c3e14e79..a0fa56fff9d6 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -14,15 +14,10 @@ new File(new File("$projectDir").parentFile, "gradle.properties").withInputStrea def properties = new Properties() properties.load(it) ext.set("kotlinVersion", properties["kotlinVersion"]) - ext.set("springFrameworkVersion", properties["springFrameworkVersion"]) - if (properties["springFrameworkVersion"].contains("-")) { - repositories { - maven { url "https://repo.spring.io/milestone" } - maven { url "https://repo.spring.io/snapshot" } - } - } } +ext.set("springFrameworkVersion", "6.0.10") + sourceCompatibility = 17 targetCompatibility = 17 diff --git a/gradle.properties b/gradle.properties index 15ed1df1ccc8..eb9bbe14aff8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 kotlinVersion=1.8.22 nativeBuildToolsVersion=0.9.23 -springFrameworkVersion=6.0.10 +springFrameworkVersion=6.1.0-M1 tomcatVersion=10.1.10 kotlin.stdlib.default.dependency=false diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/OrderedServerHttpObservationFilter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/OrderedServerHttpObservationFilter.java index 9d3146f1dbee..4541f6e16521 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/OrderedServerHttpObservationFilter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/OrderedServerHttpObservationFilter.java @@ -28,6 +28,7 @@ * * @author Moritz Halbritter */ +@SuppressWarnings({ "deprecation", "removal" }) class OrderedServerHttpObservationFilter extends ServerHttpObservationFilter implements OrderedWebFilter { private final int order; diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplate.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplate.java index dca0856b7089..24e38b8ab503 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplate.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplate.java @@ -1021,9 +1021,6 @@ public CustomHttpComponentsClientHttpRequestFactory(HttpClientOption[] httpClien if (settings.connectTimeout() != null) { setConnectTimeout((int) settings.connectTimeout().toMillis()); } - if (settings.bufferRequestBody() != null) { - setBufferRequestBody(settings.bufferRequestBody()); - } } private HttpClient createHttpClient(Duration readTimeout, boolean ssl) { diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootContextLoaderTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootContextLoaderTests.java index 6715719eab48..5102e242d562 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootContextLoaderTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootContextLoaderTests.java @@ -255,7 +255,7 @@ private String[] getActiveProfiles(Class testClass) { private Map getMergedContextConfigurationProperties(Class testClass) { TestContext context = new ExposedTestContextManager(testClass).getExposedTestContext(); MergedContextConfiguration config = (MergedContextConfiguration) ReflectionTestUtils.getField(context, - "mergedContextConfiguration"); + "mergedConfig"); return TestPropertySourceUtils.convertInlinedPropertiesToMap(config.getPropertySourceProperties()); } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/SpringBootTestContextBootstrapperTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/SpringBootTestContextBootstrapperTests.java index db724a374a1e..c38c81fc8121 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/SpringBootTestContextBootstrapperTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/SpringBootTestContextBootstrapperTests.java @@ -110,7 +110,7 @@ private TestContext buildTestContext(Class testClass) { } private MergedContextConfiguration getMergedContextConfiguration(TestContext context) { - return (MergedContextConfiguration) ReflectionTestUtils.getField(context, "mergedContextConfiguration"); + return (MergedContextConfiguration) ReflectionTestUtils.getField(context, "mergedConfig"); } @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/build.gradle index d890be6a2af9..8f5db820dfba 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/build.gradle @@ -14,6 +14,11 @@ configurations.all { if (dependency.requested.group.startsWith("com.fasterxml.jackson")) { dependency.useVersion("2.14.2") } + // Downgrade Spring Framework as Gradle cannot cope with 6.1.0-M1's + // multi-version jar files with bytecode in META-INF/versions/21 + if (dependency.requested.group.equals("org.springframework")) { + dependency.useVersion("6.0.10") + } } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle index 80b5f7e9402b..7939ef31bb98 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle @@ -23,6 +23,11 @@ configurations { if (dependency.requested.group.startsWith("com.fasterxml.jackson")) { dependency.useVersion("2.14.2") } + // Downgrade Spring Framework as Gradle cannot cope with 6.1.0-M1's + // multi-version jar files with bytecode in META-INF/versions/21 + if (dependency.requested.group.equals("org.springframework")) { + dependency.useVersion("6.0.10") + } } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle index c6b187317071..755f1cc7bc73 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle @@ -17,6 +17,17 @@ configurations { extendsFrom dependencyManagement transitive = false } + all { + resolutionStrategy { + eachDependency { dependency -> + // Downgrade Spring Framework as Gradle cannot cope with 6.1.0-M1's + // multi-version jar files with bytecode in META-INF/versions/21 + if (dependency.requested.group.equals("org.springframework")) { + dependency.useVersion("6.0.10") + } + } + } + } } dependencies { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java index 4f6de573f682..61b3e9361017 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java @@ -151,7 +151,6 @@ static HttpComponentsClientHttpRequestFactory get(ClientHttpRequestFactorySettin settings.sslBundle()); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(settings::connectTimeout).asInt(Duration::toMillis).to(requestFactory::setConnectTimeout); - map.from(settings::bufferRequestBody).to(requestFactory::setBufferRequestBody); return requestFactory; } @@ -187,8 +186,6 @@ private static HttpClient createHttpClient(Duration readTimeout, SslBundle sslBu static class OkHttp { static OkHttp3ClientHttpRequestFactory get(ClientHttpRequestFactorySettings settings) { - Assert.state(settings.bufferRequestBody() == null, - () -> "OkHttp3ClientHttpRequestFactory does not support request body buffering"); OkHttp3ClientHttpRequestFactory requestFactory = createRequestFactory(settings.sslBundle()); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(settings::connectTimeout).asInt(Duration::toMillis).to(requestFactory::setConnectTimeout); @@ -227,7 +224,6 @@ static SimpleClientHttpRequestFactory get(ClientHttpRequestFactorySettings setti PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(settings::readTimeout).asInt(Duration::toMillis).to(requestFactory::setReadTimeout); map.from(settings::connectTimeout).asInt(Duration::toMillis).to(requestFactory::setConnectTimeout); - map.from(settings::bufferRequestBody).to(requestFactory::setBufferRequestBody); return requestFactory; } @@ -274,8 +270,6 @@ private static void configure(ClientHttpRequestFactory requestFactory, PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(settings::connectTimeout).to((connectTimeout) -> setConnectTimeout(unwrapped, connectTimeout)); map.from(settings::readTimeout).to((readTimeout) -> setReadTimeout(unwrapped, readTimeout)); - map.from(settings::bufferRequestBody) - .to((bufferRequestBody) -> setBufferRequestBody(unwrapped, bufferRequestBody)); } private static ClientHttpRequestFactory unwrapRequestFactoryIfNecessary( @@ -305,11 +299,6 @@ private static void setReadTimeout(ClientHttpRequestFactory factory, Duration re invoke(factory, method, timeout); } - private static void setBufferRequestBody(ClientHttpRequestFactory factory, boolean bufferRequestBody) { - Method method = findMethod(factory, "setBufferRequestBody", boolean.class); - invoke(factory, method, bufferRequestBody); - } - private static Method findMethod(ClientHttpRequestFactory requestFactory, String methodName, Class... parameters) { Method method = ReflectionUtils.findMethod(requestFactory.getClass(), methodName, parameters); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHints.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHints.java index c47ef109a64c..457110d6c116 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHints.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHints.java @@ -69,7 +69,6 @@ private void registerReflectionHints(ReflectionHints hints, Class requestFactoryType) { registerMethod(hints, requestFactoryType, "setConnectTimeout", int.class); registerMethod(hints, requestFactoryType, "setReadTimeout", int.class); - registerMethod(hints, requestFactoryType, "setBufferRequestBody", boolean.class); } private void registerMethod(ReflectionHints hints, Class requestFactoryType, diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactorySettings.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactorySettings.java index 22deb5a4a16b..204acffb933d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactorySettings.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactorySettings.java @@ -26,7 +26,6 @@ * * @param connectTimeout the connect timeout * @param readTimeout the read timeout - * @param bufferRequestBody if request body buffering is used * @param sslBundle the SSL bundle providing SSL configuration * @author Andy Wilkinson * @author Phillip Webb @@ -34,8 +33,7 @@ * @since 3.0.0 * @see ClientHttpRequestFactories */ -public record ClientHttpRequestFactorySettings(Duration connectTimeout, Duration readTimeout, Boolean bufferRequestBody, - SslBundle sslBundle) { +public record ClientHttpRequestFactorySettings(Duration connectTimeout, Duration readTimeout, SslBundle sslBundle) { /** * Use defaults for the {@link ClientHttpRequestFactory} which can differ depending on @@ -48,15 +46,29 @@ public record ClientHttpRequestFactorySettings(Duration connectTimeout, Duration * Create a new {@link ClientHttpRequestFactorySettings} instance. * @param connectTimeout the connection timeout * @param readTimeout the read timeout - * @param bufferRequestBody the bugger request body - * @param sslBundle the ssl bundle - * @since 3.1.0 + * @param bufferRequestBody if request body buffering is used + * @deprecated since 3.2.0 for removal in 3.4.0 as support for buffering has been + * removed in Spring Framework 6.1 */ - public ClientHttpRequestFactorySettings { + @Deprecated(since = "3.2.0", forRemoval = true) + public ClientHttpRequestFactorySettings(Duration connectTimeout, Duration readTimeout, Boolean bufferRequestBody) { + this(connectTimeout, readTimeout, (SslBundle) null); } - public ClientHttpRequestFactorySettings(Duration connectTimeout, Duration readTimeout, Boolean bufferRequestBody) { - this(connectTimeout, readTimeout, bufferRequestBody, null); + /** + * Create a new {@link ClientHttpRequestFactorySettings} instance. + * @param connectTimeout the connection timeout + * @param readTimeout the read timeout + * @param bufferRequestBody if request body buffering is used + * @param sslBundle the ssl bundle + * @since 3.1.0 + * @deprecated since 3.2.0 for removal in 3.4.0 as support for buffering has been + * removed in Spring Framework 6.1 + */ + @Deprecated(since = "3.2.0", forRemoval = true) + public ClientHttpRequestFactorySettings(Duration connectTimeout, Duration readTimeout, Boolean bufferRequestBody, + SslBundle sslBundle) { + this(connectTimeout, readTimeout, sslBundle); } /** @@ -66,8 +78,7 @@ public ClientHttpRequestFactorySettings(Duration connectTimeout, Duration readTi * @return a new {@link ClientHttpRequestFactorySettings} instance */ public ClientHttpRequestFactorySettings withConnectTimeout(Duration connectTimeout) { - return new ClientHttpRequestFactorySettings(connectTimeout, this.readTimeout, this.bufferRequestBody, - this.sslBundle); + return new ClientHttpRequestFactorySettings(connectTimeout, this.readTimeout, this.sslBundle); } /** @@ -78,19 +89,19 @@ public ClientHttpRequestFactorySettings withConnectTimeout(Duration connectTimeo */ public ClientHttpRequestFactorySettings withReadTimeout(Duration readTimeout) { - return new ClientHttpRequestFactorySettings(this.connectTimeout, readTimeout, this.bufferRequestBody, - this.sslBundle); + return new ClientHttpRequestFactorySettings(this.connectTimeout, readTimeout, this.sslBundle); } /** - * Return a new {@link ClientHttpRequestFactorySettings} instance with an updated - * buffer request body setting. + * Has no effect as support for buffering has been removed in Spring Framework 6.1. * @param bufferRequestBody the new buffer request body setting * @return a new {@link ClientHttpRequestFactorySettings} instance + * @deprecated since 3.2.0 for removal in 3.4.0 as support for buffering has been + * removed in Spring Framework 6.1 */ + @Deprecated(since = "3.2.0", forRemoval = true) public ClientHttpRequestFactorySettings withBufferRequestBody(Boolean bufferRequestBody) { - return new ClientHttpRequestFactorySettings(this.connectTimeout, this.readTimeout, bufferRequestBody, - this.sslBundle); + return this; } /** @@ -101,8 +112,18 @@ public ClientHttpRequestFactorySettings withBufferRequestBody(Boolean bufferRequ * @since 3.1.0 */ public ClientHttpRequestFactorySettings withSslBundle(SslBundle sslBundle) { - return new ClientHttpRequestFactorySettings(this.connectTimeout, this.readTimeout, this.bufferRequestBody, - sslBundle); + return new ClientHttpRequestFactorySettings(this.connectTimeout, this.readTimeout, sslBundle); + } + + /** + * Returns whether request body buffering is used. + * @return whether request body buffering is used + * @deprecated since 3.2.0 for removal in 3.4.0 as support for buffering has been + * removed in Spring Framework 6.1 + */ + @Deprecated(since = "3.2.0", forRemoval = true) + public Boolean bufferRequestBody() { + return null; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilder.java index 9a981759cbf7..93e4e1b91840 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilder.java @@ -440,19 +440,18 @@ public RestTemplateBuilder setReadTimeout(Duration readTimeout) { } /** - * Sets if the underlying {@link ClientHttpRequestFactory} should buffer the - * {@linkplain ClientHttpRequest#getBody() request body} internally. + * Has no effect as support for buffering has been removed in Spring Framework 6.1. * @param bufferRequestBody value of the bufferRequestBody parameter * @return a new builder instance. * @since 2.2.0 + * @deprecated since 3.2.0 for removal in 3.4.0 as support for buffering has been + * removed in Spring Framework 6.1 * @see SimpleClientHttpRequestFactory#setBufferRequestBody(boolean) * @see HttpComponentsClientHttpRequestFactory#setBufferRequestBody(boolean) */ + @Deprecated(since = "3.2.0", forRemoval = true) public RestTemplateBuilder setBufferRequestBody(boolean bufferRequestBody) { - return new RestTemplateBuilder(this.requestFactorySettings.withBufferRequestBody(bufferRequestBody), - this.detectRequestFactory, this.rootUri, this.messageConverters, this.interceptors, this.requestFactory, - this.uriTemplateHandler, this.errorHandler, this.basicAuthentication, this.defaultHeaders, - this.customizers, this.requestCustomizers); + return this; } /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilder.java index 8f5dbb3d7c0e..5d25c93e601a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilder.java @@ -113,7 +113,7 @@ public WebServiceMessageSender build() { private ClientHttpRequestFactory getRequestFactory() { ClientHttpRequestFactorySettings settings = new ClientHttpRequestFactorySettings(this.connectTimeout, - this.readTimeout, null, this.sslBundle); + this.readTimeout, this.sslBundle); return (this.requestFactory != null) ? this.requestFactory.apply(settings) : ClientHttpRequestFactories.get(settings); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp3Tests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp3Tests.java index 59e4c3144474..b54e8050d2e5 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp3Tests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp3Tests.java @@ -27,7 +27,6 @@ import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link ClientHttpRequestFactories} when OkHttp 3 is the predominant HTTP @@ -50,12 +49,6 @@ void okHttp3IsBeingUsed() { .startsWith("okhttp-3."); } - @Test - void getFailsWhenBufferRequestBodyIsEnabled() { - assertThatIllegalStateException().isThrownBy(() -> ClientHttpRequestFactories - .get(ClientHttpRequestFactorySettings.DEFAULTS.withBufferRequestBody(true))); - } - @Override protected long connectTimeout(OkHttp3ClientHttpRequestFactory requestFactory) { return ((OkHttpClient) ReflectionTestUtils.getField(requestFactory, "client")).connectTimeoutMillis(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp4Tests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp4Tests.java index 13158708f54d..d1f533f73146 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp4Tests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp4Tests.java @@ -26,7 +26,6 @@ import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link ClientHttpRequestFactories} when OkHttp 4 is the predominant HTTP @@ -48,12 +47,6 @@ void okHttp4IsBeingUsed() { .startsWith("okhttp-4."); } - @Test - void getFailsWhenBufferRequestBodyIsEnabled() { - assertThatIllegalStateException().isThrownBy(() -> ClientHttpRequestFactories - .get(ClientHttpRequestFactorySettings.DEFAULTS.withBufferRequestBody(true))); - } - @Override protected long connectTimeout(OkHttp3ClientHttpRequestFactory requestFactory) { return ((OkHttpClient) ReflectionTestUtils.getField(requestFactory, "client")).connectTimeoutMillis(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHintsTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHintsTests.java index bb143d3297bb..bbfdafccf580 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHintsTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHintsTests.java @@ -59,12 +59,6 @@ void shouldRegisterHttpComponentHints() { assertThat(reflection .onMethod(method(HttpComponentsClientHttpRequestFactory.class, "setConnectTimeout", int.class))) .accepts(hints); - assertThat( - reflection.onMethod(method(HttpComponentsClientHttpRequestFactory.class, "setReadTimeout", int.class))) - .accepts(hints); - assertThat(reflection - .onMethod(method(HttpComponentsClientHttpRequestFactory.class, "setBufferRequestBody", boolean.class))) - .accepts(hints); } @Test @@ -88,9 +82,6 @@ void shouldRegisterSimpleHttpHints() { .accepts(hints); assertThat(reflection.onMethod(method(SimpleClientHttpRequestFactory.class, "setReadTimeout", int.class))) .accepts(hints); - assertThat(reflection - .onMethod(method(SimpleClientHttpRequestFactory.class, "setBufferRequestBody", boolean.class))) - .accepts(hints); } private static Method method(Class target, String name, Class... parameterTypes) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesTests.java index 34d591efb33d..546b862d987a 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesTests.java @@ -100,14 +100,6 @@ void getOfUnknownTypeWithReadTimeoutCreatesFactoryAndConfiguresReadTimeout() { .isEqualTo(Duration.ofSeconds(90).toMillis()); } - @Test - void getOfUnknownTypeWithBodyBufferingCreatesFactoryAndConfiguresBodyBuffering() { - ClientHttpRequestFactory requestFactory = ClientHttpRequestFactories.get(TestClientHttpRequestFactory.class, - ClientHttpRequestFactorySettings.DEFAULTS.withBufferRequestBody(true)); - assertThat(requestFactory).isInstanceOf(TestClientHttpRequestFactory.class); - assertThat(((TestClientHttpRequestFactory) requestFactory).bufferRequestBody).isTrue(); - } - @Test void getOfUnconfigurableTypeWithConnectTimeoutThrows() { assertThatIllegalStateException() @@ -124,14 +116,6 @@ void getOfUnconfigurableTypeWithReadTimeoutThrows() { .withMessageContaining("suitable setReadTimeout method"); } - @Test - void getOfUnconfigurableTypeWithBodyBufferingThrows() { - assertThatIllegalStateException() - .isThrownBy(() -> ClientHttpRequestFactories.get(UnconfigurableClientHttpRequestFactory.class, - ClientHttpRequestFactorySettings.DEFAULTS.withBufferRequestBody(true))) - .withMessageContaining("suitable setBufferRequestBody method"); - } - @Test void getOfTypeWithDeprecatedConnectTimeoutThrowsWithConnectTimeout() { assertThatIllegalStateException() @@ -148,14 +132,6 @@ void getOfTypeWithDeprecatedReadTimeoutThrowsWithReadTimeout() { .withMessageContaining("setReadTimeout method marked as deprecated"); } - @Test - void getOfTypeWithDeprecatedBufferRequestBodyThrowsWithBufferRequestBody() { - assertThatIllegalStateException() - .isThrownBy(() -> ClientHttpRequestFactories.get(DeprecatedMethodsClientHttpRequestFactory.class, - ClientHttpRequestFactorySettings.DEFAULTS.withBufferRequestBody(false))) - .withMessageContaining("setBufferRequestBody method marked as deprecated"); - } - @Test void connectTimeoutCanBeConfiguredOnAWrappedRequestFactory() { SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); @@ -176,25 +152,12 @@ void readTimeoutCanBeConfiguredOnAWrappedRequestFactory() { assertThat(requestFactory).hasFieldOrPropertyWithValue("readTimeout", 1234); } - @Test - void bufferRequestBodyCanBeConfiguredOnAWrappedRequestFactory() { - SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); - assertThat(requestFactory).hasFieldOrPropertyWithValue("bufferRequestBody", true); - BufferingClientHttpRequestFactory result = ClientHttpRequestFactories.get( - () -> new BufferingClientHttpRequestFactory(requestFactory), - ClientHttpRequestFactorySettings.DEFAULTS.withBufferRequestBody(false)); - assertThat(result).extracting("requestFactory").isSameAs(requestFactory); - assertThat(requestFactory).hasFieldOrPropertyWithValue("bufferRequestBody", false); - } - public static class TestClientHttpRequestFactory implements ClientHttpRequestFactory { private int connectTimeout; private int readTimeout; - private boolean bufferRequestBody; - @Override public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException { throw new UnsupportedOperationException(); @@ -208,10 +171,6 @@ public void setReadTimeout(int timeout) { this.readTimeout = timeout; } - public void setBufferRequestBody(boolean bufferRequestBody) { - this.bufferRequestBody = bufferRequestBody; - } - } public static class UnconfigurableClientHttpRequestFactory implements ClientHttpRequestFactory { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactorySettingsTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactorySettingsTests.java index 8103a3ae6973..c9145b7dd266 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactorySettingsTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactorySettingsTests.java @@ -39,7 +39,6 @@ void defaultsHasNullValues() { ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.DEFAULTS; assertThat(settings.connectTimeout()).isNull(); assertThat(settings.readTimeout()).isNull(); - assertThat(settings.bufferRequestBody()).isNull(); assertThat(settings.sslBundle()).isNull(); } @@ -49,7 +48,6 @@ void withConnectTimeoutReturnsInstanceWithUpdatedConnectionTimeout() { .withConnectTimeout(ONE_SECOND); assertThat(settings.connectTimeout()).isEqualTo(ONE_SECOND); assertThat(settings.readTimeout()).isNull(); - assertThat(settings.bufferRequestBody()).isNull(); assertThat(settings.sslBundle()).isNull(); } @@ -59,17 +57,6 @@ void withReadTimeoutReturnsInstanceWithUpdatedReadTimeout() { .withReadTimeout(ONE_SECOND); assertThat(settings.connectTimeout()).isNull(); assertThat(settings.readTimeout()).isEqualTo(ONE_SECOND); - assertThat(settings.bufferRequestBody()).isNull(); - assertThat(settings.sslBundle()).isNull(); - } - - @Test - void withBufferRequestBodyReturnsInstanceWithUpdatedBufferRequestBody() { - ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.DEFAULTS - .withBufferRequestBody(true); - assertThat(settings.connectTimeout()).isNull(); - assertThat(settings.readTimeout()).isNull(); - assertThat(settings.bufferRequestBody()).isTrue(); assertThat(settings.sslBundle()).isNull(); } @@ -79,7 +66,6 @@ void withSslBundleReturnsInstanceWithUpdatedSslBundle() { ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.DEFAULTS.withSslBundle(sslBundle); assertThat(settings.connectTimeout()).isNull(); assertThat(settings.readTimeout()).isNull(); - assertThat(settings.bufferRequestBody()).isNull(); assertThat(settings.sslBundle()).isSameAs(sslBundle); } diff --git a/spring-boot-system-tests/spring-boot-image-tests/build.gradle b/spring-boot-system-tests/spring-boot-image-tests/build.gradle index 9de3b9c9fbfe..e4236f1b82fc 100644 --- a/spring-boot-system-tests/spring-boot-image-tests/build.gradle +++ b/spring-boot-system-tests/spring-boot-image-tests/build.gradle @@ -19,6 +19,11 @@ configurations { if (dependency.requested.group.startsWith("com.fasterxml.jackson")) { dependency.useVersion("2.14.2") } + // Downgrade Spring Framework as Gradle cannot cope with 6.1.0-M1's + // multi-version jar files with bytecode in META-INF/versions/21 + if (dependency.requested.group.equals("org.springframework")) { + dependency.useVersion("6.0.10") + } } } } From 5cd18a05fc1ebf204a52c44ec8e9464809f11079 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:57:32 +0100 Subject: [PATCH 0055/1215] Upgrade to SQLite JDBC 3.42.0.0 Closes gh-35978 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c2af53dad5fa..f32eba420cf9 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1500,7 +1500,7 @@ bom { ] } } - library("SQLite JDBC", "3.41.2.2") { + library("SQLite JDBC", "3.42.0.0") { group("org.xerial") { modules = [ "sqlite-jdbc" From 962445ea6c244b9a241a1a60662baead19888232 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 09:57:37 +0100 Subject: [PATCH 0056/1215] Upgrade to Versions Maven Plugin 2.16.0 Closes gh-35979 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f32eba420cf9..775aeaa90234 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1576,7 +1576,7 @@ bom { ] } } - library("Versions Maven Plugin", "2.15.0") { + library("Versions Maven Plugin", "2.16.0") { group("org.codehaus.mojo") { plugins = [ "versions-maven-plugin" From a94ac2fb4465d46db3dbb4e4014dd04c04738d52 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 12:16:44 +0100 Subject: [PATCH 0057/1215] Upgrade to Rabbit AMQP Client 5.18.0 Closes gh-35981 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 775aeaa90234..133b97fcad5e 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1181,7 +1181,7 @@ bom { ] } } - library("Rabbit AMQP Client", "5.17.0") { + library("Rabbit AMQP Client", "5.18.0") { group("com.rabbitmq") { modules = [ "amqp-client" From 1f9ce508f7f996bcba8d7707a06ab0210ec5089a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 12:16:49 +0100 Subject: [PATCH 0058/1215] Upgrade to SnakeYAML 2.0 Closes gh-35982 --- .../spring-boot-dependencies/build.gradle | 2 +- .../spring-boot-docs/build.gradle | 11 +++++++ .../env/OriginTrackedYamlLoaderTests.java | 4 +-- ...lPropertySourceLoaderSnakeYaml20Tests.java | 29 ------------------- 4 files changed, 14 insertions(+), 32 deletions(-) delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/YamlPropertySourceLoaderSnakeYaml20Tests.java diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 133b97fcad5e..994d576ea33a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1375,7 +1375,7 @@ bom { ] } } - library("SnakeYAML", "1.33") { + library("SnakeYAML", "2.0") { group("org.yaml") { modules = [ "snakeyaml" diff --git a/spring-boot-project/spring-boot-docs/build.gradle b/spring-boot-project/spring-boot-docs/build.gradle index 583522f59050..3d0d6ddcd8a5 100644 --- a/spring-boot-project/spring-boot-docs/build.gradle +++ b/spring-boot-project/spring-boot-docs/build.gradle @@ -17,6 +17,17 @@ configurations { remoteSpringApplicationExample springApplicationExample testSlices + asciidoctorExtensions { + resolutionStrategy { + eachDependency { dependency -> + // Downgrade SnakeYAML as Asciidoctor fails due to an incompatibility + // in the Pysch gem + if (dependency.requested.group.equals("org.yaml")) { + dependency.useVersion("1.33") + } + } + } + } } jar { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedYamlLoaderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedYamlLoaderTests.java index b51d41a9cdfe..ce5fa86434b1 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedYamlLoaderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/OriginTrackedYamlLoaderTests.java @@ -23,7 +23,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.yaml.snakeyaml.constructor.ConstructorException; +import org.yaml.snakeyaml.composer.ComposerException; import org.springframework.boot.origin.OriginTrackedValue; import org.springframework.boot.origin.TextResourceOrigin; @@ -134,7 +134,7 @@ void unsupportedType() { String yaml = "value: !!java.net.URL [!!java.lang.String [!!java.lang.StringBuilder [\"http://localhost:9000/\"]]]"; Resource resource = new ByteArrayResource(yaml.getBytes(StandardCharsets.UTF_8)); this.loader = new OriginTrackedYamlLoader(resource); - assertThatExceptionOfType(ConstructorException.class).isThrownBy(this.loader::load); + assertThatExceptionOfType(ComposerException.class).isThrownBy(this.loader::load); } @Test diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/YamlPropertySourceLoaderSnakeYaml20Tests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/YamlPropertySourceLoaderSnakeYaml20Tests.java deleted file mode 100644 index 684440176523..000000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/YamlPropertySourceLoaderSnakeYaml20Tests.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2012-2023 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.env; - -import org.springframework.boot.testsupport.classpath.ClassPathOverrides; - -/** - * Tests for {@link YamlPropertySourceLoader} with SnakeYAML 2.0. - * - * @author Andy Wilkinson - */ -@ClassPathOverrides("org.yaml:snakeyaml:2.0") -class YamlPropertySourceLoaderSnakeYaml20Tests extends YamlPropertySourceLoaderTests { - -} From d9aac063a2cc764c1d6e569c02dc2c7efebc8353 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 19 Jun 2023 15:30:02 +0100 Subject: [PATCH 0059/1215] Prohibit upgrades to Oracle Database 23.2.0.0 Closes gh-35970 --- spring-boot-project/spring-boot-dependencies/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 994d576ea33a..e5569d72ef91 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1073,6 +1073,9 @@ bom { } } library("Oracle Database", "21.9.0.0") { + prohibit { + versionRange "23.2.0.0" + } group("com.oracle.database.jdbc") { imports = [ "ojdbc-bom" From b6120d504a1fffbbfd7a39b912b57b3407af5824 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 21 Jun 2023 19:33:12 -0700 Subject: [PATCH 0060/1215] Replace LoggingSystemProperties constants with an Enum Extract contants from `LoggingSystemProperty` and `LogbackLoggingSystemProperties` in enum classes. Closes gh-36015 --- .../springframework/boot/logging/LogFile.java | 10 +- .../boot/logging/LoggingSystemProperties.java | 146 ++++++++++++++---- .../boot/logging/LoggingSystemProperty.java | 113 ++++++++++++++ .../boot/logging/java/SimpleFormatter.java | 6 +- .../LogbackLoggingSystemProperties.java | 70 +++++---- .../logback/RollingPolicySystemProperty.java | 82 ++++++++++ ...ngApplicationListenerIntegrationTests.java | 4 +- .../LoggingApplicationListenerTests.java | 27 ++-- .../logging/AbstractLoggingSystemTests.java | 7 +- .../boot/logging/LogFileTests.java | 16 +- .../logging/LoggingSystemPropertiesTests.java | 27 ++-- .../logging/java/JavaLoggingSystemTests.java | 4 +- .../log4j2/Log4J2LoggingSystemTests.java | 6 +- .../logging/log4j2/Log4j2FileXmlTests.java | 17 +- .../boot/logging/log4j2/Log4j2XmlTests.java | 8 +- .../LogbackLoggingSystemPropertiesTests.java | 33 ++-- .../logback/LogbackLoggingSystemTests.java | 13 +- 17 files changed, 457 insertions(+), 132 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperty.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/RollingPolicySystemProperty.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LogFile.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LogFile.java index b973bc90019d..a1a201202e72 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LogFile.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LogFile.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. @@ -86,13 +86,13 @@ public void applyToSystemProperties() { * @param properties the properties to apply to */ public void applyTo(Properties properties) { - put(properties, LoggingSystemProperties.LOG_PATH, this.path); - put(properties, LoggingSystemProperties.LOG_FILE, toString()); + put(properties, LoggingSystemProperty.LOG_PATH, this.path); + put(properties, LoggingSystemProperty.LOG_FILE, toString()); } - private void put(Properties properties, String key, String value) { + private void put(Properties properties, LoggingSystemProperty property, String value) { if (StringUtils.hasLength(value)) { - properties.put(key, value); + properties.put(property.getEnvironmentVariableName(), value); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperties.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperties.java index ff4c6c7eebe1..456bb010d075 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperties.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperties.java @@ -37,68 +37,120 @@ * @author Robert Thornton * @author Eddú Meléndez * @since 2.0.0 + * @see LoggingSystemProperty */ public class LoggingSystemProperties { /** * The name of the System property that contains the process ID. + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of calling + * {@link LoggingSystemProperty#getEnvironmentVariableName()} on + * {@link LoggingSystemProperty#PID} */ - public static final String PID_KEY = "PID"; + @Deprecated(since = "3.2.0", forRemoval = true) + public static final String PID_KEY = LoggingSystemProperty.PID.getEnvironmentVariableName(); /** * The name of the System property that contains the exception conversion word. + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of calling + * {@link LoggingSystemProperty#getEnvironmentVariableName()} on + * {@link LoggingSystemProperty#EXCEPTION_CONVERSION_WORD} */ - public static final String EXCEPTION_CONVERSION_WORD = "LOG_EXCEPTION_CONVERSION_WORD"; + @Deprecated(since = "3.2.0", forRemoval = true) + public static final String EXCEPTION_CONVERSION_WORD = LoggingSystemProperty.EXCEPTION_CONVERSION_WORD + .getEnvironmentVariableName(); /** * The name of the System property that contains the log file. + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of calling + * {@link LoggingSystemProperty#getEnvironmentVariableName()} on + * {@link LoggingSystemProperty#LOG_FILE} */ - public static final String LOG_FILE = "LOG_FILE"; + @Deprecated(since = "3.2.0", forRemoval = true) + public static final String LOG_FILE = LoggingSystemProperty.LOG_FILE.getEnvironmentVariableName(); /** * The name of the System property that contains the log path. + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of calling + * {@link LoggingSystemProperty#getEnvironmentVariableName()} on + * {@link LoggingSystemProperty#LOG_PATH} */ - public static final String LOG_PATH = "LOG_PATH"; + @Deprecated(since = "3.2.0", forRemoval = true) + public static final String LOG_PATH = LoggingSystemProperty.LOG_PATH.getEnvironmentVariableName(); /** * The name of the System property that contains the console log pattern. + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of calling + * {@link LoggingSystemProperty#getEnvironmentVariableName()} on + * {@link LoggingSystemProperty#CONSOLE_PATTERN} */ - public static final String CONSOLE_LOG_PATTERN = "CONSOLE_LOG_PATTERN"; + @Deprecated(since = "3.2.0", forRemoval = true) + public static final String CONSOLE_LOG_PATTERN = LoggingSystemProperty.CONSOLE_PATTERN.getEnvironmentVariableName(); /** * The name of the System property that contains the console log charset. + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of calling + * {@link LoggingSystemProperty#getEnvironmentVariableName()} on + * {@link LoggingSystemProperty#CONSOLE_CHARSET} */ - public static final String CONSOLE_LOG_CHARSET = "CONSOLE_LOG_CHARSET"; + @Deprecated(since = "3.2.0", forRemoval = true) + public static final String CONSOLE_LOG_CHARSET = LoggingSystemProperty.CONSOLE_CHARSET.getEnvironmentVariableName(); /** * The log level threshold for console log. + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of calling + * {@link LoggingSystemProperty#getEnvironmentVariableName()} on + * {@link LoggingSystemProperty#CONSOLE_THRESHOLD} */ - public static final String CONSOLE_LOG_THRESHOLD = "CONSOLE_LOG_THRESHOLD"; + @Deprecated(since = "3.2.0", forRemoval = true) + public static final String CONSOLE_LOG_THRESHOLD = LoggingSystemProperty.CONSOLE_THRESHOLD + .getEnvironmentVariableName(); /** * The name of the System property that contains the file log pattern. + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of calling + * {@link LoggingSystemProperty#getEnvironmentVariableName()} on + * {@link LoggingSystemProperty#FILE_PATTERN} */ - public static final String FILE_LOG_PATTERN = "FILE_LOG_PATTERN"; + @Deprecated(since = "3.2.0", forRemoval = true) + public static final String FILE_LOG_PATTERN = LoggingSystemProperty.FILE_PATTERN.getEnvironmentVariableName(); /** * The name of the System property that contains the file log charset. + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of calling + * {@link LoggingSystemProperty#getEnvironmentVariableName()} on + * {@link LoggingSystemProperty#FILE_CHARSET} */ - public static final String FILE_LOG_CHARSET = "FILE_LOG_CHARSET"; + @Deprecated(since = "3.2.0", forRemoval = true) + public static final String FILE_LOG_CHARSET = LoggingSystemProperty.FILE_CHARSET.getEnvironmentVariableName(); /** * The log level threshold for file log. + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of calling + * {@link LoggingSystemProperty#getEnvironmentVariableName()} on + * {@link LoggingSystemProperty#FILE_THRESHOLD} */ - public static final String FILE_LOG_THRESHOLD = "FILE_LOG_THRESHOLD"; + @Deprecated(since = "3.2.0", forRemoval = true) + public static final String FILE_LOG_THRESHOLD = LoggingSystemProperty.FILE_THRESHOLD.getEnvironmentVariableName(); /** * The name of the System property that contains the log level pattern. + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of calling + * {@link LoggingSystemProperty#getEnvironmentVariableName()} on + * {@link LoggingSystemProperty#LEVEL_PATTERN} */ - public static final String LOG_LEVEL_PATTERN = "LOG_LEVEL_PATTERN"; + @Deprecated(since = "3.2.0", forRemoval = true) + public static final String LOG_LEVEL_PATTERN = LoggingSystemProperty.LEVEL_PATTERN.getEnvironmentVariableName(); /** * The name of the System property that contains the log date-format pattern. + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of calling + * {@link LoggingSystemProperty#getEnvironmentVariableName()} on + * {@link LoggingSystemProperty#DATEFORMAT_PATTERN} */ - public static final String LOG_DATEFORMAT_PATTERN = "LOG_DATEFORMAT_PATTERN"; + @Deprecated(since = "3.2.0", forRemoval = true) + public static final String LOG_DATEFORMAT_PATTERN = LoggingSystemProperty.DATEFORMAT_PATTERN + .getEnvironmentVariableName(); private static final BiConsumer systemPropertySetter = (name, value) -> { if (System.getProperty(name) == null && value != null) { @@ -144,22 +196,6 @@ public final void apply(LogFile logFile) { apply(logFile, resolver); } - protected void apply(LogFile logFile, PropertyResolver resolver) { - setSystemProperty(resolver, EXCEPTION_CONVERSION_WORD, "logging.exception-conversion-word"); - setSystemProperty(PID_KEY, new ApplicationPid().toString()); - setSystemProperty(resolver, CONSOLE_LOG_PATTERN, "logging.pattern.console"); - setSystemProperty(resolver, CONSOLE_LOG_CHARSET, "logging.charset.console", getDefaultCharset().name()); - setSystemProperty(resolver, CONSOLE_LOG_THRESHOLD, "logging.threshold.console"); - setSystemProperty(resolver, LOG_DATEFORMAT_PATTERN, "logging.pattern.dateformat"); - setSystemProperty(resolver, FILE_LOG_PATTERN, "logging.pattern.file"); - setSystemProperty(resolver, FILE_LOG_CHARSET, "logging.charset.file", getDefaultCharset().name()); - setSystemProperty(resolver, FILE_LOG_THRESHOLD, "logging.threshold.file"); - setSystemProperty(resolver, LOG_LEVEL_PATTERN, "logging.pattern.level"); - if (logFile != null) { - logFile.applyToSystemProperties(); - } - } - private PropertyResolver getPropertyResolver() { if (this.environment instanceof ConfigurableEnvironment configurableEnvironment) { PropertySourcesPropertyResolver resolver = new PropertySourcesPropertyResolver( @@ -171,10 +207,59 @@ private PropertyResolver getPropertyResolver() { return this.environment; } + protected void apply(LogFile logFile, PropertyResolver resolver) { + String defaultCharsetName = getDefaultCharset().name(); + setSystemProperty(LoggingSystemProperty.PID, new ApplicationPid().toString()); + setSystemProperty(LoggingSystemProperty.CONSOLE_CHARSET, resolver, defaultCharsetName); + setSystemProperty(LoggingSystemProperty.FILE_CHARSET, resolver, defaultCharsetName); + setSystemProperty(LoggingSystemProperty.CONSOLE_THRESHOLD, resolver); + setSystemProperty(LoggingSystemProperty.FILE_THRESHOLD, resolver); + setSystemProperty(LoggingSystemProperty.EXCEPTION_CONVERSION_WORD, resolver); + setSystemProperty(LoggingSystemProperty.CONSOLE_PATTERN, resolver); + setSystemProperty(LoggingSystemProperty.FILE_PATTERN, resolver); + setSystemProperty(LoggingSystemProperty.LEVEL_PATTERN, resolver); + setSystemProperty(LoggingSystemProperty.DATEFORMAT_PATTERN, resolver); + if (logFile != null) { + logFile.applyToSystemProperties(); + } + } + + private void setSystemProperty(LoggingSystemProperty property, PropertyResolver resolver) { + setSystemProperty(property, resolver, null); + } + + private void setSystemProperty(LoggingSystemProperty property, PropertyResolver resolver, String defaultValue) { + String value = (property.getApplicationPropertyName() != null) + ? resolver.getProperty(property.getApplicationPropertyName()) : null; + value = (value != null) ? value : defaultValue; + setSystemProperty(property.getEnvironmentVariableName(), value); + } + + private void setSystemProperty(LoggingSystemProperty property, String value) { + setSystemProperty(property.getEnvironmentVariableName(), value); + } + + /** + * Set a system property. + * @param resolver the resolver used to get the property value + * @param systemPropertyName the system property name + * @param propertyName the application property name + * @deprecated since 3.2.0 for removal in 3.4.0 with no replacement + */ + @Deprecated(since = "3.2.0", forRemoval = true) protected final void setSystemProperty(PropertyResolver resolver, String systemPropertyName, String propertyName) { setSystemProperty(resolver, systemPropertyName, propertyName, null); } + /** + * Set a system property. + * @param resolver the resolver used to get the property value + * @param systemPropertyName the system property name + * @param propertyName the application property name + * @param defaultValue the default value if none can be resolved + * @deprecated since 3.2.0 for removal in 3.4.0 with no replacement + */ + @Deprecated(since = "3.2.0", forRemoval = true) protected final void setSystemProperty(PropertyResolver resolver, String systemPropertyName, String propertyName, String defaultValue) { String value = resolver.getProperty(propertyName); @@ -182,6 +267,11 @@ protected final void setSystemProperty(PropertyResolver resolver, String systemP setSystemProperty(systemPropertyName, value); } + /** + * Set a system property. + * @param name the property name + * @param value the value + */ protected final void setSystemProperty(String name, String value) { this.setter.accept(name, value); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperty.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperty.java new file mode 100644 index 000000000000..d6a731453189 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperty.java @@ -0,0 +1,113 @@ +/* + * Copyright 2012-2023 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.logging; + +/** + * Logging system properties that can later be used by log configuration files. + * + * @author Phillip Webb + * @since 3.2.0 + * @see LoggingSystemProperties + */ +public enum LoggingSystemProperty { + + /** + * Logging system property for the process ID. + */ + PID("PID"), + + /** + * Logging system property for the log file. + */ + LOG_FILE("LOG_FILE"), + + /** + * Logging system property for the log path. + */ + LOG_PATH("LOG_PATH"), + + /** + * Logging system property for the console log charset. + */ + CONSOLE_CHARSET("CONSOLE_LOG_CHARSET", "logging.charset.console"), + + /** + * Logging system property for the file log charset. + */ + FILE_CHARSET("FILE_LOG_CHARSET", "logging.charset.file"), + + /** + * Logging system property for the console log. + */ + CONSOLE_THRESHOLD("CONSOLE_LOG_THRESHOLD", "logging.threshold.console"), + + /** + * Logging system property for the file log. + */ + FILE_THRESHOLD("FILE_LOG_THRESHOLD", "logging.threshold.file"), + + /** + * Logging system property for the exception conversion word. + */ + EXCEPTION_CONVERSION_WORD("LOG_EXCEPTION_CONVERSION_WORD", "logging.exception-conversion-word"), + + /** + * Logging system property for the console log pattern. + */ + CONSOLE_PATTERN("CONSOLE_LOG_PATTERN", "logging.pattern.console"), + + /** + * Logging system property for the file log pattern. + */ + FILE_PATTERN("FILE_LOG_PATTERN", "logging.pattern.file"), + + /** + * Logging system property for the log level pattern. + */ + LEVEL_PATTERN("LOG_LEVEL_PATTERN", "logging.pattern.level"), + + /** + * Logging system property for the date-format pattern. + */ + DATEFORMAT_PATTERN("LOG_DATEFORMAT_PATTERN", "logging.pattern.dateformat"); + + private final String environmentVariableName; + + private final String applicationPropertyName; + + LoggingSystemProperty(String environmentVariableName) { + this(environmentVariableName, null); + } + + LoggingSystemProperty(String environmentVariableName, String applicationPropertyName) { + this.environmentVariableName = environmentVariableName; + this.applicationPropertyName = applicationPropertyName; + } + + /** + * Return the name of environment variable that can be used to access this property. + * @return the environment variable name + */ + public String getEnvironmentVariableName() { + return this.environmentVariableName; + } + + String getApplicationPropertyName() { + return this.applicationPropertyName; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/java/SimpleFormatter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/java/SimpleFormatter.java index f92e5eae0edb..1701a6a03a23 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/java/SimpleFormatter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/java/SimpleFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. @@ -22,7 +22,7 @@ import java.util.logging.Formatter; import java.util.logging.LogRecord; -import org.springframework.boot.logging.LoggingSystemProperties; +import org.springframework.boot.logging.LoggingSystemProperty; /** * Simple 'Java Logging' {@link Formatter}. @@ -36,7 +36,7 @@ public class SimpleFormatter extends Formatter { private final String format = getOrUseDefault("LOG_FORMAT", DEFAULT_FORMAT); - private final String pid = getOrUseDefault(LoggingSystemProperties.PID_KEY, "????"); + private final String pid = getOrUseDefault(LoggingSystemProperty.PID.getEnvironmentVariableName(), "????"); private final Date date = new Date(); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystemProperties.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystemProperties.java index f9d97937d757..3b7b715e1615 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystemProperties.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystemProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -35,6 +35,7 @@ * * @author Phillip Webb * @since 2.4.0 + * @see RollingPolicySystemProperty */ public class LogbackLoggingSystemProperties extends LoggingSystemProperties { @@ -44,28 +45,53 @@ public class LogbackLoggingSystemProperties extends LoggingSystemProperties { /** * The name of the System property that contains the rolled-over log file name * pattern. + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of calling + * {@link RollingPolicySystemProperty#getEnvironmentVariableName()} on + * {@link RollingPolicySystemProperty#FILE_NAME_PATTERN} */ - public static final String ROLLINGPOLICY_FILE_NAME_PATTERN = "LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN"; + @Deprecated(since = "3.2.0", forRemoval = true) + public static final String ROLLINGPOLICY_FILE_NAME_PATTERN = RollingPolicySystemProperty.FILE_NAME_PATTERN + .getEnvironmentVariableName(); /** * The name of the System property that contains the clean history on start flag. + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of calling + * {@link RollingPolicySystemProperty#getEnvironmentVariableName()} on + * {@link RollingPolicySystemProperty#CLEAN_HISTORY_ON_START} */ - public static final String ROLLINGPOLICY_CLEAN_HISTORY_ON_START = "LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START"; + @Deprecated(since = "3.2.0", forRemoval = true) + public static final String ROLLINGPOLICY_CLEAN_HISTORY_ON_START = RollingPolicySystemProperty.CLEAN_HISTORY_ON_START + .getEnvironmentVariableName(); /** * The name of the System property that contains the file log max size. + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of calling + * {@link RollingPolicySystemProperty#getEnvironmentVariableName()} on + * {@link RollingPolicySystemProperty#MAX_FILE_SIZE} */ - public static final String ROLLINGPOLICY_MAX_FILE_SIZE = "LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE"; + @Deprecated(since = "3.2.0", forRemoval = true) + public static final String ROLLINGPOLICY_MAX_FILE_SIZE = RollingPolicySystemProperty.MAX_FILE_SIZE + .getEnvironmentVariableName(); /** * The name of the System property that contains the file total size cap. + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of calling + * {@link RollingPolicySystemProperty#getEnvironmentVariableName()} on + * {@link RollingPolicySystemProperty#TOTAL_SIZE_CAP} */ - public static final String ROLLINGPOLICY_TOTAL_SIZE_CAP = "LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP"; + @Deprecated(since = "3.2.0", forRemoval = true) + public static final String ROLLINGPOLICY_TOTAL_SIZE_CAP = RollingPolicySystemProperty.TOTAL_SIZE_CAP + .getEnvironmentVariableName(); /** * The name of the System property that contains the file log max history. + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of calling + * {@link RollingPolicySystemProperty#getEnvironmentVariableName()} on + * {@link RollingPolicySystemProperty#MAX_HISTORY} */ - public static final String ROLLINGPOLICY_MAX_HISTORY = "LOGBACK_ROLLINGPOLICY_MAX_HISTORY"; + @Deprecated(since = "3.2.0", forRemoval = true) + public static final String ROLLINGPOLICY_MAX_HISTORY = RollingPolicySystemProperty.MAX_HISTORY + .getEnvironmentVariableName(); public LogbackLoggingSystemProperties(Environment environment) { super(environment); @@ -100,32 +126,24 @@ private void applyJBossLoggingProperties() { } private void applyRollingPolicyProperties(PropertyResolver resolver) { - applyRollingPolicy(resolver, ROLLINGPOLICY_FILE_NAME_PATTERN, "logging.logback.rollingpolicy.file-name-pattern", - "logging.pattern.rolling-file-name"); - applyRollingPolicy(resolver, ROLLINGPOLICY_CLEAN_HISTORY_ON_START, - "logging.logback.rollingpolicy.clean-history-on-start", "logging.file.clean-history-on-start"); - applyRollingPolicy(resolver, ROLLINGPOLICY_MAX_FILE_SIZE, "logging.logback.rollingpolicy.max-file-size", - "logging.file.max-size", DataSize.class); - applyRollingPolicy(resolver, ROLLINGPOLICY_TOTAL_SIZE_CAP, "logging.logback.rollingpolicy.total-size-cap", - "logging.file.total-size-cap", DataSize.class); - applyRollingPolicy(resolver, ROLLINGPOLICY_MAX_HISTORY, "logging.logback.rollingpolicy.max-history", - "logging.file.max-history"); + applyRollingPolicy(RollingPolicySystemProperty.FILE_NAME_PATTERN, resolver); + applyRollingPolicy(RollingPolicySystemProperty.CLEAN_HISTORY_ON_START, resolver); + applyRollingPolicy(RollingPolicySystemProperty.MAX_FILE_SIZE, resolver, DataSize.class); + applyRollingPolicy(RollingPolicySystemProperty.TOTAL_SIZE_CAP, resolver, DataSize.class); + applyRollingPolicy(RollingPolicySystemProperty.MAX_HISTORY, resolver); } - private void applyRollingPolicy(PropertyResolver resolver, String systemPropertyName, String propertyName, - String deprecatedPropertyName) { - applyRollingPolicy(resolver, systemPropertyName, propertyName, deprecatedPropertyName, String.class); + private void applyRollingPolicy(RollingPolicySystemProperty property, PropertyResolver resolver) { + applyRollingPolicy(property, resolver, String.class); } - private void applyRollingPolicy(PropertyResolver resolver, String systemPropertyName, String propertyName, - String deprecatedPropertyName, Class type) { - T value = getProperty(resolver, propertyName, type); - if (value == null) { - value = getProperty(resolver, deprecatedPropertyName, type); - } + private void applyRollingPolicy(RollingPolicySystemProperty property, PropertyResolver resolver, + Class type) { + T value = getProperty(resolver, property.getApplicationPropertyName(), type); + value = (value != null) ? value : getProperty(resolver, property.getDeprecatedApplicationPropertyName(), type); if (value != null) { String stringValue = String.valueOf((value instanceof DataSize dataSize) ? dataSize.toBytes() : value); - setSystemProperty(systemPropertyName, stringValue); + setSystemProperty(property.getEnvironmentVariableName(), stringValue); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/RollingPolicySystemProperty.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/RollingPolicySystemProperty.java new file mode 100644 index 000000000000..f75db8f2386f --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/RollingPolicySystemProperty.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2023 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.logging.logback; + +/** + * Logback rolling policy system properties that can later be used by log configuration + * files. + * + * @author Phillip Webb + * @since 3.2.0 + * @see LogbackLoggingSystemProperties + */ +public enum RollingPolicySystemProperty { + + /** + * Logging system property for the rolled-over log file name pattern. + */ + FILE_NAME_PATTERN("file-name-pattern", "logging.pattern.rolling-file-name"), + + /** + * Logging system property for the clean history on start flag. + */ + CLEAN_HISTORY_ON_START("clean-history-on-start", "logging.file.clean-history-on-start"), + + /** + * Logging system property for the file log max size. + */ + MAX_FILE_SIZE("max-file-size", "logging.file.max-size"), + + /** + * Logging system property for the file total size cap. + */ + TOTAL_SIZE_CAP("total-size-cap", "logging.file.total-size-cap"), + + /** + * Logging system property for the file log max history. + */ + MAX_HISTORY("max-history", "logging.file.max-history"); + + private final String environmentVariableName; + + private final String applicationPropertyName; + + private final String deprecatedApplicationPropertyName; + + RollingPolicySystemProperty(String applicationPropertyName, String deprecatedApplicationPropertyName) { + this.environmentVariableName = "LOGBACK_ROLLINGPOLICY_" + name(); + this.applicationPropertyName = "logging.logback.rollingpolicy." + applicationPropertyName; + this.deprecatedApplicationPropertyName = deprecatedApplicationPropertyName; + } + + /** + * Return the name of environment variable that can be used to access this property. + * @return the environment variable name + */ + public String getEnvironmentVariableName() { + return this.environmentVariableName; + } + + String getApplicationPropertyName() { + return this.applicationPropertyName; + } + + String getDeprecatedApplicationPropertyName() { + return this.deprecatedApplicationPropertyName; + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/logging/LoggingApplicationListenerIntegrationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/logging/LoggingApplicationListenerIntegrationTests.java index 398bc8298296..d7c8b12a4901 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/logging/LoggingApplicationListenerIntegrationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/logging/LoggingApplicationListenerIntegrationTests.java @@ -30,7 +30,7 @@ import org.springframework.boot.context.event.ApplicationStartingEvent; import org.springframework.boot.logging.LogFile; import org.springframework.boot.logging.LoggingSystem; -import org.springframework.boot.logging.LoggingSystemProperties; +import org.springframework.boot.logging.LoggingSystemProperty; import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.testsupport.system.OutputCaptureExtension; import org.springframework.context.ApplicationListener; @@ -69,7 +69,7 @@ void logFileRegisteredInTheContextWhenApplicable(@TempDir File tempDir) { assertThat(service.logFile).hasToString(logFile); } finally { - System.clearProperty(LoggingSystemProperties.LOG_FILE); + System.clearProperty(LoggingSystemProperty.LOG_FILE.getEnvironmentVariableName()); } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/logging/LoggingApplicationListenerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/logging/LoggingApplicationListenerTests.java index d08311428194..4bfeac363066 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/logging/LoggingApplicationListenerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/logging/LoggingApplicationListenerTests.java @@ -58,7 +58,7 @@ import org.springframework.boot.logging.LoggerGroups; import org.springframework.boot.logging.LoggingInitializationContext; import org.springframework.boot.logging.LoggingSystem; -import org.springframework.boot.logging.LoggingSystemProperties; +import org.springframework.boot.logging.LoggingSystemProperty; import org.springframework.boot.logging.java.JavaLoggingSystem; import org.springframework.boot.system.ApplicationPid; import org.springframework.boot.testsupport.classpath.ClassPathExclusions; @@ -470,13 +470,13 @@ void systemPropertiesAreSetForLoggingConfiguration() { "logging.pattern.file=file", "logging.pattern.level=level", "logging.pattern.rolling-file-name=my.log.%d{yyyyMMdd}.%i.gz"); this.listener.initialize(this.context.getEnvironment(), this.context.getClassLoader()); - assertThat(System.getProperty(LoggingSystemProperties.CONSOLE_LOG_PATTERN)).isEqualTo("console"); - assertThat(System.getProperty(LoggingSystemProperties.FILE_LOG_PATTERN)).isEqualTo("file"); - assertThat(System.getProperty(LoggingSystemProperties.EXCEPTION_CONVERSION_WORD)).isEqualTo("conversion"); - assertThat(System.getProperty(LoggingSystemProperties.LOG_FILE)).isEqualTo(this.logFile.getAbsolutePath()); - assertThat(System.getProperty(LoggingSystemProperties.LOG_LEVEL_PATTERN)).isEqualTo("level"); - assertThat(System.getProperty(LoggingSystemProperties.LOG_PATH)).isEqualTo("path"); - assertThat(System.getProperty(LoggingSystemProperties.PID_KEY)).isNotNull(); + assertThat(getSystemProperty(LoggingSystemProperty.CONSOLE_PATTERN)).isEqualTo("console"); + assertThat(getSystemProperty(LoggingSystemProperty.FILE_PATTERN)).isEqualTo("file"); + assertThat(getSystemProperty(LoggingSystemProperty.EXCEPTION_CONVERSION_WORD)).isEqualTo("conversion"); + assertThat(getSystemProperty(LoggingSystemProperty.LOG_FILE)).isEqualTo(this.logFile.getAbsolutePath()); + assertThat(getSystemProperty(LoggingSystemProperty.LEVEL_PATTERN)).isEqualTo("level"); + assertThat(getSystemProperty(LoggingSystemProperty.LOG_PATH)).isEqualTo("path"); + assertThat(getSystemProperty(LoggingSystemProperty.PID)).isNotNull(); } @Test @@ -484,15 +484,14 @@ void environmentPropertiesIgnoreUnresolvablePlaceholders() { // gh-7719 addPropertiesToEnvironment(this.context, "logging.pattern.console=console ${doesnotexist}"); this.listener.initialize(this.context.getEnvironment(), this.context.getClassLoader()); - assertThat(System.getProperty(LoggingSystemProperties.CONSOLE_LOG_PATTERN)) - .isEqualTo("console ${doesnotexist}"); + assertThat(getSystemProperty(LoggingSystemProperty.CONSOLE_PATTERN)).isEqualTo("console ${doesnotexist}"); } @Test void environmentPropertiesResolvePlaceholders() { addPropertiesToEnvironment(this.context, "logging.pattern.console=console ${pid}"); this.listener.initialize(this.context.getEnvironment(), this.context.getClassLoader()); - assertThat(System.getProperty(LoggingSystemProperties.CONSOLE_LOG_PATTERN)) + assertThat(getSystemProperty(LoggingSystemProperty.CONSOLE_PATTERN)) .isEqualTo(this.context.getEnvironment().getProperty("logging.pattern.console")); } @@ -500,7 +499,7 @@ void environmentPropertiesResolvePlaceholders() { void logFilePropertiesCanReferenceSystemProperties() { addPropertiesToEnvironment(this.context, "logging.file.name=" + this.tempDir + "${PID}.log"); this.listener.initialize(this.context.getEnvironment(), this.context.getClassLoader()); - assertThat(System.getProperty(LoggingSystemProperties.LOG_FILE)) + assertThat(getSystemProperty(LoggingSystemProperty.LOG_FILE)) .isEqualTo(this.tempDir + new ApplicationPid().toString() + ".log"); } @@ -575,6 +574,10 @@ void loggingGroupsCanBeDefined() { assertTraceEnabled("com.foo.baz", true); } + private String getSystemProperty(LoggingSystemProperty property) { + return System.getProperty(property.getEnvironmentVariableName()); + } + private void assertTraceEnabled(String name, boolean expected) { assertThat(this.loggerContext.getLogger(name).isTraceEnabled()).isEqualTo(expected); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/AbstractLoggingSystemTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/AbstractLoggingSystemTests.java index 3ccc16255886..c80bf5654b57 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/AbstractLoggingSystemTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/AbstractLoggingSystemTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. @@ -50,8 +50,9 @@ void reinstateTempDir() { @AfterEach void clear() { - System.clearProperty(LoggingSystemProperties.LOG_FILE); - System.clearProperty(LoggingSystemProperties.PID_KEY); + for (LoggingSystemProperty property : LoggingSystemProperty.values()) { + System.getProperties().remove(property.getEnvironmentVariableName()); + } } protected final String[] getSpringConfigLocations(AbstractLoggingSystem system) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LogFileTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LogFileTests.java index 46ab410e0ee2..6639436655dd 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LogFileTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LogFileTests.java @@ -57,8 +57,9 @@ private void testLoggingFile(PropertyResolver resolver) { Properties properties = new Properties(); logFile.applyTo(properties); assertThat(logFile).hasToString("log.file"); - assertThat(properties.getProperty(LoggingSystemProperties.LOG_FILE)).isEqualTo("log.file"); - assertThat(properties.getProperty(LoggingSystemProperties.LOG_PATH)).isNull(); + assertThat(properties.getProperty(LoggingSystemProperty.LOG_FILE.getEnvironmentVariableName())) + .isEqualTo("log.file"); + assertThat(properties.getProperty(LoggingSystemProperty.LOG_PATH.getEnvironmentVariableName())).isNull(); } @Test @@ -72,9 +73,10 @@ private void testLoggingPath(PropertyResolver resolver) { Properties properties = new Properties(); logFile.applyTo(properties); assertThat(logFile).hasToString("logpath" + File.separatorChar + "spring.log"); - assertThat(properties.getProperty(LoggingSystemProperties.LOG_FILE)) + assertThat(properties.getProperty(LoggingSystemProperty.LOG_FILE.getEnvironmentVariableName())) .isEqualTo("logpath" + File.separatorChar + "spring.log"); - assertThat(properties.getProperty(LoggingSystemProperties.LOG_PATH)).isEqualTo("logpath"); + assertThat(properties.getProperty(LoggingSystemProperty.LOG_PATH.getEnvironmentVariableName())) + .isEqualTo("logpath"); } @Test @@ -91,8 +93,10 @@ private void testLoggingFileAndPath(PropertyResolver resolver) { Properties properties = new Properties(); logFile.applyTo(properties); assertThat(logFile).hasToString("log.file"); - assertThat(properties.getProperty(LoggingSystemProperties.LOG_FILE)).isEqualTo("log.file"); - assertThat(properties.getProperty(LoggingSystemProperties.LOG_PATH)).isEqualTo("logpath"); + assertThat(properties.getProperty(LoggingSystemProperty.LOG_FILE.getEnvironmentVariableName())) + .isEqualTo("log.file"); + assertThat(properties.getProperty(LoggingSystemProperty.LOG_PATH.getEnvironmentVariableName())) + .isEqualTo("logpath"); } private PropertyResolver getPropertyResolver(Map properties) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingSystemPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingSystemPropertiesTests.java index 3fb0106d718a..c374f0d31c0e 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingSystemPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingSystemPropertiesTests.java @@ -43,8 +43,9 @@ class LoggingSystemPropertiesTests { @BeforeEach void captureSystemPropertyNames() { - System.getProperties().remove(LoggingSystemProperties.CONSOLE_LOG_CHARSET); - System.getProperties().remove(LoggingSystemProperties.FILE_LOG_CHARSET); + for (LoggingSystemProperty property : LoggingSystemProperty.values()) { + System.getProperties().remove(property.getEnvironmentVariableName()); + } this.systemPropertyNames = new HashSet<>(System.getProperties().keySet()); } @@ -56,58 +57,62 @@ void restoreSystemProperties() { @Test void pidIsSet() { new LoggingSystemProperties(new MockEnvironment()).apply(null); - assertThat(System.getProperty(LoggingSystemProperties.PID_KEY)).isNotNull(); + assertThat(getSystemProperty(LoggingSystemProperty.PID)).isNotNull(); } @Test void consoleLogPatternIsSet() { new LoggingSystemProperties(new MockEnvironment().withProperty("logging.pattern.console", "console pattern")) .apply(null); - assertThat(System.getProperty(LoggingSystemProperties.CONSOLE_LOG_PATTERN)).isEqualTo("console pattern"); + assertThat(getSystemProperty(LoggingSystemProperty.CONSOLE_PATTERN)).isEqualTo("console pattern"); } @Test void consoleCharsetWhenNoPropertyUsesUtf8() { new LoggingSystemProperties(new MockEnvironment()).apply(null); - assertThat(System.getProperty(LoggingSystemProperties.CONSOLE_LOG_CHARSET)).isEqualTo("UTF-8"); + assertThat(getSystemProperty(LoggingSystemProperty.CONSOLE_CHARSET)).isEqualTo("UTF-8"); } @Test void consoleCharsetIsSet() { new LoggingSystemProperties(new MockEnvironment().withProperty("logging.charset.console", "UTF-16")) .apply(null); - assertThat(System.getProperty(LoggingSystemProperties.CONSOLE_LOG_CHARSET)).isEqualTo("UTF-16"); + assertThat(getSystemProperty(LoggingSystemProperty.CONSOLE_CHARSET)).isEqualTo("UTF-16"); } @Test void fileLogPatternIsSet() { new LoggingSystemProperties(new MockEnvironment().withProperty("logging.pattern.file", "file pattern")) .apply(null); - assertThat(System.getProperty(LoggingSystemProperties.FILE_LOG_PATTERN)).isEqualTo("file pattern"); + assertThat(getSystemProperty(LoggingSystemProperty.FILE_PATTERN)).isEqualTo("file pattern"); } @Test void fileCharsetWhenNoPropertyUsesUtf8() { new LoggingSystemProperties(new MockEnvironment()).apply(null); - assertThat(System.getProperty(LoggingSystemProperties.FILE_LOG_CHARSET)).isEqualTo("UTF-8"); + assertThat(getSystemProperty(LoggingSystemProperty.FILE_CHARSET)).isEqualTo("UTF-8"); } @Test void fileCharsetIsSet() { new LoggingSystemProperties(new MockEnvironment().withProperty("logging.charset.file", "UTF-16")).apply(null); - assertThat(System.getProperty(LoggingSystemProperties.FILE_LOG_CHARSET)).isEqualTo("UTF-16"); + assertThat(getSystemProperty(LoggingSystemProperty.FILE_CHARSET)).isEqualTo("UTF-16"); } @Test void consoleLogPatternCanReferencePid() { new LoggingSystemProperties(environment("logging.pattern.console", "${PID:unknown}")).apply(null); - assertThat(System.getProperty(LoggingSystemProperties.CONSOLE_LOG_PATTERN)).matches("[0-9]+"); + assertThat(getSystemProperty(LoggingSystemProperty.CONSOLE_PATTERN)).matches("[0-9]+"); } @Test void fileLogPatternCanReferencePid() { new LoggingSystemProperties(environment("logging.pattern.file", "${PID:unknown}")).apply(null); - assertThat(System.getProperty(LoggingSystemProperties.FILE_LOG_PATTERN)).matches("[0-9]+"); + assertThat(getSystemProperty(LoggingSystemProperty.FILE_PATTERN)).matches("[0-9]+"); + } + + private String getSystemProperty(LoggingSystemProperty property) { + return System.getProperty(property.getEnvironmentVariableName()); } private Environment environment(String key, Object value) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/java/JavaLoggingSystemTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/java/JavaLoggingSystemTests.java index cb73992349a9..c13156c843d4 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/java/JavaLoggingSystemTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/java/JavaLoggingSystemTests.java @@ -33,7 +33,7 @@ import org.springframework.boot.logging.LogLevel; import org.springframework.boot.logging.LoggerConfiguration; import org.springframework.boot.logging.LoggingSystem; -import org.springframework.boot.logging.LoggingSystemProperties; +import org.springframework.boot.logging.LoggingSystemProperty; import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.testsupport.system.OutputCaptureExtension; import org.springframework.util.ClassUtils; @@ -113,7 +113,7 @@ void testCustomFormatter(CapturedOutput output) { @Test void testSystemPropertyInitializesFormat(CapturedOutput output) { - System.setProperty(LoggingSystemProperties.PID_KEY, "1234"); + System.setProperty(LoggingSystemProperty.PID.getEnvironmentVariableName(), "1234"); this.loggingSystem.beforeInitialize(); this.loggingSystem.initialize(null, "classpath:" + ClassUtils.addResourcePathToPackagePath(getClass(), "logging.properties"), null); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java index 7aabe82966fb..a177de6bae83 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java @@ -53,7 +53,7 @@ import org.springframework.boot.logging.LoggerConfiguration; import org.springframework.boot.logging.LoggingInitializationContext; import org.springframework.boot.logging.LoggingSystem; -import org.springframework.boot.logging.LoggingSystemProperties; +import org.springframework.boot.logging.LoggingSystemProperty; import org.springframework.boot.testsupport.classpath.ClassPathExclusions; import org.springframework.boot.testsupport.logging.ConfigureClasspathToPreferLog4j2; import org.springframework.boot.testsupport.system.CapturedOutput; @@ -361,7 +361,7 @@ void beforeInitializeFilterDisablesErrorLogging() { @Test void customExceptionConversionWord(CapturedOutput output) { - System.setProperty(LoggingSystemProperties.EXCEPTION_CONVERSION_WORD, "%ex"); + System.setProperty(LoggingSystemProperty.EXCEPTION_CONVERSION_WORD.getEnvironmentVariableName(), "%ex"); try { this.loggingSystem.beforeInitialize(); this.logger.info("Hidden"); @@ -373,7 +373,7 @@ void customExceptionConversionWord(CapturedOutput output) { assertThat(output).contains("java.lang.RuntimeException: Expected").doesNotContain("Wrapped by:"); } finally { - System.clearProperty(LoggingSystemProperties.EXCEPTION_CONVERSION_WORD); + System.clearProperty(LoggingSystemProperty.EXCEPTION_CONVERSION_WORD.getEnvironmentVariableName()); } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4j2FileXmlTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4j2FileXmlTests.java index e5ea6eb56bdd..53517e76dc29 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4j2FileXmlTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4j2FileXmlTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -23,7 +23,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import org.springframework.boot.logging.LoggingSystemProperties; +import org.springframework.boot.logging.LoggingSystemProperty; import static org.assertj.core.api.Assertions.assertThat; @@ -42,7 +42,9 @@ class Log4j2FileXmlTests extends Log4j2XmlTests { @AfterEach void stopConfiguration() { super.stopConfiguration(); - System.clearProperty(LoggingSystemProperties.LOG_FILE); + for (LoggingSystemProperty property : LoggingSystemProperty.values()) { + System.getProperties().remove(property.getEnvironmentVariableName()); + } } @Test @@ -52,7 +54,7 @@ void whenLogExceptionConversionWordIsNotConfiguredThenFileAppenderUsesDefault() @Test void whenLogExceptionConversionWordIsSetThenFileAppenderUsesIt() { - withSystemProperty(LoggingSystemProperties.EXCEPTION_CONVERSION_WORD, "custom", + withSystemProperty(LoggingSystemProperty.EXCEPTION_CONVERSION_WORD.getEnvironmentVariableName(), "custom", () -> assertThat(fileAppenderPattern()).contains("custom")); } @@ -63,7 +65,7 @@ void whenLogLevelPatternIsNotConfiguredThenFileAppenderUsesDefault() { @Test void whenLogLevelPatternIsSetThenFileAppenderUsesIt() { - withSystemProperty(LoggingSystemProperties.LOG_LEVEL_PATTERN, "custom", + withSystemProperty(LoggingSystemProperty.LEVEL_PATTERN.getEnvironmentVariableName(), "custom", () -> assertThat(fileAppenderPattern()).contains("custom")); } @@ -74,7 +76,7 @@ void whenLogLDateformatPatternIsNotConfiguredThenFileAppenderUsesDefault() { @Test void whenLogDateformatPatternIsSetThenFileAppenderUsesIt() { - withSystemProperty(LoggingSystemProperties.LOG_DATEFORMAT_PATTERN, "dd-MM-yyyy", + withSystemProperty(LoggingSystemProperty.DATEFORMAT_PATTERN.getEnvironmentVariableName(), "dd-MM-yyyy", () -> assertThat(fileAppenderPattern()).contains("dd-MM-yyyy")); } @@ -85,7 +87,8 @@ protected String getConfigFileName() { @Override protected void prepareConfiguration() { - System.setProperty(LoggingSystemProperties.LOG_FILE, new File(this.temp, "test.log").getAbsolutePath()); + System.setProperty(LoggingSystemProperty.LOG_FILE.getEnvironmentVariableName(), + new File(this.temp, "test.log").getAbsolutePath()); super.prepareConfiguration(); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4j2XmlTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4j2XmlTests.java index 850d7a4d6b45..86ecd1e293c7 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4j2XmlTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4j2XmlTests.java @@ -27,7 +27,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -import org.springframework.boot.logging.LoggingSystemProperties; +import org.springframework.boot.logging.LoggingSystemProperty; import static org.assertj.core.api.Assertions.assertThat; @@ -53,7 +53,7 @@ void whenLogExceptionConversionWordIsNotConfiguredThenConsoleUsesDefault() { @Test void whenLogExceptionConversionWordIsSetThenConsoleUsesIt() { - withSystemProperty(LoggingSystemProperties.EXCEPTION_CONVERSION_WORD, "custom", + withSystemProperty(LoggingSystemProperty.EXCEPTION_CONVERSION_WORD.getEnvironmentVariableName(), "custom", () -> assertThat(consolePattern()).contains("custom")); } @@ -64,7 +64,7 @@ void whenLogLevelPatternIsNotConfiguredThenConsoleUsesDefault() { @Test void whenLogLevelPatternIsSetThenConsoleUsesIt() { - withSystemProperty(LoggingSystemProperties.LOG_LEVEL_PATTERN, "custom", + withSystemProperty(LoggingSystemProperty.LEVEL_PATTERN.getEnvironmentVariableName(), "custom", () -> assertThat(consolePattern()).contains("custom")); } @@ -75,7 +75,7 @@ void whenLogLDateformatPatternIsNotConfiguredThenConsoleUsesDefault() { @Test void whenLogDateformatPatternIsSetThenConsoleUsesIt() { - withSystemProperty(LoggingSystemProperties.LOG_DATEFORMAT_PATTERN, "dd-MM-yyyy", + withSystemProperty(LoggingSystemProperty.DATEFORMAT_PATTERN.getEnvironmentVariableName(), "dd-MM-yyyy", () -> assertThat(consolePattern()).contains("dd-MM-yyyy")); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemPropertiesTests.java index 2ab61b672f98..45c82c533de1 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemPropertiesTests.java @@ -26,6 +26,7 @@ import org.springframework.boot.convert.ApplicationConversionService; import org.springframework.boot.logging.LoggingSystemProperties; +import org.springframework.boot.logging.LoggingSystemProperty; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.mock.env.MockEnvironment; @@ -44,8 +45,9 @@ class LogbackLoggingSystemPropertiesTests { @BeforeEach void captureSystemPropertyNames() { - System.getProperties().remove(LoggingSystemProperties.CONSOLE_LOG_CHARSET); - System.getProperties().remove(LoggingSystemProperties.FILE_LOG_CHARSET); + for (LoggingSystemProperty property : LoggingSystemProperty.values()) { + System.getProperties().remove(property.getEnvironmentVariableName()); + } this.systemPropertyNames = new HashSet<>(System.getProperties().keySet()); this.environment = new MockEnvironment(); this.environment @@ -62,7 +64,8 @@ void restoreSystemProperties() { void applySetsStandardSystemProperties() { this.environment.setProperty("logging.pattern.console", "boot"); new LogbackLoggingSystemProperties(this.environment).apply(); - assertThat(System.getProperties()).containsEntry(LoggingSystemProperties.CONSOLE_LOG_PATTERN, "boot"); + assertThat(System.getProperties()) + .containsEntry(LoggingSystemProperty.CONSOLE_PATTERN.getEnvironmentVariableName(), "boot"); } @Test @@ -74,11 +77,11 @@ void applySetsLogbackSystemProperties() { this.environment.setProperty("logging.logback.rollingpolicy.max-history", "mh"); new LogbackLoggingSystemProperties(this.environment).apply(); assertThat(System.getProperties()) - .containsEntry(LogbackLoggingSystemProperties.ROLLINGPOLICY_FILE_NAME_PATTERN, "fnp") - .containsEntry(LogbackLoggingSystemProperties.ROLLINGPOLICY_CLEAN_HISTORY_ON_START, "chos") - .containsEntry(LogbackLoggingSystemProperties.ROLLINGPOLICY_MAX_FILE_SIZE, "1024") - .containsEntry(LogbackLoggingSystemProperties.ROLLINGPOLICY_TOTAL_SIZE_CAP, "2048") - .containsEntry(LogbackLoggingSystemProperties.ROLLINGPOLICY_MAX_HISTORY, "mh"); + .containsEntry(RollingPolicySystemProperty.FILE_NAME_PATTERN.getEnvironmentVariableName(), "fnp") + .containsEntry(RollingPolicySystemProperty.CLEAN_HISTORY_ON_START.getEnvironmentVariableName(), "chos") + .containsEntry(RollingPolicySystemProperty.MAX_FILE_SIZE.getEnvironmentVariableName(), "1024") + .containsEntry(RollingPolicySystemProperty.TOTAL_SIZE_CAP.getEnvironmentVariableName(), "2048") + .containsEntry(RollingPolicySystemProperty.MAX_HISTORY.getEnvironmentVariableName(), "mh"); } @Test @@ -90,24 +93,24 @@ void applySetsLogbackSystemPropertiesFromDeprecated() { this.environment.setProperty("logging.file.max-history", "mh"); new LogbackLoggingSystemProperties(this.environment).apply(); assertThat(System.getProperties()) - .containsEntry(LogbackLoggingSystemProperties.ROLLINGPOLICY_FILE_NAME_PATTERN, "fnp") - .containsEntry(LogbackLoggingSystemProperties.ROLLINGPOLICY_CLEAN_HISTORY_ON_START, "chos") - .containsEntry(LogbackLoggingSystemProperties.ROLLINGPOLICY_MAX_FILE_SIZE, "1024") - .containsEntry(LogbackLoggingSystemProperties.ROLLINGPOLICY_TOTAL_SIZE_CAP, "2048") - .containsEntry(LogbackLoggingSystemProperties.ROLLINGPOLICY_MAX_HISTORY, "mh"); + .containsEntry(RollingPolicySystemProperty.FILE_NAME_PATTERN.getEnvironmentVariableName(), "fnp") + .containsEntry(RollingPolicySystemProperty.CLEAN_HISTORY_ON_START.getEnvironmentVariableName(), "chos") + .containsEntry(RollingPolicySystemProperty.MAX_FILE_SIZE.getEnvironmentVariableName(), "1024") + .containsEntry(RollingPolicySystemProperty.TOTAL_SIZE_CAP.getEnvironmentVariableName(), "2048") + .containsEntry(RollingPolicySystemProperty.MAX_HISTORY.getEnvironmentVariableName(), "mh"); } @Test void consoleCharsetWhenNoPropertyUsesDefault() { new LoggingSystemProperties(new MockEnvironment()).apply(null); - assertThat(System.getProperty(LoggingSystemProperties.CONSOLE_LOG_CHARSET)) + assertThat(System.getProperty(LoggingSystemProperty.CONSOLE_CHARSET.getEnvironmentVariableName())) .isEqualTo(Charset.defaultCharset().name()); } @Test void fileCharsetWhenNoPropertyUsesDefault() { new LoggingSystemProperties(new MockEnvironment()).apply(null); - assertThat(System.getProperty(LoggingSystemProperties.FILE_LOG_CHARSET)) + assertThat(System.getProperty(LoggingSystemProperty.FILE_CHARSET.getEnvironmentVariableName())) .isEqualTo(Charset.defaultCharset().name()); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java index a0a27b077e7e..515d09a1e197 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java @@ -56,6 +56,7 @@ import org.springframework.boot.logging.LoggingInitializationContext; import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.logging.LoggingSystemProperties; +import org.springframework.boot.logging.LoggingSystemProperty; import org.springframework.boot.testsupport.classpath.ClassPathOverrides; import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.testsupport.system.OutputCaptureExtension; @@ -102,8 +103,9 @@ class LogbackLoggingSystemTests extends AbstractLoggingSystemTests { @BeforeEach void setup() { - System.getProperties().remove(LoggingSystemProperties.CONSOLE_LOG_CHARSET); - System.getProperties().remove(LoggingSystemProperties.FILE_LOG_CHARSET); + for (LoggingSystemProperty property : LoggingSystemProperty.values()) { + System.getProperties().remove(property.getEnvironmentVariableName()); + } this.systemPropertyNames = new HashSet<>(System.getProperties().keySet()); this.loggingSystem.cleanUp(); this.logger = ((LoggerContext) LoggerFactory.getILoggerFactory()).getLogger(getClass()); @@ -503,7 +505,7 @@ void exceptionsIncludeClassPackaging(CapturedOutput output) { @Test void customExceptionConversionWord(CapturedOutput output) { - System.setProperty(LoggingSystemProperties.EXCEPTION_CONVERSION_WORD, "%ex"); + System.setProperty(LoggingSystemProperty.EXCEPTION_CONVERSION_WORD.getEnvironmentVariableName(), "%ex"); try { this.loggingSystem.beforeInitialize(); this.logger.info("Hidden"); @@ -514,7 +516,7 @@ void customExceptionConversionWord(CapturedOutput output) { assertThat(output).contains("java.lang.RuntimeException: Expected").doesNotContain("Wrapped by:"); } finally { - System.clearProperty(LoggingSystemProperties.EXCEPTION_CONVERSION_WORD); + System.clearProperty(LoggingSystemProperty.EXCEPTION_CONVERSION_WORD.getEnvironmentVariableName()); } } @@ -525,7 +527,8 @@ void initializeShouldSetSystemProperty() { this.logger.info("Hidden"); LogFile logFile = getLogFile(tmpDir() + "/example.log", null, false); initialize(this.initializationContext, "classpath:logback-nondefault.xml", logFile); - assertThat(System.getProperty(LoggingSystemProperties.LOG_FILE)).endsWith("example.log"); + assertThat(System.getProperty(LoggingSystemProperty.LOG_FILE.getEnvironmentVariableName())) + .endsWith("example.log"); } @Test From c1b295fd71573209122b2321b256675babfacc96 Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Tue, 20 Jun 2023 21:35:36 -0700 Subject: [PATCH 0061/1215] Log correlation IDs when Micrometer tracing is being used Add support for logging correlation IDs with Logback or Log4J2 whenever Micrometer tracing is being used. The `LoggingSystemProperties` class now accepts a defualt value resolver which will be used whenever a value isn't in the environment. The `AbstractLoggingSystem` provides a resolver that supports the `logging.pattern.correlation` property and will return a value whenever `LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY` is set. Using `LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY` allows us to provide a consistent width for the correlation ID, even when it's missing from the MDC. The exact correlation pattern returned will depend on the `LoggingSytem` implementation. Currently Logback and Log4J2 are supported and both make use of a custom converter which delegates to a new `CorrelationIdFormatter` class. Closes gh-33280 --- ...ogCorrelationEnvironmentPostProcessor.java | 69 ++++++ .../main/resources/META-INF/spring.factories | 4 + ...relationEnvironmentPostProcessorTests.java | 67 ++++++ .../src/docs/asciidoc/actuator/tracing.adoc | 20 +- .../src/docs/asciidoc/features/logging.adoc | 1 + .../boot/logging/AbstractLoggingSystem.java | 33 ++- .../boot/logging/CorrelationIdFormatter.java | 199 ++++++++++++++++++ .../boot/logging/LoggingSystem.java | 10 +- .../boot/logging/LoggingSystemProperties.java | 29 ++- .../boot/logging/LoggingSystemProperty.java | 7 +- .../log4j2/CorrelationIdConverter.java | 69 ++++++ .../logging/log4j2/Log4J2LoggingSystem.java | 33 +-- .../logback/CorrelationIdConverter.java | 62 ++++++ .../logback/DefaultLogbackConfiguration.java | 6 +- .../logging/logback/LogbackLoggingSystem.java | 11 +- .../LogbackLoggingSystemProperties.java | 14 ++ .../logging/logback/LogbackRuntimeHints.java | 5 +- ...itional-spring-configuration-metadata.json | 6 + .../boot/logging/log4j2/log4j2-file.xml | 4 +- .../boot/logging/log4j2/log4j2.xml | 4 +- .../boot/logging/logback/defaults.xml | 5 +- .../LoggingApplicationListenerTests.java | 3 +- .../logging/AbstractLoggingSystemTests.java | 18 ++ .../logging/CorrelationIdFormatterTests.java | 122 +++++++++++ .../logging/LoggingSystemPropertiesTests.java | 21 ++ .../log4j2/CorrelationIdConverterTests.java | 65 ++++++ .../log4j2/Log4J2LoggingSystemTests.java | 77 ++++++- .../log4j2/TestLog4J2LoggingSystem.java | 18 +- .../logback/CorrelationIdConverterTests.java | 67 ++++++ .../logback/LogbackLoggingSystemTests.java | 87 +++++++- 30 files changed, 1086 insertions(+), 50 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessor.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessorTests.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/CorrelationIdFormatter.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/CorrelationIdConverter.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/CorrelationIdConverter.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/CorrelationIdFormatterTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/CorrelationIdConverterTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/CorrelationIdConverterTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessor.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessor.java new file mode 100644 index 000000000000..d7e86849d05e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessor.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.tracing; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.boot.logging.LoggingSystem; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertySource; +import org.springframework.util.ClassUtils; + +/** + * {@link EnvironmentPostProcessor} to add a {@link PropertySource} to support log + * correlation IDs when Micrometer is present. Adds support for the + * {@value LoggingSystem#EXPECT_CORRELATION_ID_PROPERTY} property by delegating to + * {@code management.tracing.enabled}. + * + * @author Jonatan Ivanov + * @author Phillip Webb + */ +class LogCorrelationEnvironmentPostProcessor implements EnvironmentPostProcessor { + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + if (ClassUtils.isPresent("io.micrometer.tracing.Tracer", application.getClassLoader())) { + environment.getPropertySources().addLast(new LogCorrelationPropertySource(this, environment)); + } + } + + /** + * Log correlation {@link PropertySource}. + */ + private static class LogCorrelationPropertySource extends PropertySource { + + private static final String NAME = "logCorrelation"; + + private final Environment environment; + + LogCorrelationPropertySource(Object source, Environment environment) { + super(NAME, source); + this.environment = environment; + } + + @Override + public Object getProperty(String name) { + if (name.equals(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY)) { + return this.environment.getProperty("management.tracing.enabled", Boolean.class, Boolean.TRUE); + } + return null; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories index 7d4fdd7b4051..eb43227d975b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories @@ -1,3 +1,7 @@ # Failure Analyzers org.springframework.boot.diagnostics.FailureAnalyzer=\ org.springframework.boot.actuate.autoconfigure.metrics.ValidationFailureAnalyzer + +# Environment Post Processors +org.springframework.boot.env.EnvironmentPostProcessor=\ +org.springframework.boot.actuate.autoconfigure.tracing.LogCorrelationEnvironmentPostProcessor diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessorTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessorTests.java new file mode 100644 index 000000000000..a75a91a28bd5 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessorTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.tracing; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.logging.LoggingSystem; +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.StandardEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LogCorrelationEnvironmentPostProcessor}. + * + * @author Jonatan Ivanov + * @author Phillip Webb + */ +class LogCorrelationEnvironmentPostProcessorTests { + + private final ConfigurableEnvironment environment = new StandardEnvironment(); + + private final SpringApplication application = new SpringApplication(); + + private final LogCorrelationEnvironmentPostProcessor postProcessor = new LogCorrelationEnvironmentPostProcessor(); + + @Test + void getExpectCorrelationIdPropertyWhenMicrometerPresentReturnsTrue() { + this.postProcessor.postProcessEnvironment(this.environment, this.application); + assertThat(this.environment.getProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, Boolean.class, false)) + .isTrue(); + } + + @Test + @ClassPathExclusions("micrometer-tracing-*.jar") + void getExpectCorrelationIdPropertyWhenMicrometerMissingReturnsFalse() { + this.postProcessor.postProcessEnvironment(this.environment, this.application); + assertThat(this.environment.getProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, Boolean.class, false)) + .isFalse(); + } + + @Test + void getExpectCorrelationIdPropertyWhenTracingDisabledReturnsFalse() { + TestPropertyValues.of("management.tracing.enabled=false").applyTo(this.environment); + this.postProcessor.postProcessEnvironment(this.environment, this.application); + assertThat(this.environment.getProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, Boolean.class, false)) + .isFalse(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc index 650b7efe2494..0b9378d1acd5 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc @@ -65,7 +65,25 @@ Now open the Zipkin UI at `http://localhost:9411` and press the "Run Query" butt You should see one trace. Press the "Show" button to see the details of that trace. -TIP: You can include the current trace and span id in the logs by setting the `logging.pattern.level` property to `%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]` + + +[[actuator.micrometer-tracing.logging]] +=== Logging Correlation IDs +Correlation IDs provide a helpful way to link lines in your log files to distributed traces. +By default, as long as configprop:management.tracing.enabled[] has not been set to `false`, Spring Boot will include correlation IDs in your logs whenever you are using Micrometer tracing. + +The default correlation ID is built from `traceId` and `spanId` https://logback.qos.ch/manual/mdc.html[MDC] values. +For example, if Micrometer tracing has added an MDC `traceId` of `803B448A0489F84084905D3093480352` and an MDC `spanId` of `3425F23BB2432450` the log output will include the correlation ID `[803B448A0489F84084905D3093480352-3425F23BB2432450]`. + +If you prefer to use a different format for your correlation ID, you can use the configprop:logging.pattern.correlation[] property to define one. +For example, the following will provide a correlation ID for Logback in format previously used by Spring Sleuth: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + logging: + pattern: + correlation: "[${spring.application.name:},%X{traceId:-},%X{spanId:-}]" +---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc index 5f36f43f630e..b1d81e5ef07a 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc @@ -32,6 +32,7 @@ The following items are output: * Process ID. * A `---` separator to distinguish the start of actual log messages. * Thread name: Enclosed in square brackets (may be truncated for console output). +* Correlation ID: If tracing is enabled (not shown in the sample above) * Logger name: This is usually the source class name (often abbreviated). * The log message. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/AbstractLoggingSystem.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/AbstractLoggingSystem.java index 48be4c91f46a..1d22b9aee8c6 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/AbstractLoggingSystem.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/AbstractLoggingSystem.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. @@ -22,6 +22,7 @@ import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; +import java.util.function.Function; import org.springframework.core.env.Environment; import org.springframework.core.io.ClassPathResource; @@ -174,7 +175,35 @@ protected final String getPackagedConfigFile(String fileName) { } protected final void applySystemProperties(Environment environment, LogFile logFile) { - new LoggingSystemProperties(environment).apply(logFile); + new LoggingSystemProperties(environment, getDefaultValueResolver(environment), null).apply(logFile); + } + + /** + * Return the default value resolver to use when resolving system properties. + * @param environment the environment + * @return the default value resolver + * @since 3.2.0 + */ + protected Function getDefaultValueResolver(Environment environment) { + String defaultLogCorrelationPattern = getDefaultLogCorrelationPattern(); + return (name) -> { + if (StringUtils.hasLength(defaultLogCorrelationPattern) + && LoggingSystemProperty.CORRELATION_PATTERN.getApplicationPropertyName().equals(name) + && environment.getProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, Boolean.class, false)) { + return defaultLogCorrelationPattern; + } + return null; + }; + } + + /** + * Return the default log correlation pattern or {@code null} if log correlation + * patterns are not supported. + * @return the default log correlation pattern + * @since 3.2.0 + */ + protected String getDefaultLogCorrelationPattern() { + return null; } /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/CorrelationIdFormatter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/CorrelationIdFormatter.java new file mode 100644 index 000000000000..5270833a6c90 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/CorrelationIdFormatter.java @@ -0,0 +1,199 @@ +/* + * Copyright 2012-2023 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.logging; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * Utility class that can be used to format a correlation identifier for logging based on + * w3c + * recommendations. + *

+ * The formatter can be configured with a comma-separated list of names and the expected + * length of their resolved value. Each item should be specified in the form + * {@code "(length)"}. For example, {@code "traceId(32),spanId(16)"} specifies the + * names {@code "traceId"} and {@code "spanId"} with expected lengths of {@code 32} and + * {@code 16} respectively. + *

+ * Correlation IDs are formatted as dash separated strings surrounded in square brackets. + * Formatted output is always of a fixed width and with trailing whitespace. Dashes are + * omitted of none of the named items can be resolved. + *

+ * The following example would return a formatted result of + * {@code "[01234567890123456789012345678901-0123456789012345] "}:

+ * CorrelationIdFormatter formatter = CorrelationIdFormatter.of("traceId(32),spanId(16)");
+ * Map<String, String> mdc = Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345");
+ * return formatter.format(mdc::get);
+ * 
+ *

+ * If {@link #of(String)} is called with an empty spec the {@link #DEFAULT} formatter will + * be used. + * + * @author Phillip Webb + * @since 3.2.0 + * @see #of(String) + * @see #of(Collection) + */ +public final class CorrelationIdFormatter { + + /** + * Default {@link CorrelationIdFormatter}. + */ + public static final CorrelationIdFormatter DEFAULT = CorrelationIdFormatter.of("traceId(32),spanId(16)"); + + private final List parts; + + private final String blank; + + private CorrelationIdFormatter(List parts) { + this.parts = parts; + this.blank = String.format("[%s] ", parts.stream().map(Part::blank).collect(Collectors.joining(" "))); + } + + /** + * Format a correlation from the values in the given resolver. + * @param resolver the resolver used to resolve named values + * @return a formatted correlation id + */ + public String format(Function resolver) { + StringBuilder result = new StringBuilder(); + formatTo(resolver, result); + return result.toString(); + } + + /** + * Format a correlation from the values in the given resolver and append it to the + * given {@link Appendable}. + * @param resolver the resolver used to resolve named values + * @param appendable the appendable for the formatted correlation id + */ + public void formatTo(Function resolver, Appendable appendable) { + Predicate canResolve = (part) -> StringUtils.hasLength(resolver.apply(part.name())); + try { + if (this.parts.stream().anyMatch(canResolve)) { + appendable.append("["); + for (Iterator iterator = this.parts.iterator(); iterator.hasNext();) { + appendable.append(iterator.next().resolve(resolver)); + appendable.append((!iterator.hasNext()) ? "" : "-"); + } + appendable.append("] "); + } + else { + appendable.append(this.blank); + } + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + @Override + public String toString() { + return this.parts.stream().map(Part::toString).collect(Collectors.joining(",")); + } + + /** + * Create a new {@link CorrelationIdFormatter} instance from the given specification. + * @param spec a comma separated specification + * @return a new {@link CorrelationIdFormatter} instance + */ + public static CorrelationIdFormatter of(String spec) { + try { + return (!StringUtils.hasText(spec)) ? DEFAULT : of(List.of(spec.split(","))); + } + catch (Exception ex) { + throw new IllegalStateException("Unable to parse correlation formatter spec '%s'".formatted(spec), ex); + } + } + + /** + * Create a new {@link CorrelationIdFormatter} instance from the given specification. + * @param spec a pre-separated specification + * @return a new {@link CorrelationIdFormatter} instance + */ + public static CorrelationIdFormatter of(String[] spec) { + return of((spec != null) ? Arrays.asList(spec) : Collections.emptyList()); + } + + /** + * Create a new {@link CorrelationIdFormatter} instance from the given specification. + * @param spec a pre-separated specification + * @return a new {@link CorrelationIdFormatter} instance + */ + public static CorrelationIdFormatter of(Collection spec) { + if (CollectionUtils.isEmpty(spec)) { + return DEFAULT; + } + List parts = spec.stream().map(Part::of).toList(); + return new CorrelationIdFormatter(parts); + } + + /** + * A part of the correlation id. + * + * @param name the name of the correlation part + * @param length the expected length of the correlation part + */ + static final record Part(String name, int length) { + + private static final Pattern pattern = Pattern.compile("^(.+?)\\((\\d+)\\)?$"); + + String resolve(Function resolver) { + String resolved = resolver.apply(name()); + if (resolved == null) { + return blank(); + } + int padding = length() - resolved.length(); + return resolved + " ".repeat((padding > 0) ? padding : 0); + } + + String blank() { + return " ".repeat(this.length); + } + + @Override + public String toString() { + return "%s(%s)".formatted(name(), length()); + } + + static Part of(String part) { + Matcher matcher = pattern.matcher(part.trim()); + Assert.state(matcher.matches(), () -> "Invalid specification part '%s'".formatted(part)); + String name = matcher.group(1); + int length = Integer.parseInt(matcher.group(2)); + return new Part(name, length); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystem.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystem.java index d35b5ba4c64b..1fd3a398378d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystem.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystem.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -23,6 +23,7 @@ import java.util.Set; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -58,6 +59,13 @@ public abstract class LoggingSystem { private static final LoggingSystemFactory SYSTEM_FACTORY = LoggingSystemFactory.fromSpringFactories(); + /** + * The name of an {@link Environment} property used to indicate that a correlation ID + * is expected to be logged at some point. + * @since 3.2.0 + */ + public static final String EXPECT_CORRELATION_ID_PROPERTY = "logging.expect-correlation-id"; + /** * Return the {@link LoggingSystemProperties} that should be applied. * @param environment the {@link ConfigurableEnvironment} used to obtain value diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperties.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperties.java index 456bb010d075..8b1b20f57f10 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperties.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperties.java @@ -19,6 +19,7 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.function.BiConsumer; +import java.util.function.Function; import org.springframework.boot.system.ApplicationPid; import org.springframework.core.env.ConfigurableEnvironment; @@ -36,6 +37,7 @@ * @author Vedran Pavic * @author Robert Thornton * @author Eddú Meléndez + * @author Jonatan Ivanov * @since 2.0.0 * @see LoggingSystemProperty */ @@ -160,6 +162,8 @@ public class LoggingSystemProperties { private final Environment environment; + private final Function defaultValueResolver; + private final BiConsumer setter; /** @@ -167,20 +171,34 @@ public class LoggingSystemProperties { * @param environment the source environment */ public LoggingSystemProperties(Environment environment) { - this(environment, systemPropertySetter); + this(environment, null); } /** * Create a new {@link LoggingSystemProperties} instance. * @param environment the source environment - * @param setter setter used to apply the property + * @param setter setter used to apply the property or {@code null} for system + * properties * @since 2.4.2 */ public LoggingSystemProperties(Environment environment, BiConsumer setter) { + this(environment, null, setter); + } + + /** + * Create a new {@link LoggingSystemProperties} instance. + * @param environment the source environment + * @param defaultValueResolver function used to resolve default values or {@code null} + * @param setter setter used to apply the property or {@code null} for system + * properties + * @since 3.2.0 + */ + public LoggingSystemProperties(Environment environment, Function defaultValueResolver, + BiConsumer setter) { Assert.notNull(environment, "Environment must not be null"); - Assert.notNull(setter, "Setter must not be null"); this.environment = environment; - this.setter = setter; + this.defaultValueResolver = (defaultValueResolver != null) ? defaultValueResolver : (name) -> null; + this.setter = (setter != null) ? setter : systemPropertySetter; } protected Charset getDefaultCharset() { @@ -219,6 +237,7 @@ protected void apply(LogFile logFile, PropertyResolver resolver) { setSystemProperty(LoggingSystemProperty.FILE_PATTERN, resolver); setSystemProperty(LoggingSystemProperty.LEVEL_PATTERN, resolver); setSystemProperty(LoggingSystemProperty.DATEFORMAT_PATTERN, resolver); + setSystemProperty(LoggingSystemProperty.CORRELATION_PATTERN, resolver); if (logFile != null) { logFile.applyToSystemProperties(); } @@ -231,6 +250,7 @@ private void setSystemProperty(LoggingSystemProperty property, PropertyResolver private void setSystemProperty(LoggingSystemProperty property, PropertyResolver resolver, String defaultValue) { String value = (property.getApplicationPropertyName() != null) ? resolver.getProperty(property.getApplicationPropertyName()) : null; + value = (value != null) ? value : this.defaultValueResolver.apply(property.getApplicationPropertyName()); value = (value != null) ? value : defaultValue; setSystemProperty(property.getEnvironmentVariableName(), value); } @@ -263,6 +283,7 @@ protected final void setSystemProperty(PropertyResolver resolver, String systemP protected final void setSystemProperty(PropertyResolver resolver, String systemPropertyName, String propertyName, String defaultValue) { String value = resolver.getProperty(propertyName); + value = (value != null) ? value : this.defaultValueResolver.apply(systemPropertyName); value = (value != null) ? value : defaultValue; setSystemProperty(systemPropertyName, value); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperty.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperty.java index d6a731453189..b835feeb547d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperty.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperty.java @@ -83,7 +83,12 @@ public enum LoggingSystemProperty { /** * Logging system property for the date-format pattern. */ - DATEFORMAT_PATTERN("LOG_DATEFORMAT_PATTERN", "logging.pattern.dateformat"); + DATEFORMAT_PATTERN("LOG_DATEFORMAT_PATTERN", "logging.pattern.dateformat"), + + /** + * Logging system property for the correlation pattern. + */ + CORRELATION_PATTERN("LOG_CORRELATION_PATTERN", "logging.pattern.correlation"); private final String environmentVariableName; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/CorrelationIdConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/CorrelationIdConverter.java new file mode 100644 index 000000000000..5dcf9195da48 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/CorrelationIdConverter.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2023 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.logging.log4j2; + +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.pattern.ConverterKeys; +import org.apache.logging.log4j.core.pattern.LogEventPatternConverter; +import org.apache.logging.log4j.core.pattern.MdcPatternConverter; +import org.apache.logging.log4j.core.pattern.PatternConverter; +import org.apache.logging.log4j.util.PerformanceSensitive; +import org.apache.logging.log4j.util.ReadOnlyStringMap; + +import org.springframework.boot.logging.CorrelationIdFormatter; +import org.springframework.util.ObjectUtils; + +/** + * Log4j2 {@link LogEventPatternConverter} to convert a {@link CorrelationIdFormatter} + * pattern into formatted output using data from the {@link LogEvent#getContextData() + * MDC}. + * + * @author Phillip Webb + * @since 3.2.0 + * @see MdcPatternConverter + */ +@Plugin(name = "CorrelationIdConverter", category = PatternConverter.CATEGORY) +@ConverterKeys("correlationId") +@PerformanceSensitive("allocation") +public final class CorrelationIdConverter extends LogEventPatternConverter { + + private final CorrelationIdFormatter formatter; + + private CorrelationIdConverter(CorrelationIdFormatter formatter) { + super("correlationId{%s}".formatted(formatter), "mdc"); + this.formatter = formatter; + } + + @Override + public void format(LogEvent event, StringBuilder toAppendTo) { + ReadOnlyStringMap contextData = event.getContextData(); + this.formatter.formatTo(contextData::getValue, toAppendTo); + } + + /** + * Factory method to create a new {@link CorrelationIdConverter}. + * @param options options, may be null or first element contains name of property to + * format. + * @return instance of PropertiesPatternConverter. + */ + public static CorrelationIdConverter newInstance(String[] options) { + String pattern = (!ObjectUtils.isEmpty(options)) ? options[0] : null; + return new CorrelationIdConverter(CorrelationIdFormatter.of(pattern)); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java index 1ff7c840ed07..da996bfd5c97 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java @@ -249,27 +249,29 @@ public void initialize(LoggingInitializationContext initializationContext, Strin @Override protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) { - if (logFile != null) { - loadConfiguration(getPackagedConfigFile("log4j2-file.xml"), logFile, getOverrides(initializationContext)); - } - else { - loadConfiguration(getPackagedConfigFile("log4j2.xml"), logFile, getOverrides(initializationContext)); - } - } - - private List getOverrides(LoggingInitializationContext initializationContext) { - BindResult> overrides = Binder.get(initializationContext.getEnvironment()) - .bind("logging.log4j2.config.override", Bindable.listOf(String.class)); - return overrides.orElse(Collections.emptyList()); + String location = (logFile != null) ? getPackagedConfigFile("log4j2-file.xml") + : getPackagedConfigFile("log4j2.xml"); + load(initializationContext, location, logFile); } @Override protected void loadConfiguration(LoggingInitializationContext initializationContext, String location, LogFile logFile) { + load(initializationContext, location, logFile); + } + + private void load(LoggingInitializationContext initializationContext, String location, LogFile logFile) { + List overrides = getOverrides(initializationContext); if (initializationContext != null) { applySystemProperties(initializationContext.getEnvironment(), logFile); } - loadConfiguration(location, logFile, getOverrides(initializationContext)); + loadConfiguration(location, logFile, overrides); + } + + private List getOverrides(LoggingInitializationContext initializationContext) { + BindResult> overrides = Binder.get(initializationContext.getEnvironment()) + .bind("logging.log4j2.config.override", Bindable.listOf(String.class)); + return overrides.orElse(Collections.emptyList()); } /** @@ -492,6 +494,11 @@ private void markAsUninitialized(LoggerContext loggerContext) { loggerContext.setExternalContext(null); } + @Override + protected String getDefaultLogCorrelationPattern() { + return "%correlationId"; + } + /** * Get the Spring {@link Environment} attached to the given {@link LoggerContext} or * {@code null} if no environment is available. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/CorrelationIdConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/CorrelationIdConverter.java new file mode 100644 index 000000000000..87b1e792b6c8 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/CorrelationIdConverter.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2023 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.logging.logback; + +import java.util.Map; + +import ch.qos.logback.classic.pattern.MDCConverter; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.pattern.DynamicConverter; + +import org.springframework.boot.logging.CorrelationIdFormatter; +import org.springframework.core.env.Environment; + +/** + * Logback {@link DynamicConverter} to convert a {@link CorrelationIdFormatter} pattern + * into formatted output using data from the {@link ILoggingEvent#getMDCPropertyMap() MDC} + * and {@link Environment}. + * + * @author Phillip Webb + * @since 3.2.0 + * @see MDCConverter + */ +public class CorrelationIdConverter extends DynamicConverter { + + private CorrelationIdFormatter formatter; + + @Override + public void start() { + this.formatter = CorrelationIdFormatter.of(getOptionList()); + super.start(); + } + + @Override + public void stop() { + this.formatter = null; + super.stop(); + } + + @Override + public String convert(ILoggingEvent event) { + if (this.formatter == null) { + return ""; + } + Map mdc = event.getMDCPropertyMap(); + return this.formatter.format(mdc::get); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java index 0329653359da..c86cd372e5f9 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java @@ -43,6 +43,7 @@ * @author Vedran Pavic * @author Robert Thornton * @author Scott Frederick + * @author Jonatan Ivanov */ class DefaultLogbackConfiguration { @@ -68,12 +69,14 @@ void apply(LogbackConfigurator config) { private void defaults(LogbackConfigurator config) { config.conversionRule("clr", ColorConverter.class); + config.conversionRule("correlationId", CorrelationIdConverter.class); config.conversionRule("wex", WhitespaceThrowableProxyConverter.class); config.conversionRule("wEx", ExtendedWhitespaceThrowableProxyConverter.class); config.getContext() .putProperty("CONSOLE_LOG_PATTERN", resolve(config, "${CONSOLE_LOG_PATTERN:-" + "%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) " - + "%clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} " + + "%clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} " + + "%clr(${LOG_CORRELATION_PATTERN:-}){faint}%clr(%-40.40logger{39}){cyan} " + "%clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}")); String defaultCharset = Charset.defaultCharset().name(); config.getContext() @@ -82,6 +85,7 @@ private void defaults(LogbackConfigurator config) { config.getContext() .putProperty("FILE_LOG_PATTERN", resolve(config, "${FILE_LOG_PATTERN:-" + "%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] " + + "${LOG_CORRELATION_PATTERN:-}" + "%-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}")); config.getContext() .putProperty("FILE_LOG_CHARSET", resolve(config, "${FILE_LOG_CHARSET:-" + defaultCharset + "}")); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java index ee31b9b07a45..6babadee5245 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java @@ -109,7 +109,7 @@ public LogbackLoggingSystem(ClassLoader classLoader) { @Override public LoggingSystemProperties getSystemProperties(ConfigurableEnvironment environment) { - return new LogbackLoggingSystemProperties(environment); + return new LogbackLoggingSystemProperties(environment, getDefaultValueResolver(environment), null); } @Override @@ -186,6 +186,7 @@ public void initialize(LoggingInitializationContext initializationContext, Strin if (!initializeFromAotGeneratedArtifactsIfPossible(initializationContext, logFile)) { super.initialize(initializationContext, configLocation, logFile); } + loggerContext.putObject(Environment.class.getName(), initializationContext.getEnvironment()); loggerContext.getTurboFilterList().remove(FILTER); markAsInitialized(loggerContext); if (StringUtils.hasText(System.getProperty(CONFIGURATION_FILE_PROPERTY))) { @@ -223,7 +224,8 @@ protected void loadDefaults(LoggingInitializationContext initializationContext, } Environment environment = initializationContext.getEnvironment(); // Apply system properties directly in case the same JVM runs multiple apps - new LogbackLoggingSystemProperties(environment, context::putProperty).apply(logFile); + new LogbackLoggingSystemProperties(environment, getDefaultValueResolver(environment), context::putProperty) + .apply(logFile); LogbackConfigurator configurator = debug ? new DebugLogbackConfigurator(context) : new LogbackConfigurator(context); new DefaultLogbackConfiguration(logFile).apply(configurator); @@ -415,6 +417,11 @@ private void markAsUninitialized(LoggerContext loggerContext) { loggerContext.removeObject(LoggingSystem.class.getName()); } + @Override + protected String getDefaultLogCorrelationPattern() { + return "%correlationId"; + } + @Override public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { String key = BeanFactoryInitializationAotContribution.class.getName(); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystemProperties.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystemProperties.java index 3b7b715e1615..df821a473394 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystemProperties.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystemProperties.java @@ -18,6 +18,7 @@ import java.nio.charset.Charset; import java.util.function.BiConsumer; +import java.util.function.Function; import ch.qos.logback.core.util.FileSize; @@ -107,6 +108,19 @@ public LogbackLoggingSystemProperties(Environment environment, BiConsumer defaultValueResolver, + BiConsumer setter) { + super(environment, defaultValueResolver, setter); + } + @Override protected Charset getDefaultCharset() { return Charset.defaultCharset(); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackRuntimeHints.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackRuntimeHints.java index 532a2adf78d4..44e50d50900d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackRuntimeHints.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -59,7 +59,8 @@ private void registerHintsForBuiltInLogbackConverters(ReflectionHints reflection private void registerHintsForSpringBootConverters(ReflectionHints reflection) { registerForPublicConstructorInvocation(reflection, ColorConverter.class, - ExtendedWhitespaceThrowableProxyConverter.class, WhitespaceThrowableProxyConverter.class); + ExtendedWhitespaceThrowableProxyConverter.class, WhitespaceThrowableProxyConverter.class, + CorrelationIdConverter.class); } private void registerForPublicConstructorInvocation(ReflectionHints reflection, Class... classes) { diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 4835aa0c30a3..17c21808424d 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -165,6 +165,12 @@ "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", "defaultValue": "%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}" }, + { + "name": "logging.pattern.correlation", + "type": "java.lang.String", + "description": "Appender pattern for log correlation. Supported only with the default Logback setup.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener" + }, { "name": "logging.pattern.dateformat", "type": "java.lang.String", diff --git a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml index d7c510bb7a98..ee6812d92a66 100644 --- a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml +++ b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml @@ -4,8 +4,8 @@ %xwEx %5p yyyy-MM-dd'T'HH:mm:ss.SSSXXX - %clr{%d{${sys:LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${sys:LOG_LEVEL_PATTERN}} %clr{%pid}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} - %d{${sys:LOG_DATEFORMAT_PATTERN}} ${sys:LOG_LEVEL_PATTERN} %pid --- [%t] %-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} + %clr{%d{${sys:LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${sys:LOG_LEVEL_PATTERN}} %clr{%pid}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{${sys:LOG_CORRELATION_PATTERN:-}}{faint}%clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} + %d{${sys:LOG_DATEFORMAT_PATTERN}} ${sys:LOG_LEVEL_PATTERN} %pid --- [%t] ${sys:LOG_CORRELATION_PATTERN:-}%-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} diff --git a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2.xml b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2.xml index 65f1a1b612d7..de8588498e1e 100644 --- a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2.xml +++ b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2.xml @@ -4,8 +4,8 @@ %xwEx %5p yyyy-MM-dd'T'HH:mm:ss.SSSXXX - %clr{%d{${sys:LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${sys:LOG_LEVEL_PATTERN}} %clr{%pid}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} - %d{${sys:LOG_DATEFORMAT_PATTERN}} ${sys:LOG_LEVEL_PATTERN} %pid --- [%t] %-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} + %clr{%d{${sys:LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${sys:LOG_LEVEL_PATTERN}} %clr{%pid}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{${sys:LOG_CORRELATION_PATTERN:-}}{faint}%clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} + %d{${sys:LOG_DATEFORMAT_PATTERN}} ${sys:LOG_LEVEL_PATTERN} %pid --- [%t] ${sys:LOG_CORRELATION_PATTERN:-}%-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} diff --git a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/logback/defaults.xml b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/logback/defaults.xml index bc2ec1238193..d10022ca0705 100644 --- a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/logback/defaults.xml +++ b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/logback/defaults.xml @@ -6,13 +6,14 @@ Default logback configuration provided for import + - + - + diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/logging/LoggingApplicationListenerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/logging/LoggingApplicationListenerTests.java index 4bfeac363066..d422f10c49a5 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/logging/LoggingApplicationListenerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/logging/LoggingApplicationListenerTests.java @@ -92,6 +92,7 @@ * @author Ben Hale * @author Fahim Farook * @author Eddú Meléndez + * @author Jonatan Ivanov */ @ExtendWith(OutputCaptureExtension.class) @ClassPathExclusions("log4j*.jar") @@ -467,7 +468,7 @@ void closingChildContextDoesNotCleanUpLoggingSystem() { void systemPropertiesAreSetForLoggingConfiguration() { addPropertiesToEnvironment(this.context, "logging.exception-conversion-word=conversion", "logging.file.name=" + this.logFile, "logging.file.path=path", "logging.pattern.console=console", - "logging.pattern.file=file", "logging.pattern.level=level", + "logging.pattern.file=file", "logging.pattern.level=level", "logging.pattern.correlation=correlation", "logging.pattern.rolling-file-name=my.log.%d{yyyyMMdd}.%i.gz"); this.listener.initialize(this.context.getEnvironment(), this.context.getClassLoader()); assertThat(getSystemProperty(LoggingSystemProperty.CONSOLE_PATTERN)).isEqualTo("console"); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/AbstractLoggingSystemTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/AbstractLoggingSystemTests.java index c80bf5654b57..cba035869fff 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/AbstractLoggingSystemTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/AbstractLoggingSystemTests.java @@ -16,14 +16,19 @@ package org.springframework.boot.logging; +import java.io.File; import java.nio.file.Path; +import java.util.Arrays; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.io.TempDir; +import org.slf4j.MDC; import org.springframework.util.StringUtils; +import static org.assertj.core.api.Assertions.contentOf; + /** * Base for {@link LoggingSystem} tests. * @@ -41,6 +46,7 @@ public abstract class AbstractLoggingSystemTests { void configureTempDir(@TempDir Path temp) { this.originalTempDirectory = System.getProperty(JAVA_IO_TMPDIR); System.setProperty(JAVA_IO_TMPDIR, temp.toAbsolutePath().toString()); + MDC.clear(); } @AfterEach @@ -53,6 +59,7 @@ void clear() { for (LoggingSystemProperty property : LoggingSystemProperty.values()) { System.getProperties().remove(property.getEnvironmentVariableName()); } + MDC.clear(); } protected final String[] getSpringConfigLocations(AbstractLoggingSystem system) { @@ -79,4 +86,15 @@ protected final String tmpDir() { return path; } + protected final String getLineWithText(File file, CharSequence outputSearch) { + return getLineWithText(contentOf(file), outputSearch); + } + + protected final String getLineWithText(CharSequence output, CharSequence outputSearch) { + return Arrays.stream(output.toString().split("\\r?\\n")) + .filter((line) -> line.contains(outputSearch)) + .findFirst() + .orElse(null); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/CorrelationIdFormatterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/CorrelationIdFormatterTests.java new file mode 100644 index 000000000000..b34771352a47 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/CorrelationIdFormatterTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2012-2023 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.logging; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link CorrelationIdFormatter}. + * + * @author Phillip Webb + */ +class CorrelationIdFormatterTests { + + @Test + void formatWithDefaultSpecWhenHasBothParts() { + Map context = new HashMap<>(); + context.put("traceId", "01234567890123456789012345678901"); + context.put("spanId", "0123456789012345"); + String formatted = CorrelationIdFormatter.DEFAULT.format(context::get); + assertThat(formatted).isEqualTo("[01234567890123456789012345678901-0123456789012345] "); + } + + @Test + void formatWithDefaultSpecWhenHasNoParts() { + Map context = new HashMap<>(); + String formatted = CorrelationIdFormatter.DEFAULT.format(context::get); + assertThat(formatted).isEqualTo("[ ] "); + } + + @Test + void formatWithDefaultSpecWhenHasOnlyFirstPart() { + Map context = new HashMap<>(); + context.put("traceId", "01234567890123456789012345678901"); + String formatted = CorrelationIdFormatter.DEFAULT.format(context::get); + assertThat(formatted).isEqualTo("[01234567890123456789012345678901- ] "); + } + + @Test + void formatWithDefaultSpecWhenHasOnlySecondPart() { + Map context = new HashMap<>(); + context.put("spanId", "0123456789012345"); + String formatted = CorrelationIdFormatter.DEFAULT.format(context::get); + assertThat(formatted).isEqualTo("[ -0123456789012345] "); + } + + @Test + void formatWhenPartsAreShort() { + Map context = new HashMap<>(); + context.put("traceId", "0123456789012345678901234567"); + context.put("spanId", "012345678901"); + String formatted = CorrelationIdFormatter.DEFAULT.format(context::get); + assertThat(formatted).isEqualTo("[0123456789012345678901234567 -012345678901 ] "); + } + + @Test + void formatWhenPartsAreLong() { + Map context = new HashMap<>(); + context.put("traceId", "01234567890123456789012345678901FFFF"); + context.put("spanId", "0123456789012345FFFF"); + String formatted = CorrelationIdFormatter.DEFAULT.format(context::get); + assertThat(formatted).isEqualTo("[01234567890123456789012345678901FFFF-0123456789012345FFFF] "); + } + + @Test + void formatWithCustomSpec() { + Map context = new HashMap<>(); + context.put("a", "01234567890123456789012345678901"); + context.put("b", "0123456789012345"); + String formatted = CorrelationIdFormatter.of("a(32),b(16)").format(context::get); + assertThat(formatted).isEqualTo("[01234567890123456789012345678901-0123456789012345] "); + } + + @Test + void formatToWithDefaultSpec() { + Map context = new HashMap<>(); + context.put("traceId", "01234567890123456789012345678901"); + context.put("spanId", "0123456789012345"); + StringBuilder formatted = new StringBuilder(); + CorrelationIdFormatter.of("").formatTo(context::get, formatted); + assertThat(formatted).hasToString("[01234567890123456789012345678901-0123456789012345] "); + } + + @Test + void ofWhenSpecIsMalformed() { + assertThatIllegalStateException().isThrownBy(() -> CorrelationIdFormatter.of("good(12),bad")) + .withMessage("Unable to parse correlation formatter spec 'good(12),bad'") + .havingCause() + .withMessage("Invalid specification part 'bad'"); + } + + @Test + void ofWhenSpecIsEmpty() { + assertThat(CorrelationIdFormatter.of("")).isSameAs(CorrelationIdFormatter.DEFAULT); + } + + @Test + void toStringReturnsSpec() { + assertThat(CorrelationIdFormatter.DEFAULT).hasToString("traceId(32),spanId(16)"); + assertThat(CorrelationIdFormatter.of("a(32),b(16)")).hasToString("a(32),b(16)"); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingSystemPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingSystemPropertiesTests.java index c374f0d31c0e..0f5a3a44ceb1 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingSystemPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingSystemPropertiesTests.java @@ -18,6 +18,7 @@ import java.util.Collections; import java.util.HashSet; +import java.util.Map; import java.util.Set; import org.junit.jupiter.api.AfterEach; @@ -36,6 +37,7 @@ * * @author Andy Wilkinson * @author Eddú Meléndez + * @author Jonatan Ivanov */ class LoggingSystemPropertiesTests { @@ -115,6 +117,25 @@ private String getSystemProperty(LoggingSystemProperty property) { return System.getProperty(property.getEnvironmentVariableName()); } + @Test + void correlationPatternIsSet() { + new LoggingSystemProperties( + new MockEnvironment().withProperty("logging.pattern.correlation", "correlation pattern")) + .apply(null); + assertThat(System.getProperty(LoggingSystemProperty.CORRELATION_PATTERN.getEnvironmentVariableName())) + .isEqualTo("correlation pattern"); + } + + @Test + void defaultValueResolverIsUsed() { + MockEnvironment environment = new MockEnvironment(); + Map defaultValues = Map + .of(LoggingSystemProperty.CORRELATION_PATTERN.getApplicationPropertyName(), "default correlation pattern"); + new LoggingSystemProperties(environment, defaultValues::get, null).apply(null); + assertThat(System.getProperty(LoggingSystemProperty.CORRELATION_PATTERN.getEnvironmentVariableName())) + .isEqualTo("default correlation pattern"); + } + private Environment environment(String key, Object value) { StandardEnvironment environment = new StandardEnvironment(); environment.getPropertySources().addLast(new MapPropertySource("test", Collections.singletonMap(key, value))); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/CorrelationIdConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/CorrelationIdConverterTests.java new file mode 100644 index 000000000000..87d9b22cf3f5 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/CorrelationIdConverterTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2023 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.logging.log4j2; + +import java.util.Map; + +import org.apache.logging.log4j.core.AbstractLogEvent; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.impl.JdkMapAdapterStringMap; +import org.apache.logging.log4j.util.ReadOnlyStringMap; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CorrelationIdConverter}. + * + * @author Phillip Webb + */ +class CorrelationIdConverterTests { + + private CorrelationIdConverter converter = CorrelationIdConverter.newInstance(null); + + private final LogEvent event = new TestLogEvent(); + + @Test + void defaultPattern() { + StringBuilder result = new StringBuilder(); + this.converter.format(this.event, result); + assertThat(result).hasToString("[01234567890123456789012345678901-0123456789012345] "); + } + + @Test + void customPattern() { + this.converter = CorrelationIdConverter.newInstance(new String[] { "traceId(0),spanId(0)" }); + StringBuilder result = new StringBuilder(); + this.converter.format(this.event, result); + assertThat(result).hasToString("[01234567890123456789012345678901-0123456789012345] "); + } + + static class TestLogEvent extends AbstractLogEvent { + + @Override + public ReadOnlyStringMap getContextData() { + return new JdkMapAdapterStringMap( + Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345")); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java index a177de6bae83..ceb5b64650a0 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java @@ -39,6 +39,7 @@ import org.apache.logging.log4j.core.config.LoggerConfig; import org.apache.logging.log4j.core.config.Reconfigurable; import org.apache.logging.log4j.core.config.composite.CompositeConfiguration; +import org.apache.logging.log4j.core.config.plugins.util.PluginRegistry; import org.apache.logging.log4j.core.util.ShutdownCallbackRegistry; import org.apache.logging.log4j.jul.Log4jBridgeHandler; import org.apache.logging.log4j.util.PropertiesUtil; @@ -47,12 +48,15 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.MDC; import org.springframework.boot.logging.AbstractLoggingSystemTests; +import org.springframework.boot.logging.LogFile; import org.springframework.boot.logging.LogLevel; import org.springframework.boot.logging.LoggerConfiguration; import org.springframework.boot.logging.LoggingInitializationContext; import org.springframework.boot.logging.LoggingSystem; +import org.springframework.boot.logging.LoggingSystemProperties; import org.springframework.boot.logging.LoggingSystemProperty; import org.springframework.boot.testsupport.classpath.ClassPathExclusions; import org.springframework.boot.testsupport.logging.ConfigureClasspathToPreferLog4j2; @@ -86,12 +90,11 @@ @ConfigureClasspathToPreferLog4j2 class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { - private final TestLog4J2LoggingSystem loggingSystem = new TestLog4J2LoggingSystem(); + private TestLog4J2LoggingSystem loggingSystem; - private final MockEnvironment environment = new MockEnvironment(); + private MockEnvironment environment; - private final LoggingInitializationContext initializationContext = new LoggingInitializationContext( - this.environment); + private LoggingInitializationContext initializationContext; private Logger logger; @@ -99,6 +102,10 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { @BeforeEach void setup() { + PluginRegistry.getInstance().clear(); + this.loggingSystem = new TestLog4J2LoggingSystem(); + this.environment = new MockEnvironment(); + this.initializationContext = new LoggingInitializationContext(this.environment); LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); this.configuration = loggerContext.getConfiguration(); this.loggingSystem.cleanUp(); @@ -113,6 +120,7 @@ void cleanUp() { loggerContext.stop(); loggerContext.start(((Reconfigurable) this.configuration).reconfigure()); cleanUpPropertySources(); + PluginRegistry.getInstance().clear(); } @SuppressWarnings("unchecked") @@ -495,6 +503,67 @@ void nonFileUrlsAreResolvedUsingLog4J2UrlConnectionFactory() { .withMessageContaining("http has not been enabled"); } + @Test + void correlationLoggingToFileWhenExpectCorrelationIdTrueAndMdcContent() { + this.environment.setProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, "true"); + new LoggingSystemProperties(this.environment).apply(); + File file = new File(tmpDir(), "log4j2-test.log"); + LogFile logFile = getLogFile(file.getPath(), null); + this.loggingSystem.setStandardConfigLocations(false); + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, logFile); + MDC.setContextMap(Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345")); + this.logger.info("Hello world"); + assertThat(getLineWithText(file, "Hello world")) + .contains(" [01234567890123456789012345678901-0123456789012345] "); + } + + @Test + void correlationLoggingToConsoleWhenExpectCorrelationIdTrueAndMdcContent(CapturedOutput output) { + this.environment.setProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, "true"); + this.loggingSystem.setStandardConfigLocations(false); + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, null); + MDC.setContextMap(Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345")); + this.logger.info("Hello world"); + assertThat(getLineWithText(output, "Hello world")) + .contains(" [01234567890123456789012345678901-0123456789012345] "); + } + + @Test + void correlationLoggingToConsoleWhenExpectCorrelationIdFalseAndMdcContent(CapturedOutput output) { + this.environment.setProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, "false"); + this.loggingSystem.setStandardConfigLocations(false); + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, null); + MDC.setContextMap(Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345")); + this.logger.info("Hello world"); + assertThat(getLineWithText(output, "Hello world")).doesNotContain("0123456789012345"); + } + + @Test + void correlationLoggingToConsoleWhenExpectCorrelationIdTrueAndNoMdcContent(CapturedOutput output) { + this.environment.setProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, "true"); + this.loggingSystem.setStandardConfigLocations(false); + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, null); + this.logger.info("Hello world"); + assertThat(getLineWithText(output, "Hello world")) + .contains(" [ ] "); + } + + @Test + void correlationLoggingToConsoleWhenHasCorrelationPattern(CapturedOutput output) { + this.environment.setProperty("logging.pattern.correlation", "%correlationId{spanId(0),traceId(0)}"); + this.loggingSystem.setStandardConfigLocations(false); + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, null); + MDC.setContextMap(Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345")); + this.logger.info("Hello world"); + assertThat(getLineWithText(output, "Hello world")) + .contains(" [0123456789012345-01234567890123456789012345678901] "); + } + private String getRelativeClasspathLocation(String fileName) { String defaultPath = ClassUtils.getPackageName(getClass()); defaultPath = defaultPath.replace('.', '/'); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/TestLog4J2LoggingSystem.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/TestLog4J2LoggingSystem.java index cc945ae690a5..47f746592c03 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/TestLog4J2LoggingSystem.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/TestLog4J2LoggingSystem.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -27,6 +27,8 @@ class TestLog4J2LoggingSystem extends Log4J2LoggingSystem { private final List availableClasses = new ArrayList<>(); + private String[] standardConfigLocations; + TestLog4J2LoggingSystem() { super(TestLog4J2LoggingSystem.class.getClassLoader()); } @@ -44,4 +46,18 @@ void availableClasses(String... classNames) { Collections.addAll(this.availableClasses, classNames); } + @Override + protected String[] getStandardConfigLocations() { + return (this.standardConfigLocations != null) ? this.standardConfigLocations + : super.getStandardConfigLocations(); + } + + void setStandardConfigLocations(boolean standardConfigLocations) { + this.standardConfigLocations = (!standardConfigLocations) ? new String[0] : null; + } + + void setStandardConfigLocations(String[] standardConfigLocations) { + this.standardConfigLocations = standardConfigLocations; + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/CorrelationIdConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/CorrelationIdConverterTests.java new file mode 100644 index 000000000000..061251739dc7 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/CorrelationIdConverterTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2023 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.logging.logback; + +import java.util.List; +import java.util.Map; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.LoggingEvent; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CorrelationIdConverter}. + * + * @author Phillip Webb + */ +class CorrelationIdConverterTests { + + private final CorrelationIdConverter converter; + + private final LoggingEvent event = new LoggingEvent(); + + CorrelationIdConverterTests() { + this.converter = new CorrelationIdConverter(); + this.converter.setContext(new LoggerContext()); + } + + @Test + void defaultPattern() { + addMdcProperties(this.event); + this.converter.start(); + String converted = this.converter.convert(this.event); + this.converter.stop(); + assertThat(converted).isEqualTo("[01234567890123456789012345678901-0123456789012345] "); + } + + @Test + void customPattern() { + this.converter.setOptionList(List.of("traceId(0)", "spanId(0)")); + addMdcProperties(this.event); + this.converter.start(); + String converted = this.converter.convert(this.event); + this.converter.stop(); + assertThat(converted).isEqualTo("[01234567890123456789012345678901-0123456789012345] "); + } + + private void addMdcProperties(LoggingEvent event) { + event.setMDCPropertyMap(Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345")); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java index 515d09a1e197..8673dbd701cf 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java @@ -45,6 +45,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.slf4j.ILoggerFactory; import org.slf4j.LoggerFactory; +import org.slf4j.MDC; import org.slf4j.bridge.SLF4JBridgeHandler; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; @@ -87,6 +88,7 @@ * @author Robert Thornton * @author Eddú Meléndez * @author Scott Frederick + * @author Jonatan Ivanov */ @ExtendWith(OutputCaptureExtension.class) class LogbackLoggingSystemTests extends AbstractLoggingSystemTests { @@ -547,6 +549,7 @@ void initializeShouldApplyLogbackSystemPropertiesToTheContext() { (field) -> expectedProperties.add((String) field.get(null)), this::isPublicStaticFinal); expectedProperties.removeAll(Arrays.asList("LOG_FILE", "LOG_PATH")); expectedProperties.add("org.jboss.logging.provider"); + expectedProperties.add("LOG_CORRELATION_PATTERN"); assertThat(properties).containsOnlyKeys(expectedProperties); assertThat(properties).containsEntry("CONSOLE_LOG_CHARSET", Charset.defaultCharset().name()); } @@ -679,8 +682,81 @@ void springProfileIfNestedWithinSecondPhaseElementSanityChecker(CapturedOutput o assertThat(output).contains(" elements cannot be nested within an"); } + @Test + void correlationLoggingToFileWhenExpectCorrelationIdTrueAndMdcContent() { + this.environment.setProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, "true"); + File file = new File(tmpDir(), "logback-test.log"); + LogFile logFile = getLogFile(file.getPath(), null); + initialize(this.initializationContext, null, logFile); + MDC.setContextMap(Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345")); + this.logger.info("Hello world"); + assertThat(getLineWithText(file, "Hello world")) + .contains(" [01234567890123456789012345678901-0123456789012345] "); + } + + @Test + void correlationLoggingToConsoleWhenExpectCorrelationIdTrueAndMdcContent(CapturedOutput output) { + this.environment.setProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, "true"); + initialize(this.initializationContext, null, null); + MDC.setContextMap(Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345")); + this.logger.info("Hello world"); + assertThat(getLineWithText(output, "Hello world")) + .contains(" [01234567890123456789012345678901-0123456789012345] "); + } + + @Test + void correlationLoggingToConsoleWhenExpectCorrelationIdFalseAndMdcContent(CapturedOutput output) { + this.environment.setProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, "false"); + initialize(this.initializationContext, null, null); + MDC.setContextMap(Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345")); + this.logger.info("Hello world"); + assertThat(getLineWithText(output, "Hello world")).doesNotContain("0123456789012345"); + } + + @Test + void correlationLoggingToConsoleWhenExpectCorrelationIdTrueAndNoMdcContent(CapturedOutput output) { + this.environment.setProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, "true"); + initialize(this.initializationContext, null, null); + this.logger.info("Hello world"); + assertThat(getLineWithText(output, "Hello world")) + .contains(" [ ] "); + } + + @Test + void correlationLoggingToConsoleWhenHasCorrelationPattern(CapturedOutput output) { + this.environment.setProperty("logging.pattern.correlation", "%correlationId{spanId(0),traceId(0)}"); + initialize(this.initializationContext, null, null); + MDC.setContextMap(Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345")); + this.logger.info("Hello world"); + assertThat(getLineWithText(output, "Hello world")) + .contains(" [0123456789012345-01234567890123456789012345678901] "); + } + + @Test + void correlationLoggingToConsoleWhenUsingXmlConfiguration(CapturedOutput output) { + this.environment.setProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, "true"); + initialize(this.initializationContext, "classpath:logback-include-base.xml", null); + MDC.setContextMap(Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345")); + this.logger.info("Hello world"); + assertThat(getLineWithText(output, "Hello world")) + .contains(" [01234567890123456789012345678901-0123456789012345] "); + } + + @Test + void correlationLoggingToConsoleWhenUsingFileConfiguration() { + this.environment.setProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, "true"); + File file = new File(tmpDir(), "logback-test.log"); + LogFile logFile = getLogFile(file.getPath(), null); + initialize(this.initializationContext, "classpath:logback-include-base.xml", logFile); + MDC.setContextMap(Map.of("traceId", "01234567890123456789012345678901", "spanId", "0123456789012345")); + this.logger.info("Hello world"); + assertThat(getLineWithText(file, "Hello world")) + .contains(" [01234567890123456789012345678901-0123456789012345] "); + } + private void initialize(LoggingInitializationContext context, String configLocation, LogFile logFile) { this.loggingSystem.getSystemProperties((ConfigurableEnvironment) context.getEnvironment()).apply(logFile); + this.loggingSystem.beforeInitialize(); this.loggingSystem.initialize(context, configLocation, logFile); } @@ -702,15 +778,4 @@ private static SizeAndTimeBasedRollingPolicy getRollingPolicy() { return (SizeAndTimeBasedRollingPolicy) getFileAppender().getRollingPolicy(); } - private String getLineWithText(File file, CharSequence outputSearch) { - return getLineWithText(contentOf(file), outputSearch); - } - - private String getLineWithText(CharSequence output, CharSequence outputSearch) { - return Arrays.stream(output.toString().split("\\r?\\n")) - .filter((line) -> line.contains(outputSearch)) - .findFirst() - .orElse(null); - } - } From 493777d3c950ca0e9278dbededf9a48f41cffefc Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 21 Jun 2023 21:56:20 -0700 Subject: [PATCH 0062/1215] Include the application name on each log line when it is available Update Logback and Log4J2 so that they include the application name on each log line. If `spring.application.name` had not been set, or if `logging.include-application-name` is `false` then the name is not logged. Closes gh-35593 --- .../spring-boot-docs/build.gradle | 2 +- .../src/docs/asciidoc/features/logging.adoc | 2 ++ .../boot/logging/LoggingSystemProperties.java | 12 +++++++++++ .../boot/logging/LoggingSystemProperty.java | 5 +++++ .../logback/DefaultLogbackConfiguration.java | 4 ++-- ...itional-spring-configuration-metadata.json | 7 +++++++ .../boot/logging/log4j2/log4j2-file.xml | 4 ++-- .../boot/logging/log4j2/log4j2.xml | 4 ++-- .../boot/logging/logback/defaults.xml | 4 ++-- .../logging/LoggingSystemPropertiesTests.java | 19 +++++++++++++++++ .../log4j2/Log4J2LoggingSystemTests.java | 21 +++++++++++++++++++ .../logback/LogbackLoggingSystemTests.java | 17 +++++++++++++++ .../src/main/resources/application.properties | 1 + .../src/main/resources/log4j2.xml | 2 +- .../src/main/resources/application.properties | 1 + 15 files changed, 95 insertions(+), 10 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/build.gradle b/spring-boot-project/spring-boot-docs/build.gradle index 3d0d6ddcd8a5..baaf0f823289 100644 --- a/spring-boot-project/spring-boot-docs/build.gradle +++ b/spring-boot-project/spring-boot-docs/build.gradle @@ -293,7 +293,7 @@ task runSpringApplicationExample(type: org.springframework.boot.build.docs.Appli task runLoggingFormatExample(type: org.springframework.boot.build.docs.ApplicationRunner) { classpath = configurations.springApplicationExample + sourceSets.main.output mainClass = "org.springframework.boot.docs.features.springapplication.MyApplication" - args = ["--spring.main.banner-mode=off", "--server.port=0"] + args = ["--spring.main.banner-mode=off", "--server.port=0", "--spring.application.name=myapp"] output = file("$buildDir/example-output/logging-format.txt") expectedLogging = "Started MyApplication in " normalizeTomcatPort() diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc index b1d81e5ef07a..102998a9d8c9 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc @@ -31,6 +31,7 @@ The following items are output: * Log Level: `ERROR`, `WARN`, `INFO`, `DEBUG`, or `TRACE`. * Process ID. * A `---` separator to distinguish the start of actual log messages. +* Application name: Enclosed in square brackets (logged by default only if configprop:spring.application.name[] is set) * Thread name: Enclosed in square brackets (may be truncated for console output). * Correlation ID: If tracing is enabled (not shown in the sample above) * Logger name: This is usually the source class name (often abbreviated). @@ -39,6 +40,7 @@ The following items are output: NOTE: Logback does not have a `FATAL` level. It is mapped to `ERROR`. +TIP: If you have a configprop:spring.application.name[] property but don't want it logged you can set configprop:logging.include-application-name[] to `false`. [[features.logging.console-output]] diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperties.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperties.java index 8b1b20f57f10..17b2e1a3aec9 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperties.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperties.java @@ -27,6 +27,7 @@ import org.springframework.core.env.PropertyResolver; import org.springframework.core.env.PropertySourcesPropertyResolver; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Utility to set system properties that can later be used by log configuration files. @@ -227,6 +228,7 @@ private PropertyResolver getPropertyResolver() { protected void apply(LogFile logFile, PropertyResolver resolver) { String defaultCharsetName = getDefaultCharset().name(); + setApplicationNameSystemProperty(resolver); setSystemProperty(LoggingSystemProperty.PID, new ApplicationPid().toString()); setSystemProperty(LoggingSystemProperty.CONSOLE_CHARSET, resolver, defaultCharsetName); setSystemProperty(LoggingSystemProperty.FILE_CHARSET, resolver, defaultCharsetName); @@ -243,6 +245,16 @@ protected void apply(LogFile logFile, PropertyResolver resolver) { } } + private void setApplicationNameSystemProperty(PropertyResolver resolver) { + if (resolver.getProperty("logging.include-application-name", Boolean.class, Boolean.TRUE)) { + String applicationName = resolver.getProperty("spring.application.name"); + if (StringUtils.hasText(applicationName)) { + setSystemProperty(LoggingSystemProperty.APPLICATION_NAME.getEnvironmentVariableName(), + "[%s] ".formatted(applicationName)); + } + } + } + private void setSystemProperty(LoggingSystemProperty property, PropertyResolver resolver) { setSystemProperty(property, resolver, null); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperty.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperty.java index b835feeb547d..489ebec89feb 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperty.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/LoggingSystemProperty.java @@ -25,6 +25,11 @@ */ public enum LoggingSystemProperty { + /** + * Logging system property for the application name that should be logged. + */ + APPLICATION_NAME("LOGGED_APPLICATION_NAME"), + /** * Logging system property for the process ID. */ diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java index c86cd372e5f9..0298aa6325ff 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/DefaultLogbackConfiguration.java @@ -75,7 +75,7 @@ private void defaults(LogbackConfigurator config) { config.getContext() .putProperty("CONSOLE_LOG_PATTERN", resolve(config, "${CONSOLE_LOG_PATTERN:-" + "%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) " - + "%clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} " + + "%clr(${PID:- }){magenta} %clr(---){faint} %clr(${LOGGED_APPLICATION_NAME:-}[%15.15t]){faint} " + "%clr(${LOG_CORRELATION_PATTERN:-}){faint}%clr(%-40.40logger{39}){cyan} " + "%clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}")); String defaultCharset = Charset.defaultCharset().name(); @@ -84,7 +84,7 @@ private void defaults(LogbackConfigurator config) { config.getContext().putProperty("CONSOLE_LOG_THRESHOLD", resolve(config, "${CONSOLE_LOG_THRESHOLD:-TRACE}")); config.getContext() .putProperty("FILE_LOG_PATTERN", resolve(config, "${FILE_LOG_PATTERN:-" - + "%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] " + + "%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- ${LOGGED_APPLICATION_NAME:-}[%t] " + "${LOG_CORRELATION_PATTERN:-}" + "%-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}")); config.getContext() diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 17c21808424d..a22819a934c5 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -103,6 +103,13 @@ "description": "Log groups to quickly change multiple loggers at the same time. For instance, `logging.group.db=org.hibernate,org.springframework.jdbc`.", "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener" }, + { + "name": "logging.include-application-name", + "type": "java.lang.String>", + "description": "Whether to include the application name in the logs.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": true + }, { "name": "logging.level", "type": "java.util.Map", diff --git a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml index ee6812d92a66..fb3edde9dfe7 100644 --- a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml +++ b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml @@ -4,8 +4,8 @@ %xwEx %5p yyyy-MM-dd'T'HH:mm:ss.SSSXXX - %clr{%d{${sys:LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${sys:LOG_LEVEL_PATTERN}} %clr{%pid}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{${sys:LOG_CORRELATION_PATTERN:-}}{faint}%clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} - %d{${sys:LOG_DATEFORMAT_PATTERN}} ${sys:LOG_LEVEL_PATTERN} %pid --- [%t] ${sys:LOG_CORRELATION_PATTERN:-}%-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} + %clr{%d{${sys:LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${sys:LOG_LEVEL_PATTERN}} %clr{%pid}{magenta} %clr{---}{faint} %clr{${sys:LOGGED_APPLICATION_NAME:-}[%15.15t]}{faint} %clr{${sys:LOG_CORRELATION_PATTERN:-}}{faint}%clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} + %d{${sys:LOG_DATEFORMAT_PATTERN}} ${sys:LOG_LEVEL_PATTERN} %pid --- ${sys:LOGGED_APPLICATION_NAME}[%t] ${sys:LOG_CORRELATION_PATTERN:-}%-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} diff --git a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2.xml b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2.xml index de8588498e1e..600f2fa207ed 100644 --- a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2.xml +++ b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2.xml @@ -4,8 +4,8 @@ %xwEx %5p yyyy-MM-dd'T'HH:mm:ss.SSSXXX - %clr{%d{${sys:LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${sys:LOG_LEVEL_PATTERN}} %clr{%pid}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{${sys:LOG_CORRELATION_PATTERN:-}}{faint}%clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} - %d{${sys:LOG_DATEFORMAT_PATTERN}} ${sys:LOG_LEVEL_PATTERN} %pid --- [%t] ${sys:LOG_CORRELATION_PATTERN:-}%-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} + %clr{%d{${sys:LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${sys:LOG_LEVEL_PATTERN}} %clr{%pid}{magenta} %clr{---}{faint} %clr{${sys:LOGGED_APPLICATION_NAME}[%15.15t]}{faint} %clr{${sys:LOG_CORRELATION_PATTERN:-}}{faint}%clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} + %d{${sys:LOG_DATEFORMAT_PATTERN}} ${sys:LOG_LEVEL_PATTERN} %pid --- ${sys:LOGGED_APPLICATION_NAME}[%t] ${sys:LOG_CORRELATION_PATTERN:-}%-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} diff --git a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/logback/defaults.xml b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/logback/defaults.xml index d10022ca0705..9c02f84e4099 100644 --- a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/logback/defaults.xml +++ b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/logback/defaults.xml @@ -10,10 +10,10 @@ Default logback configuration provided for import - + - + diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingSystemPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingSystemPropertiesTests.java index 0f5a3a44ceb1..2c5a1aaf2855 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingSystemPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/LoggingSystemPropertiesTests.java @@ -136,6 +136,25 @@ void defaultValueResolverIsUsed() { .isEqualTo("default correlation pattern"); } + @Test + void loggedApplicationNameWhenHasApplicationName() { + new LoggingSystemProperties(new MockEnvironment().withProperty("spring.application.name", "test")).apply(null); + assertThat(getSystemProperty(LoggingSystemProperty.APPLICATION_NAME)).isEqualTo("[test] "); + } + + @Test + void loggedApplicationNameWhenHasNoApplicationName() { + new LoggingSystemProperties(new MockEnvironment()).apply(null); + assertThat(getSystemProperty(LoggingSystemProperty.APPLICATION_NAME)).isNull(); + } + + @Test + void loggedApplicationNameWhenApplicationNameLoggingDisabled() { + new LoggingSystemProperties(new MockEnvironment().withProperty("spring.application.name", "test") + .withProperty("logging.include-application-name", "false")).apply(null); + assertThat(getSystemProperty(LoggingSystemProperty.APPLICATION_NAME)).isNull(); + } + private Environment environment(String key, Object value) { StandardEnvironment environment = new StandardEnvironment(); environment.getPropertySources().addLast(new MapPropertySource("test", Collections.singletonMap(key, value))); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java index ceb5b64650a0..fdd2add6889f 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java @@ -564,6 +564,27 @@ void correlationLoggingToConsoleWhenHasCorrelationPattern(CapturedOutput output) .contains(" [0123456789012345-01234567890123456789012345678901] "); } + @Test + void applicationNameLoggingWhenHasApplicationName(CapturedOutput output) { + this.environment.setProperty("spring.application.name", "myapp"); + this.loggingSystem.setStandardConfigLocations(false); + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, null); + this.logger.info("Hello world"); + assertThat(getLineWithText(output, "Hello world")).contains("[myapp] "); + } + + @Test + void applicationNameLoggingWhenDisabled(CapturedOutput output) { + this.environment.setProperty("spring.application.name", "myapp"); + this.environment.setProperty("logging.include-application-name", "false"); + this.loggingSystem.setStandardConfigLocations(false); + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, null); + this.logger.info("Hello world"); + assertThat(getLineWithText(output, "Hello world")).doesNotContain("myapp"); + } + private String getRelativeClasspathLocation(String fileName) { String defaultPath = ClassUtils.getPackageName(getClass()); defaultPath = defaultPath.replace('.', '/'); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java index 8673dbd701cf..685923ead60c 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java @@ -754,6 +754,23 @@ void correlationLoggingToConsoleWhenUsingFileConfiguration() { .contains(" [01234567890123456789012345678901-0123456789012345] "); } + @Test + void applicationNameLoggingWhenHasApplicationName(CapturedOutput output) { + this.environment.setProperty("spring.application.name", "myapp"); + initialize(this.initializationContext, null, null); + this.logger.info("Hello world"); + assertThat(getLineWithText(output, "Hello world")).contains("[myapp] "); + } + + @Test + void applicationNameLoggingWhenDisabled(CapturedOutput output) { + this.environment.setProperty("spring.application.name", "myapp"); + this.environment.setProperty("logging.include-application-name", "false"); + initialize(this.initializationContext, null, null); + this.logger.info("Hello world"); + assertThat(getLineWithText(output, "Hello world")).doesNotContain("myapp"); + } + private void initialize(LoggingInitializationContext context, String configLocation, LogFile logFile) { this.loggingSystem.getSystemProperties((ConfigurableEnvironment) context.getEnvironment()).apply(logFile); this.loggingSystem.beforeInitialize(); diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-log4j2/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-log4j2/src/main/resources/application.properties index 2583f7fcefd9..d622bb1f7ca4 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-log4j2/src/main/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-log4j2/src/main/resources/application.properties @@ -1,3 +1,4 @@ +spring.application.name=sample spring.security.user.name=user spring.security.user.password=password management.endpoint.shutdown.enabled=true diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-log4j2/src/main/resources/log4j2.xml b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-log4j2/src/main/resources/log4j2.xml index 5320cd61c746..1c84d286b093 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-log4j2/src/main/resources/log4j2.xml +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator-log4j2/src/main/resources/log4j2.xml @@ -2,7 +2,7 @@ ???? - %clr{%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}}{faint} %clr{%5p} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n%xwEx + %clr{%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}}{faint} %clr{%5p} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{${sys:LOGGED_APPLICATION_NAME:-}[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n%xwEx diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties index 81cc777bfc89..2c35d22ff033 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/src/main/resources/application.properties @@ -1,3 +1,4 @@ +spring.application.name=sample service.name=Phil spring.security.user.name=user From 228b8eb8e4285cb79549e27842cf705acb6c69e9 Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Thu, 22 Jun 2023 14:43:55 -0700 Subject: [PATCH 0063/1215] Polish log correlation docs Docs related to gh-33280 (log correlation) and gh-35593 (application name in each log line) need some polishing: - Fix project names - Show how to avoid having the application name duplicated in logs - Call out that a trailing space is needed in the correlation pattern Closes gh-36035 See gh-33280 See gh-35593 --- .../src/docs/asciidoc/actuator/tracing.adoc | 14 +++++++++----- .../spring-boot-smoke-test-actuator/build.gradle | 1 + 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc index 0b9378d1acd5..53ab1290e237 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc @@ -69,22 +69,26 @@ Press the "Show" button to see the details of that trace. [[actuator.micrometer-tracing.logging]] === Logging Correlation IDs -Correlation IDs provide a helpful way to link lines in your log files to distributed traces. -By default, as long as configprop:management.tracing.enabled[] has not been set to `false`, Spring Boot will include correlation IDs in your logs whenever you are using Micrometer tracing. +Correlation IDs provide a helpful way to link lines in your log files to spans/traces. +By default, as long as configprop:management.tracing.enabled[] has not been set to `false`, Spring Boot will include correlation IDs in your logs whenever you are using Micrometer Tracing. The default correlation ID is built from `traceId` and `spanId` https://logback.qos.ch/manual/mdc.html[MDC] values. -For example, if Micrometer tracing has added an MDC `traceId` of `803B448A0489F84084905D3093480352` and an MDC `spanId` of `3425F23BB2432450` the log output will include the correlation ID `[803B448A0489F84084905D3093480352-3425F23BB2432450]`. +For example, if Micrometer Tracing has added an MDC `traceId` of `803B448A0489F84084905D3093480352` and an MDC `spanId` of `3425F23BB2432450` the log output will include the correlation ID `[803B448A0489F84084905D3093480352-3425F23BB2432450]`. If you prefer to use a different format for your correlation ID, you can use the configprop:logging.pattern.correlation[] property to define one. -For example, the following will provide a correlation ID for Logback in format previously used by Spring Sleuth: +For example, the following will provide a correlation ID for Logback in format previously used by Spring Cloud Sleuth: [source,yaml,indent=0,subs="verbatim",configprops,configblocks] ---- logging: pattern: - correlation: "[${spring.application.name:},%X{traceId:-},%X{spanId:-}]" + correlation: "[${spring.application.name:},%X{traceId:-},%X{spanId:-}] " + include-application-name: false ---- +NOTE: In the example above, configprop:logging.include-application-name[] is set to `false` to avoid the application name being duplicated in the log messages (configprop:logging.pattern.correlation[] already contains it). +It's also worth mentioning that configprop:logging.pattern.correlation[] contains a trailing space so that it is separated from the logger name that comes right after it by default. + [[actuator.micrometer-tracing.tracer-implementations]] diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/build.gradle index b616d3a9697a..656ad05a752d 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/build.gradle @@ -11,6 +11,7 @@ dependencies { implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-security")) implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web")) implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-validation")) + implementation 'io.micrometer:micrometer-tracing-bridge-brave' runtimeOnly("com.h2database:h2") From 8f7fdc507e0fa3a4a8d5849832daf50cb9af55a4 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 23 Jun 2023 08:26:32 +0200 Subject: [PATCH 0064/1215] Polish CorrelationIdFormatter --- .../springframework/boot/logging/CorrelationIdFormatter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/CorrelationIdFormatter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/CorrelationIdFormatter.java index 5270833a6c90..5dcab9f4d503 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/CorrelationIdFormatter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/CorrelationIdFormatter.java @@ -47,7 +47,7 @@ *

* Correlation IDs are formatted as dash separated strings surrounded in square brackets. * Formatted output is always of a fixed width and with trailing whitespace. Dashes are - * omitted of none of the named items can be resolved. + * omitted if none of the named items can be resolved. *

* The following example would return a formatted result of * {@code "[01234567890123456789012345678901-0123456789012345] "}:

@@ -164,7 +164,7 @@ public static CorrelationIdFormatter of(Collection spec) {
 	 * @param name the name of the correlation part
 	 * @param length the expected length of the correlation part
 	 */
-	static final record Part(String name, int length) {
+	record Part(String name, int length) {
 
 		private static final Pattern pattern = Pattern.compile("^(.+?)\\((\\d+)\\)?$");
 

From 50d8b20d6c496481bf1bd042e234bdd2dd557119 Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Fri, 23 Jun 2023 16:01:26 +0100
Subject: [PATCH 0065/1215] Re-enable forward merge issues for merges into main

---
 git/hooks/prepare-forward-merge | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/git/hooks/prepare-forward-merge b/git/hooks/prepare-forward-merge
index dea35e7391d8..786afcfb312a 100755
--- a/git/hooks/prepare-forward-merge
+++ b/git/hooks/prepare-forward-merge
@@ -27,7 +27,7 @@ end
 def rewrite_message(message_file, fixed)
   current_branch = `git rev-parse --abbrev-ref HEAD`.strip
   if current_branch == "main"
-    return nil
+    current_branch = $main_branch
   end
   rewritten_message = ""
   message = File.read(message_file)
@@ -68,6 +68,4 @@ end
 $log.debug "Searching for forward merge"
 fixed = get_fixed_issues()
 rewritten_message = rewrite_message(message_file, fixed)
-unless rewritten_message.nil?
-  File.write(message_file, rewritten_message)
-end
+File.write(message_file, rewritten_message)

From b645eb32ac14af60f9acfa6866b20bc9b15e2940 Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Fri, 23 Jun 2023 17:28:22 +0100
Subject: [PATCH 0066/1215] Remove deprecated code that was to be removed in
 3.2

Closes gh-36034
---
 ...mpositeHealthContributorConfiguration.java |  45 ----
 ...mpositeHealthContributorConfiguration.java |  14 +-
 ...eactiveHealthContributorConfiguration.java |  14 +-
 .../metrics/MetricsProperties.java            |  53 -----
 .../JerseyServerMetricsAutoConfiguration.java |  23 +-
 .../observation/ObservationProperties.java    |  10 +-
 .../GraphQlObservationAutoConfiguration.java  |   1 -
 ...lientHttpObservationConventionAdapter.java |  62 -----
 .../ClientObservationConventionAdapter.java   |  76 ------
 ...tpClientObservationsAutoConfiguration.java |   5 +-
 .../RestTemplateObservationConfiguration.java |  34 +--
 .../WebClientObservationConfiguration.java    |  34 +--
 ...erRequestObservationConventionAdapter.java |  79 -------
 .../WebFluxObservationAutoConfiguration.java  |  43 +---
 ...erRequestObservationConventionAdapter.java |  76 ------
 .../WebMvcObservationAutoConfiguration.java   |  53 +----
 ...itional-spring-configuration-metadata.json |  20 +-
 ...ntributorConfigurationReflectionTests.java |  57 -----
 ...ntributorConfigurationReflectionTests.java |  60 -----
 ...HttpObservationConventionAdapterTests.java |  90 -------
 ...ientObservationConventionAdapterTests.java | 100 --------
 ...TemplateObservationConfigurationTests.java |  59 +----
 ...ebClientObservationConfigurationTests.java |  24 +-
 ...uestObservationConventionAdapterTests.java |  64 -----
 ...FluxObservationAutoConfigurationTests.java |  73 ------
 ...uestObservationConventionAdapterTests.java | 111 ---------
 ...bMvcObservationAutoConfigurationTests.java |  81 -------
 .../PrometheusPushGatewayManager.java         |   9 +-
 ...faultRestTemplateExchangeTagsProvider.java |  49 ----
 .../web/client/RestTemplateExchangeTags.java  | 144 ------------
 .../RestTemplateExchangeTagsProvider.java     |  49 ----
 .../DefaultWebClientExchangeTagsProvider.java |  49 ----
 .../client/WebClientExchangeTags.java         | 127 ----------
 .../client/WebClientExchangeTagsProvider.java |  46 ----
 .../server/DefaultWebFluxTagsProvider.java    |  89 -------
 .../web/reactive/server/WebFluxTags.java      | 191 ---------------
 .../server/WebFluxTagsContributor.java        |  44 ----
 .../reactive/server/WebFluxTagsProvider.java  |  44 ----
 .../web/reactive/server/package-info.java     |  20 --
 .../servlet/DefaultWebMvcTagsProvider.java    |  94 --------
 .../metrics/web/servlet/WebMvcTags.java       | 193 ---------------
 .../web/servlet/WebMvcTagsContributor.java    |  58 -----
 .../web/servlet/WebMvcTagsProvider.java       |  58 -----
 .../metrics/web/servlet/package-info.java     |  20 --
 .../DefaultWebMvcTagsProviderTests.java       | 120 ----------
 .../endpoint/web/servlet/WebMvcTagsTests.java | 184 ---------------
 .../PrometheusPushGatewayManagerTests.java    |  12 -
 .../client/RestTemplateExchangeTagsTests.java | 118 ----------
 ...ultWebClientExchangeTagsProviderTests.java | 100 --------
 .../client/WebClientExchangeTagsTests.java    | 182 ---------------
 .../DefaultWebFluxTagsProviderTests.java      |  82 -------
 .../web/reactive/server/WebFluxTagsTests.java | 220 ------------------
 .../flyway/FlywayAutoConfiguration.java       |  10 -
 .../liquibase/LiquibaseProperties.java        |  12 -
 .../session/SessionAutoConfiguration.java     |   2 +-
 .../autoconfigure/web/ServerProperties.java   |  29 +--
 .../NettyWebServerFactoryCustomizer.java      |  10 -
 .../web/servlet/WebMvcAutoConfiguration.java  |  19 --
 .../web/servlet/WebMvcProperties.java         |  18 --
 ...itional-spring-configuration-metadata.json |  33 ++-
 .../LiquibaseAutoConfigurationTests.java      |   8 -
 .../web/ServerPropertiesTests.java            |  26 ---
 .../JettyWebServerFactoryCustomizerTests.java |  24 --
 .../NettyWebServerFactoryCustomizerTests.java |  12 -
 ...TomcatWebServerFactoryCustomizerTests.java |  51 ----
 ...dertowWebServerFactoryCustomizerTests.java |  12 +-
 ...ervletWebServerFactoryCustomizerTests.java |   5 +-
 .../servlet/WebMvcAutoConfigurationTests.java |  52 -----
 ...endencyInjectionTestExecutionListener.java |  63 -----
 .../actuate/metrics/AutoConfigureMetrics.java |  45 ----
 .../actuate/metrics/package-info.java         |  20 --
 ...nfigureMetricsMissingIntegrationTests.java |  53 -----
 ...nfigureMetricsPresentIntegrationTests.java |  53 -----
 ...ConfigureMetricsSpringBootApplication.java |  37 ---
 ...ltTestExecutionListenersPostProcessor.java |  48 ----
 .../SpringBootTestContextBootstrapper.java    |  14 --
 ...stContextBootstrapperIntegrationTests.java |   7 -
 ...ltTestExecutionListenersPostProcessor.java |  52 -----
 .../PropertyDescriptorResolverTests.java      |  11 -
 .../DeprecatedConstructorBinding.java         |  39 ----
 ...edImmutableMultiConstructorProperties.java |  46 ----
 .../properties/ConfigurationProperties.java   |   3 +-
 .../properties/ConstructorBinding.java        |  52 -----
 .../context/properties/ConstructorBound.java  |   3 +-
 .../boot/jackson/JsonMixinModule.java         |  21 --
 .../ClientHttpRequestFactorySupplier.java     |  41 ----
 .../boot/web/client/RestTemplateBuilder.java  |   5 +-
 .../AbstractServletWebServerFactory.java      |   5 +-
 .../boot/web/servlet/server/Session.java      |  32 +--
 .../ConfigurationPropertiesBeanTests.java     |  32 ---
 .../boot/jackson/JsonMixinModuleTests.java    |  10 -
 .../TomcatServletWebServerFactoryTests.java   |   5 -
 .../AbstractServletWebServerFactoryTests.java |  14 --
 93 files changed, 117 insertions(+), 4580 deletions(-)
 delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/ClientHttpObservationConventionAdapter.java
 delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/ClientObservationConventionAdapter.java
 delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/ServerRequestObservationConventionAdapter.java
 delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/ServerRequestObservationConventionAdapter.java
 delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/CompositeHealthContributorConfigurationReflectionTests.java
 delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/CompositeReactiveHealthContributorConfigurationReflectionTests.java
 delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/ClientHttpObservationConventionAdapterTests.java
 delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/ClientObservationConventionAdapterTests.java
 delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/ServerRequestObservationConventionAdapterTests.java
 delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/ServerRequestObservationConventionAdapterTests.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/DefaultRestTemplateExchangeTagsProvider.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/RestTemplateExchangeTags.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/RestTemplateExchangeTagsProvider.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProvider.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTags.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsProvider.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProvider.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTags.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsContributor.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsProvider.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/package-info.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/DefaultWebMvcTagsProvider.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTags.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTagsContributor.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTagsProvider.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/package-info.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/DefaultWebMvcTagsProviderTests.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcTagsTests.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/client/RestTemplateExchangeTagsTests.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProviderTests.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsTests.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProviderTests.java
 delete mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsTests.java
 delete mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/SpringBootDependencyInjectionTestExecutionListener.java
 delete mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetrics.java
 delete mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/metrics/package-info.java
 delete mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetricsMissingIntegrationTests.java
 delete mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetricsPresentIntegrationTests.java
 delete mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetricsSpringBootApplication.java
 delete mode 100644 spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/DefaultTestExecutionListenersPostProcessor.java
 delete mode 100644 spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/TestDefaultTestExecutionListenersPostProcessor.java
 delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/DeprecatedConstructorBinding.java
 delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/DeprecatedImmutableMultiConstructorProperties.java
 delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConstructorBinding.java
 delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactorySupplier.java

diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AbstractCompositeHealthContributorConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AbstractCompositeHealthContributorConfiguration.java
index 5a7454b08a50..3b5aeda866da 100644
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AbstractCompositeHealthContributorConfiguration.java
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/AbstractCompositeHealthContributorConfiguration.java
@@ -16,12 +16,9 @@
 
 package org.springframework.boot.actuate.autoconfigure.health;
 
-import java.lang.reflect.Constructor;
 import java.util.Map;
 import java.util.function.Function;
 
-import org.springframework.beans.BeanUtils;
-import org.springframework.core.ResolvableType;
 import org.springframework.util.Assert;
 
 /**
@@ -39,18 +36,6 @@ public abstract class AbstractCompositeHealthContributorConfiguration indicatorFactory;
 
-	/**
-	 * Creates a {@code AbstractCompositeHealthContributorConfiguration} that will use
-	 * reflection to create health indicator instances.
-	 * @deprecated since 3.0.0 in favor of
-	 * {@link #AbstractCompositeHealthContributorConfiguration(Function)}
-	 */
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	protected AbstractCompositeHealthContributorConfiguration() {
-		this.indicatorFactory = new ReflectionIndicatorFactory(
-				ResolvableType.forClass(AbstractCompositeHealthContributorConfiguration.class, getClass()));
-	}
-
 	/**
 	 * Creates a {@code AbstractCompositeHealthContributorConfiguration} that will use the
 	 * given {@code indicatorFactory} to create health indicator instances.
@@ -75,34 +60,4 @@ protected I createIndicator(B bean) {
 		return this.indicatorFactory.apply(bean);
 	}
 
-	private class ReflectionIndicatorFactory implements Function {
-
-		private final Class indicatorType;
-
-		private final Class beanType;
-
-		ReflectionIndicatorFactory(ResolvableType type) {
-			this.indicatorType = type.resolveGeneric(1);
-			this.beanType = type.resolveGeneric(2);
-		}
-
-		@Override
-		public I apply(B bean) {
-			try {
-				return BeanUtils.instantiateClass(getConstructor(), bean);
-			}
-			catch (Exception ex) {
-				throw new IllegalStateException("Unable to create health indicator %s for bean type %s"
-					.formatted(this.indicatorType, this.beanType), ex);
-			}
-
-		}
-
-		@SuppressWarnings("unchecked")
-		private Constructor getConstructor() throws NoSuchMethodException {
-			return (Constructor) this.indicatorType.getDeclaredConstructor(this.beanType);
-		}
-
-	}
-
 }
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/CompositeHealthContributorConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/CompositeHealthContributorConfiguration.java
index 7901e1307552..4b979e94d19e 100644
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/CompositeHealthContributorConfiguration.java
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/CompositeHealthContributorConfiguration.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2022 the original author or authors.
+ * Copyright 2012-2023 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.
@@ -36,18 +36,6 @@
 public abstract class CompositeHealthContributorConfiguration
 		extends AbstractCompositeHealthContributorConfiguration {
 
-	/**
-	 * Creates a {@code CompositeHealthContributorConfiguration} that will use reflection
-	 * to create {@link HealthIndicator} instances.
-	 * @deprecated since 3.0.0 in favor of
-	 * {@link #CompositeHealthContributorConfiguration(Function)}
-	 */
-	@SuppressWarnings("removal")
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	public CompositeHealthContributorConfiguration() {
-		super();
-	}
-
 	/**
 	 * Creates a {@code CompositeHealthContributorConfiguration} that will use the given
 	 * {@code indicatorFactory} to create {@link HealthIndicator} instances.
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/CompositeReactiveHealthContributorConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/CompositeReactiveHealthContributorConfiguration.java
index 57b45ff1a10f..12c4ff22a88e 100644
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/CompositeReactiveHealthContributorConfiguration.java
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/CompositeReactiveHealthContributorConfiguration.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2022 the original author or authors.
+ * Copyright 2012-2023 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.
@@ -36,18 +36,6 @@
 public abstract class CompositeReactiveHealthContributorConfiguration
 		extends AbstractCompositeHealthContributorConfiguration {
 
-	/**
-	 * Creates a {@code CompositeReactiveHealthContributorConfiguration} that will use
-	 * reflection to create {@link ReactiveHealthIndicator} instances.
-	 * @deprecated since 3.0.0 in favor of
-	 * {@link #CompositeReactiveHealthContributorConfiguration(Function)}
-	 */
-	@SuppressWarnings("removal")
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	public CompositeReactiveHealthContributorConfiguration() {
-		super();
-	}
-
 	/**
 	 * Creates a {@code CompositeReactiveHealthContributorConfiguration} that will use the
 	 * given {@code indicatorFactory} to create {@link ReactiveHealthIndicator} instances.
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java
index 771aa7ad46f4..d3994aa32893 100644
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java
@@ -116,8 +116,6 @@ public Server getServer() {
 
 		public static class Client {
 
-			private final ClientRequest request = new ClientRequest();
-
 			/**
 			 * Maximum number of unique URI tag values allowed. After the max number of
 			 * tag values is reached, metrics with additional tag values are denied by
@@ -125,10 +123,6 @@ public static class Client {
 			 */
 			private int maxUriTags = 100;
 
-			public ClientRequest getRequest() {
-				return this.request;
-			}
-
 			public int getMaxUriTags() {
 				return this.maxUriTags;
 			}
@@ -137,32 +131,10 @@ public void setMaxUriTags(int maxUriTags) {
 				this.maxUriTags = maxUriTags;
 			}
 
-			public static class ClientRequest {
-
-				/**
-				 * Name of the metric for sent requests.
-				 */
-				private String metricName = "http.client.requests";
-
-				@Deprecated(since = "3.0.0", forRemoval = true)
-				@DeprecatedConfigurationProperty(replacement = "management.observations.http.client.requests.name")
-				public String getMetricName() {
-					return this.metricName;
-				}
-
-				@Deprecated(since = "3.0.0", forRemoval = true)
-				public void setMetricName(String metricName) {
-					this.metricName = metricName;
-				}
-
-			}
-
 		}
 
 		public static class Server {
 
-			private final ServerRequest request = new ServerRequest();
-
 			/**
 			 * Maximum number of unique URI tag values allowed. After the max number of
 			 * tag values is reached, metrics with additional tag values are denied by
@@ -170,10 +142,6 @@ public static class Server {
 			 */
 			private int maxUriTags = 100;
 
-			public ServerRequest getRequest() {
-				return this.request;
-			}
-
 			public int getMaxUriTags() {
 				return this.maxUriTags;
 			}
@@ -182,27 +150,6 @@ public void setMaxUriTags(int maxUriTags) {
 				this.maxUriTags = maxUriTags;
 			}
 
-			public static class ServerRequest {
-
-				/**
-				 * Name of the metric for received requests.
-				 */
-				private String metricName = "http.server.requests";
-
-				@Deprecated(since = "3.0.0", forRemoval = true)
-				@DeprecatedConfigurationProperty(replacement = "management.observations.http.server.requests.name")
-				public String getMetricName() {
-					return this.metricName;
-				}
-
-				@Deprecated(since = "3.0.0", forRemoval = true)
-				@DeprecatedConfigurationProperty(replacement = "management.observations.http.server.requests.name")
-				public void setMetricName(String metricName) {
-					this.metricName = metricName;
-				}
-
-			}
-
 		}
 
 	}
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfiguration.java
index fdb018f2276d..654a87564e46 100644
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfiguration.java
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfiguration.java
@@ -29,9 +29,9 @@
 
 import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
 import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties;
-import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties.Web.Server;
 import org.springframework.boot.actuate.autoconfigure.metrics.OnlyOnceLoggingDenyMeterFilter;
 import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
+import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@@ -57,13 +57,12 @@
 @ConditionalOnClass({ ResourceConfig.class, MetricsApplicationEventListener.class })
 @ConditionalOnBean({ MeterRegistry.class, ResourceConfig.class })
 @EnableConfigurationProperties(MetricsProperties.class)
-@SuppressWarnings("removal")
 public class JerseyServerMetricsAutoConfiguration {
 
-	private final MetricsProperties properties;
+	private final ObservationProperties observationProperties;
 
-	public JerseyServerMetricsAutoConfiguration(MetricsProperties properties) {
-		this.properties = properties;
+	public JerseyServerMetricsAutoConfiguration(ObservationProperties observationProperties) {
+		this.observationProperties = observationProperties;
 	}
 
 	@Bean
@@ -75,19 +74,19 @@ public DefaultJerseyTagsProvider jerseyTagsProvider() {
 	@Bean
 	public ResourceConfigCustomizer jerseyServerMetricsResourceConfigCustomizer(MeterRegistry meterRegistry,
 			JerseyTagsProvider tagsProvider) {
-		Server server = this.properties.getWeb().getServer();
-		return (config) -> config.register(new MetricsApplicationEventListener(meterRegistry, tagsProvider,
-				server.getRequest().getMetricName(), true, new AnnotationUtilsAnnotationFinder()));
+		String metricName = this.observationProperties.getHttp().getServer().getRequests().getName();
+		return (config) -> config.register(new MetricsApplicationEventListener(meterRegistry, tagsProvider, metricName,
+				true, new AnnotationUtilsAnnotationFinder()));
 	}
 
 	@Bean
 	@Order(0)
-	public MeterFilter jerseyMetricsUriTagFilter() {
-		String metricName = this.properties.getWeb().getServer().getRequest().getMetricName();
+	public MeterFilter jerseyMetricsUriTagFilter(MetricsProperties metricsProperties) {
+		String metricName = this.observationProperties.getHttp().getServer().getRequests().getName();
 		MeterFilter filter = new OnlyOnceLoggingDenyMeterFilter(
 				() -> String.format("Reached the maximum number of URI tags for '%s'.", metricName));
-		return MeterFilter.maximumAllowableTags(metricName, "uri", this.properties.getWeb().getServer().getMaxUriTags(),
-				filter);
+		return MeterFilter.maximumAllowableTags(metricName, "uri",
+				metricsProperties.getWeb().getServer().getMaxUriTags(), filter);
 	}
 
 	/**
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java
index b42ecf3e2122..24d81daa4b1a 100644
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java
@@ -91,10 +91,9 @@ public ClientRequests getRequests() {
 			public static class ClientRequests {
 
 				/**
-				 * Name of the observation for client requests. If empty, will use the
-				 * default "http.client.requests".
+				 * Name of the observation for client requests.
 				 */
-				private String name;
+				private String name = "http.client.requests";
 
 				public String getName() {
 					return this.name;
@@ -125,10 +124,9 @@ public Filter getFilter() {
 			public static class ServerRequests {
 
 				/**
-				 * Name of the observation for server requests. If empty, will use the
-				 * default "http.server.requests".
+				 * Name of the observation for server requests.
 				 */
-				private String name;
+				private String name = "http.server.requests";
 
 				public String getName() {
 					return this.name;
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/graphql/GraphQlObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/graphql/GraphQlObservationAutoConfiguration.java
index 5c447db00fba..86b5ed0aee35 100644
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/graphql/GraphQlObservationAutoConfiguration.java
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/graphql/GraphQlObservationAutoConfiguration.java
@@ -43,7 +43,6 @@
 @AutoConfiguration(after = ObservationAutoConfiguration.class)
 @ConditionalOnBean(ObservationRegistry.class)
 @ConditionalOnClass({ GraphQL.class, GraphQlSource.class, Observation.class })
-@SuppressWarnings("removal")
 public class GraphQlObservationAutoConfiguration {
 
 	@Bean
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/ClientHttpObservationConventionAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/ClientHttpObservationConventionAdapter.java
deleted file mode 100644
index 87205527f5e2..000000000000
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/ClientHttpObservationConventionAdapter.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.autoconfigure.observation.web.client;
-
-import io.micrometer.common.KeyValues;
-import io.micrometer.core.instrument.Tag;
-
-import org.springframework.boot.actuate.metrics.web.client.RestTemplateExchangeTagsProvider;
-import org.springframework.http.client.observation.ClientRequestObservationContext;
-import org.springframework.http.client.observation.ClientRequestObservationConvention;
-
-/**
- * Adapter class that applies {@link RestTemplateExchangeTagsProvider} tags as a
- * {@link ClientRequestObservationConvention}.
- *
- * @author Brian Clozel
- */
-@SuppressWarnings({ "removal" })
-class ClientHttpObservationConventionAdapter implements ClientRequestObservationConvention {
-
-	private final String metricName;
-
-	private final RestTemplateExchangeTagsProvider tagsProvider;
-
-	ClientHttpObservationConventionAdapter(String metricName, RestTemplateExchangeTagsProvider tagsProvider) {
-		this.metricName = metricName;
-		this.tagsProvider = tagsProvider;
-	}
-
-	@Override
-	@SuppressWarnings("deprecation")
-	public KeyValues getLowCardinalityKeyValues(ClientRequestObservationContext context) {
-		Iterable tags = this.tagsProvider.getTags(context.getUriTemplate(), context.getCarrier(),
-				context.getResponse());
-		return KeyValues.of(tags, Tag::getKey, Tag::getValue);
-	}
-
-	@Override
-	public KeyValues getHighCardinalityKeyValues(ClientRequestObservationContext context) {
-		return KeyValues.empty();
-	}
-
-	@Override
-	public String getName() {
-		return this.metricName;
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/ClientObservationConventionAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/ClientObservationConventionAdapter.java
deleted file mode 100644
index 0f3230a5bf8a..000000000000
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/ClientObservationConventionAdapter.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.autoconfigure.observation.web.client;
-
-import io.micrometer.common.KeyValues;
-import io.micrometer.core.instrument.Tag;
-import io.micrometer.observation.Observation;
-
-import org.springframework.boot.actuate.metrics.web.reactive.client.WebClientExchangeTagsProvider;
-import org.springframework.core.Conventions;
-import org.springframework.web.reactive.function.client.ClientRequest;
-import org.springframework.web.reactive.function.client.ClientRequestObservationContext;
-import org.springframework.web.reactive.function.client.ClientRequestObservationConvention;
-import org.springframework.web.reactive.function.client.WebClient;
-
-/**
- * Adapter class that applies {@link WebClientExchangeTagsProvider} tags as a
- * {@link ClientRequestObservationConvention}.
- *
- * @author Brian Clozel
- */
-@SuppressWarnings("removal")
-class ClientObservationConventionAdapter implements ClientRequestObservationConvention {
-
-	private static final String URI_TEMPLATE_ATTRIBUTE = Conventions.getQualifiedAttributeName(WebClient.class,
-			"uriTemplate");
-
-	private final String metricName;
-
-	private final WebClientExchangeTagsProvider tagsProvider;
-
-	ClientObservationConventionAdapter(String metricName, WebClientExchangeTagsProvider tagsProvider) {
-		this.metricName = metricName;
-		this.tagsProvider = tagsProvider;
-	}
-
-	@Override
-	public boolean supportsContext(Observation.Context context) {
-		return context instanceof ClientRequestObservationContext;
-	}
-
-	@Override
-	public KeyValues getLowCardinalityKeyValues(ClientRequestObservationContext context) {
-		ClientRequest request = context.getRequest();
-		if (request == null) {
-			request = context.getCarrier().attribute(URI_TEMPLATE_ATTRIBUTE, context.getUriTemplate()).build();
-		}
-		Iterable tags = this.tagsProvider.tags(request, context.getResponse(), context.getError());
-		return KeyValues.of(tags, Tag::getKey, Tag::getValue);
-	}
-
-	@Override
-	public KeyValues getHighCardinalityKeyValues(ClientRequestObservationContext context) {
-		return KeyValues.empty();
-	}
-
-	@Override
-	public String getName() {
-		return this.metricName;
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/HttpClientObservationsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/HttpClientObservationsAutoConfiguration.java
index 23323d25238c..563a805e654c 100644
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/HttpClientObservationsAutoConfiguration.java
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/HttpClientObservationsAutoConfiguration.java
@@ -65,13 +65,10 @@ static class MeterFilterConfiguration {
 
 		@Bean
 		@Order(0)
-		@SuppressWarnings("removal")
 		MeterFilter metricsHttpClientUriTagFilter(ObservationProperties observationProperties,
 				MetricsProperties metricsProperties) {
 			Client clientProperties = metricsProperties.getWeb().getClient();
-			String metricName = clientProperties.getRequest().getMetricName();
-			String observationName = observationProperties.getHttp().getClient().getRequests().getName();
-			String name = (observationName != null) ? observationName : metricName;
+			String name = observationProperties.getHttp().getClient().getRequests().getName();
 			MeterFilter denyFilter = new OnlyOnceLoggingDenyMeterFilter(
 					() -> "Reached the maximum number of URI tags for '%s'. Are you using 'uriVariables'?"
 						.formatted(name));
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfiguration.java
index 0f62f2849b19..a1d2a152de56 100644
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfiguration.java
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfiguration.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2022 the original author or authors.
+ * Copyright 2012-2023 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.
@@ -22,7 +22,6 @@
 import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties;
 import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties;
 import org.springframework.boot.actuate.metrics.web.client.ObservationRestTemplateCustomizer;
-import org.springframework.boot.actuate.metrics.web.client.RestTemplateExchangeTagsProvider;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
 import org.springframework.boot.web.client.RestTemplateBuilder;
@@ -40,39 +39,16 @@
 @Configuration(proxyBeanMethods = false)
 @ConditionalOnClass(RestTemplate.class)
 @ConditionalOnBean(RestTemplateBuilder.class)
-@SuppressWarnings("removal")
 class RestTemplateObservationConfiguration {
 
 	@Bean
 	ObservationRestTemplateCustomizer observationRestTemplateCustomizer(ObservationRegistry observationRegistry,
 			ObjectProvider customConvention,
-			ObservationProperties observationProperties, MetricsProperties metricsProperties,
-			ObjectProvider optionalTagsProvider) {
-		String name = observationName(observationProperties, metricsProperties);
-		ClientRequestObservationConvention observationConvention = createConvention(customConvention.getIfAvailable(),
-				name, optionalTagsProvider.getIfAvailable());
+			ObservationProperties observationProperties, MetricsProperties metricsProperties) {
+		String name = observationProperties.getHttp().getClient().getRequests().getName();
+		ClientRequestObservationConvention observationConvention = customConvention
+			.getIfAvailable(() -> new DefaultClientRequestObservationConvention(name));
 		return new ObservationRestTemplateCustomizer(observationRegistry, observationConvention);
 	}
 
-	private static String observationName(ObservationProperties observationProperties,
-			MetricsProperties metricsProperties) {
-		String metricName = metricsProperties.getWeb().getClient().getRequest().getMetricName();
-		String observationName = observationProperties.getHttp().getClient().getRequests().getName();
-		return (observationName != null) ? observationName : metricName;
-	}
-
-	private static ClientRequestObservationConvention createConvention(
-			ClientRequestObservationConvention customConvention, String name,
-			RestTemplateExchangeTagsProvider tagsProvider) {
-		if (customConvention != null) {
-			return customConvention;
-		}
-		else if (tagsProvider != null) {
-			return new ClientHttpObservationConventionAdapter(name, tagsProvider);
-		}
-		else {
-			return new DefaultClientRequestObservationConvention(name);
-		}
-	}
-
 }
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/WebClientObservationConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/WebClientObservationConfiguration.java
index ce531912e1dc..2df9c4bf9104 100644
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/WebClientObservationConfiguration.java
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/WebClientObservationConfiguration.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2022 the original author or authors.
+ * Copyright 2012-2023 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.
@@ -22,7 +22,6 @@
 import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties;
 import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties;
 import org.springframework.boot.actuate.metrics.web.reactive.client.ObservationWebClientCustomizer;
-import org.springframework.boot.actuate.metrics.web.reactive.client.WebClientExchangeTagsProvider;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -37,39 +36,16 @@
  */
 @Configuration(proxyBeanMethods = false)
 @ConditionalOnClass(WebClient.class)
-@SuppressWarnings("removal")
 class WebClientObservationConfiguration {
 
 	@Bean
 	ObservationWebClientCustomizer observationWebClientCustomizer(ObservationRegistry observationRegistry,
 			ObjectProvider customConvention,
-			ObservationProperties observationProperties, ObjectProvider tagsProvider,
-			MetricsProperties metricsProperties) {
-		String name = observationName(observationProperties, metricsProperties);
-		ClientRequestObservationConvention observationConvention = createConvention(customConvention.getIfAvailable(),
-				tagsProvider.getIfAvailable(), name);
+			ObservationProperties observationProperties, MetricsProperties metricsProperties) {
+		String name = observationProperties.getHttp().getClient().getRequests().getName();
+		ClientRequestObservationConvention observationConvention = customConvention
+			.getIfAvailable(() -> new DefaultClientRequestObservationConvention(name));
 		return new ObservationWebClientCustomizer(observationRegistry, observationConvention);
 	}
 
-	private static ClientRequestObservationConvention createConvention(
-			ClientRequestObservationConvention customConvention, WebClientExchangeTagsProvider tagsProvider,
-			String name) {
-		if (customConvention != null) {
-			return customConvention;
-		}
-		else if (tagsProvider != null) {
-			return new ClientObservationConventionAdapter(name, tagsProvider);
-		}
-		else {
-			return new DefaultClientRequestObservationConvention(name);
-		}
-	}
-
-	private static String observationName(ObservationProperties observationProperties,
-			MetricsProperties metricsProperties) {
-		String metricName = metricsProperties.getWeb().getClient().getRequest().getMetricName();
-		String observationName = observationProperties.getHttp().getClient().getRequests().getName();
-		return (observationName != null) ? observationName : metricName;
-	}
-
 }
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/ServerRequestObservationConventionAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/ServerRequestObservationConventionAdapter.java
deleted file mode 100644
index 43689a962a05..000000000000
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/ServerRequestObservationConventionAdapter.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.autoconfigure.observation.web.reactive;
-
-import java.util.List;
-
-import io.micrometer.common.KeyValues;
-import io.micrometer.core.instrument.Tag;
-
-import org.springframework.boot.actuate.metrics.web.reactive.server.DefaultWebFluxTagsProvider;
-import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsContributor;
-import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsProvider;
-import org.springframework.http.codec.ServerCodecConfigurer;
-import org.springframework.http.server.reactive.observation.ServerRequestObservationContext;
-import org.springframework.http.server.reactive.observation.ServerRequestObservationConvention;
-import org.springframework.web.server.adapter.DefaultServerWebExchange;
-import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
-import org.springframework.web.server.i18n.LocaleContextResolver;
-import org.springframework.web.server.session.DefaultWebSessionManager;
-import org.springframework.web.server.session.WebSessionManager;
-
-/**
- * Adapter class that applies {@link WebFluxTagsProvider} tags as a
- * {@link ServerRequestObservationConvention}.
- *
- * @author Brian Clozel
- */
-@SuppressWarnings("removal")
-@Deprecated(since = "3.0.0", forRemoval = true)
-class ServerRequestObservationConventionAdapter implements ServerRequestObservationConvention {
-
-	private final WebSessionManager webSessionManager = new DefaultWebSessionManager();
-
-	private final ServerCodecConfigurer serverCodecConfigurer = ServerCodecConfigurer.create();
-
-	private final LocaleContextResolver localeContextResolver = new AcceptHeaderLocaleContextResolver();
-
-	private final String name;
-
-	private final WebFluxTagsProvider tagsProvider;
-
-	ServerRequestObservationConventionAdapter(String name, WebFluxTagsProvider tagsProvider) {
-		this.name = name;
-		this.tagsProvider = tagsProvider;
-	}
-
-	ServerRequestObservationConventionAdapter(String name, List contributors) {
-		this(name, new DefaultWebFluxTagsProvider(contributors));
-	}
-
-	@Override
-	public String getName() {
-		return this.name;
-	}
-
-	@Override
-	public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) {
-		DefaultServerWebExchange serverWebExchange = new DefaultServerWebExchange(context.getCarrier(),
-				context.getResponse(), this.webSessionManager, this.serverCodecConfigurer, this.localeContextResolver);
-		serverWebExchange.getAttributes().putAll(context.getAttributes());
-		Iterable tags = this.tagsProvider.httpRequestTags(serverWebExchange, context.getError());
-		return KeyValues.of(tags, Tag::getKey, Tag::getValue);
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java
index 17c2a9d7f274..457d2cdf3937 100644
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java
@@ -16,8 +16,6 @@
 
 package org.springframework.boot.actuate.autoconfigure.observation.web.reactive;
 
-import java.util.List;
-
 import io.micrometer.core.instrument.MeterRegistry;
 import io.micrometer.core.instrument.config.MeterFilter;
 import io.micrometer.observation.Observation;
@@ -31,8 +29,6 @@
 import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
 import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
 import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties;
-import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsContributor;
-import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsProvider;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@@ -66,48 +62,23 @@
 @SuppressWarnings("removal")
 public class WebFluxObservationAutoConfiguration {
 
-	private final MetricsProperties metricsProperties;
-
 	private final ObservationProperties observationProperties;
 
-	public WebFluxObservationAutoConfiguration(MetricsProperties metricsProperties,
-			ObservationProperties observationProperties) {
-		this.metricsProperties = metricsProperties;
+	public WebFluxObservationAutoConfiguration(ObservationProperties observationProperties) {
 		this.observationProperties = observationProperties;
 	}
 
 	@Bean
 	@ConditionalOnMissingBean(ServerHttpObservationFilter.class)
 	public OrderedServerHttpObservationFilter webfluxObservationFilter(ObservationRegistry registry,
-			ObjectProvider customConvention,
-			ObjectProvider tagConfigurer,
-			ObjectProvider contributorsProvider) {
-		String observationName = this.observationProperties.getHttp().getServer().getRequests().getName();
-		String metricName = this.metricsProperties.getWeb().getServer().getRequest().getMetricName();
-		String name = (observationName != null) ? observationName : metricName;
-		WebFluxTagsProvider tagsProvider = tagConfigurer.getIfAvailable();
-		List tagsContributors = contributorsProvider.orderedStream().toList();
-		ServerRequestObservationConvention convention = createConvention(customConvention.getIfAvailable(), name,
-				tagsProvider, tagsContributors);
+			ObjectProvider customConvention) {
+		String name = this.observationProperties.getHttp().getServer().getRequests().getName();
+		ServerRequestObservationConvention convention = customConvention
+			.getIfAvailable(() -> new DefaultServerRequestObservationConvention(name));
 		int order = this.observationProperties.getHttp().getServer().getFilter().getOrder();
 		return new OrderedServerHttpObservationFilter(registry, convention, order);
 	}
 
-	private static ServerRequestObservationConvention createConvention(
-			ServerRequestObservationConvention customConvention, String name, WebFluxTagsProvider tagsProvider,
-			List tagsContributors) {
-		if (customConvention != null) {
-			return customConvention;
-		}
-		if (tagsProvider != null) {
-			return new ServerRequestObservationConventionAdapter(name, tagsProvider);
-		}
-		if (!tagsContributors.isEmpty()) {
-			return new ServerRequestObservationConventionAdapter(name, tagsContributors);
-		}
-		return new DefaultServerRequestObservationConvention(name);
-	}
-
 	@Configuration(proxyBeanMethods = false)
 	@ConditionalOnClass(MeterRegistry.class)
 	@ConditionalOnBean(MeterRegistry.class)
@@ -117,9 +88,7 @@ static class MeterFilterConfiguration {
 		@Order(0)
 		MeterFilter metricsHttpServerUriTagFilter(MetricsProperties metricsProperties,
 				ObservationProperties observationProperties) {
-			String observationName = observationProperties.getHttp().getServer().getRequests().getName();
-			String name = (observationName != null) ? observationName
-					: metricsProperties.getWeb().getServer().getRequest().getMetricName();
+			String name = observationProperties.getHttp().getServer().getRequests().getName();
 			MeterFilter filter = new OnlyOnceLoggingDenyMeterFilter(
 					() -> "Reached the maximum number of URI tags for '%s'.".formatted(name));
 			return MeterFilter.maximumAllowableTags(name, "uri", metricsProperties.getWeb().getServer().getMaxUriTags(),
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/ServerRequestObservationConventionAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/ServerRequestObservationConventionAdapter.java
deleted file mode 100644
index df52cbca7407..000000000000
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/ServerRequestObservationConventionAdapter.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.autoconfigure.observation.web.servlet;
-
-import java.util.List;
-
-import io.micrometer.common.KeyValues;
-import io.micrometer.core.instrument.Tag;
-import io.micrometer.observation.Observation;
-
-import org.springframework.boot.actuate.metrics.web.servlet.DefaultWebMvcTagsProvider;
-import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsContributor;
-import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsProvider;
-import org.springframework.http.server.observation.ServerRequestObservationContext;
-import org.springframework.http.server.observation.ServerRequestObservationConvention;
-import org.springframework.util.Assert;
-import org.springframework.web.servlet.HandlerMapping;
-
-/**
- * Adapter class that applies {@link WebMvcTagsProvider} tags as a
- * {@link ServerRequestObservationConvention}.
- *
- * @author Brian Clozel
- */
-@SuppressWarnings("removal")
-@Deprecated(since = "3.0.0", forRemoval = true)
-class ServerRequestObservationConventionAdapter implements ServerRequestObservationConvention {
-
-	private final String observationName;
-
-	private final WebMvcTagsProvider tagsProvider;
-
-	ServerRequestObservationConventionAdapter(String observationName, WebMvcTagsProvider tagsProvider,
-			List contributors) {
-		Assert.state((tagsProvider != null) || (contributors != null),
-				"adapter should adapt to a WebMvcTagsProvider or a list of contributors");
-		this.observationName = observationName;
-		this.tagsProvider = (tagsProvider != null) ? tagsProvider : new DefaultWebMvcTagsProvider(contributors);
-	}
-
-	@Override
-	public String getName() {
-		return this.observationName;
-	}
-
-	@Override
-	public boolean supportsContext(Observation.Context context) {
-		return context instanceof ServerRequestObservationContext;
-	}
-
-	@Override
-	public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) {
-		Iterable tags = this.tagsProvider.getTags(context.getCarrier(), context.getResponse(), getHandler(context),
-				context.getError());
-		return KeyValues.of(tags, Tag::getKey, Tag::getValue);
-	}
-
-	private Object getHandler(ServerRequestObservationContext context) {
-		return context.getCarrier().getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE);
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java
index dba50c22f3d6..e6f1c487def1 100644
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java
@@ -16,8 +16,6 @@
 
 package org.springframework.boot.actuate.autoconfigure.observation.web.servlet;
 
-import java.util.List;
-
 import io.micrometer.core.instrument.MeterRegistry;
 import io.micrometer.core.instrument.config.MeterFilter;
 import io.micrometer.observation.Observation;
@@ -32,8 +30,6 @@
 import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
 import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
 import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties;
-import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsContributor;
-import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsProvider;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@@ -66,56 +62,23 @@
 @ConditionalOnClass({ DispatcherServlet.class, Observation.class })
 @ConditionalOnBean(ObservationRegistry.class)
 @EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class })
-@SuppressWarnings("removal")
 public class WebMvcObservationAutoConfiguration {
 
-	private final MetricsProperties metricsProperties;
-
-	private final ObservationProperties observationProperties;
-
-	public WebMvcObservationAutoConfiguration(ObservationProperties observationProperties,
-			MetricsProperties metricsProperties) {
-		this.observationProperties = observationProperties;
-		this.metricsProperties = metricsProperties;
-	}
-
 	@Bean
 	@ConditionalOnMissingFilterBean
 	public FilterRegistrationBean webMvcObservationFilter(ObservationRegistry registry,
 			ObjectProvider customConvention,
-			ObjectProvider customTagsProvider,
-			ObjectProvider contributorsProvider) {
-		String name = httpRequestsMetricName(this.observationProperties, this.metricsProperties);
-		ServerRequestObservationConvention convention = createConvention(customConvention.getIfAvailable(), name,
-				customTagsProvider.getIfAvailable(), contributorsProvider.orderedStream().toList());
+			ObservationProperties observationProperties) {
+		String name = observationProperties.getHttp().getServer().getRequests().getName();
+		ServerRequestObservationConvention convention = customConvention
+			.getIfAvailable(() -> new DefaultServerRequestObservationConvention(name));
 		ServerHttpObservationFilter filter = new ServerHttpObservationFilter(registry, convention);
 		FilterRegistrationBean registration = new FilterRegistrationBean<>(filter);
-		registration.setOrder(this.observationProperties.getHttp().getServer().getFilter().getOrder());
+		registration.setOrder(observationProperties.getHttp().getServer().getFilter().getOrder());
 		registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
 		return registration;
 	}
 
-	private static ServerRequestObservationConvention createConvention(
-			ServerRequestObservationConvention customConvention, String name, WebMvcTagsProvider tagsProvider,
-			List contributors) {
-		if (customConvention != null) {
-			return customConvention;
-		}
-		else if (tagsProvider != null || contributors.size() > 0) {
-			return new ServerRequestObservationConventionAdapter(name, tagsProvider, contributors);
-		}
-		else {
-			return new DefaultServerRequestObservationConvention(name);
-		}
-	}
-
-	private static String httpRequestsMetricName(ObservationProperties observationProperties,
-			MetricsProperties metricsProperties) {
-		String observationName = observationProperties.getHttp().getServer().getRequests().getName();
-		return (observationName != null) ? observationName
-				: metricsProperties.getWeb().getServer().getRequest().getMetricName();
-	}
-
 	@Configuration(proxyBeanMethods = false)
 	@ConditionalOnClass(MeterRegistry.class)
 	@ConditionalOnBean(MeterRegistry.class)
@@ -123,9 +86,9 @@ static class MeterFilterConfiguration {
 
 		@Bean
 		@Order(0)
-		MeterFilter metricsHttpServerUriTagFilter(MetricsProperties metricsProperties,
-				ObservationProperties observationProperties) {
-			String name = httpRequestsMetricName(observationProperties, metricsProperties);
+		MeterFilter metricsHttpServerUriTagFilter(ObservationProperties observationProperties,
+				MetricsProperties metricsProperties) {
+			String name = observationProperties.getHttp().getServer().getRequests().getName();
 			MeterFilter filter = new OnlyOnceLoggingDenyMeterFilter(
 					() -> String.format("Reached the maximum number of URI tags for '%s'.", name));
 			return MeterFilter.maximumAllowableTags(name, "uri", metricsProperties.getWeb().getServer().getMaxUriTags(),
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json
index 9fb2160d5b2a..bd717a474b56 100644
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -1980,11 +1980,19 @@
         "reason": "Should be applied at the ObservationRegistry level."
       }
     },
+    {
+      "name": "management.metrics.web.client.request.metric-name",
+      "type": "java.lang.String",
+      "deprecation": {
+        "replacement": "management.observations.http.client.requests.name",
+        "level": "error"
+      }
+    },
     {
       "name": "management.metrics.web.client.requests-metric-name",
       "type": "java.lang.String",
       "deprecation": {
-        "replacement": "management.metrics.web.client.request.metric-name",
+        "replacement": "management.observations.http.client.requests.name",
         "level": "error"
       }
     },
@@ -2030,11 +2038,19 @@
         "reason": "Not needed anymore, direct instrumentation in Spring MVC."
       }
     },
+    {
+      "name": "management.metrics.web.server.request.metric-name",
+      "type": "java.lang.String",
+      "deprecation": {
+        "replacement": "management.observations.http.server.requests.name",
+        "level": "error"
+      }
+    },
     {
       "name": "management.metrics.web.server.requests-metric-name",
       "type": "java.lang.String",
       "deprecation": {
-        "replacement": "management.metrics.web.server.request.metric-name",
+        "replacement": "management.observations.http.server.requests.name",
         "level": "error"
       }
     },
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/CompositeHealthContributorConfigurationReflectionTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/CompositeHealthContributorConfigurationReflectionTests.java
deleted file mode 100644
index c6100f971b7b..000000000000
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/CompositeHealthContributorConfigurationReflectionTests.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.autoconfigure.health;
-
-import org.springframework.boot.actuate.autoconfigure.health.CompositeHealthContributorConfigurationReflectionTests.TestHealthIndicator;
-import org.springframework.boot.actuate.health.AbstractHealthIndicator;
-import org.springframework.boot.actuate.health.Health.Builder;
-import org.springframework.boot.actuate.health.HealthContributor;
-
-/**
- * Tests for {@link CompositeHealthContributorConfiguration} using reflection to create
- * indicator instances.
- *
- * @author Phillip Webb
- */
-@SuppressWarnings("removal")
-@Deprecated(since = "3.0.0", forRemoval = true)
-class CompositeHealthContributorConfigurationReflectionTests
-		extends AbstractCompositeHealthContributorConfigurationTests {
-
-	@Override
-	protected AbstractCompositeHealthContributorConfiguration newComposite() {
-		return new ReflectiveTestCompositeHealthContributorConfiguration();
-	}
-
-	static class ReflectiveTestCompositeHealthContributorConfiguration
-			extends CompositeHealthContributorConfiguration {
-
-	}
-
-	static class TestHealthIndicator extends AbstractHealthIndicator {
-
-		TestHealthIndicator(TestBean testBean) {
-		}
-
-		@Override
-		protected void doHealthCheck(Builder builder) throws Exception {
-			builder.up();
-		}
-
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/CompositeReactiveHealthContributorConfigurationReflectionTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/CompositeReactiveHealthContributorConfigurationReflectionTests.java
deleted file mode 100644
index 183a3c7bd3a7..000000000000
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/health/CompositeReactiveHealthContributorConfigurationReflectionTests.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.autoconfigure.health;
-
-import reactor.core.publisher.Mono;
-
-import org.springframework.boot.actuate.autoconfigure.health.CompositeReactiveHealthContributorConfigurationReflectionTests.TestReactiveHealthIndicator;
-import org.springframework.boot.actuate.health.AbstractReactiveHealthIndicator;
-import org.springframework.boot.actuate.health.Health;
-import org.springframework.boot.actuate.health.Health.Builder;
-import org.springframework.boot.actuate.health.ReactiveHealthContributor;
-
-/**
- * Tests for {@link CompositeReactiveHealthContributorConfiguration} using reflection to
- * create indicator instances.
- *
- * @author Phillip Webb
- */
-@SuppressWarnings("removal")
-@Deprecated(since = "3.0.0", forRemoval = true)
-class CompositeReactiveHealthContributorConfigurationReflectionTests extends
-		AbstractCompositeHealthContributorConfigurationTests {
-
-	@Override
-	protected AbstractCompositeHealthContributorConfiguration newComposite() {
-		return new TestCompositeReactiveHealthContributorConfiguration();
-	}
-
-	static class TestCompositeReactiveHealthContributorConfiguration
-			extends CompositeReactiveHealthContributorConfiguration {
-
-	}
-
-	static class TestReactiveHealthIndicator extends AbstractReactiveHealthIndicator {
-
-		TestReactiveHealthIndicator(TestBean testBean) {
-		}
-
-		@Override
-		protected Mono doHealthCheck(Builder builder) {
-			return Mono.just(builder.up().build());
-		}
-
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/ClientHttpObservationConventionAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/ClientHttpObservationConventionAdapterTests.java
deleted file mode 100644
index 814e71a4c720..000000000000
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/ClientHttpObservationConventionAdapterTests.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.autoconfigure.observation.web.client;
-
-import java.net.URI;
-
-import io.micrometer.common.KeyValue;
-import io.micrometer.observation.Observation;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import org.springframework.boot.actuate.metrics.web.client.DefaultRestTemplateExchangeTagsProvider;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.client.ClientHttpRequest;
-import org.springframework.http.client.ClientHttpResponse;
-import org.springframework.http.client.observation.ClientRequestObservationContext;
-import org.springframework.mock.http.client.MockClientHttpRequest;
-import org.springframework.mock.http.client.MockClientHttpResponse;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * Tests for {@link ClientHttpObservationConventionAdapter}.
- *
- * @author Brian Clozel
- */
-@SuppressWarnings({ "deprecation", "removal" })
-class ClientHttpObservationConventionAdapterTests {
-
-	private static final String TEST_METRIC_NAME = "test.metric.name";
-
-	private final ClientHttpObservationConventionAdapter convention = new ClientHttpObservationConventionAdapter(
-			TEST_METRIC_NAME, new DefaultRestTemplateExchangeTagsProvider());
-
-	private final ClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, URI.create("/resource/test"));
-
-	private final ClientHttpResponse response = new MockClientHttpResponse("foo".getBytes(), HttpStatus.OK);
-
-	private ClientRequestObservationContext context;
-
-	@BeforeEach
-	void setup() {
-		this.context = new ClientRequestObservationContext(this.request);
-		this.context.setResponse(this.response);
-		this.context.setUriTemplate("/resource/{name}");
-	}
-
-	@Test
-	void shouldUseConfiguredName() {
-		assertThat(this.convention.getName()).isEqualTo(TEST_METRIC_NAME);
-	}
-
-	@Test
-	void shouldOnlySupportClientHttpObservationContext() {
-		assertThat(this.convention.supportsContext(this.context)).isTrue();
-		assertThat(this.convention.supportsContext(new OtherContext())).isFalse();
-	}
-
-	@Test
-	void shouldPushTagsAsLowCardinalityKeyValues() {
-		assertThat(this.convention.getLowCardinalityKeyValues(this.context)).contains(KeyValue.of("status", "200"),
-				KeyValue.of("outcome", "SUCCESS"), KeyValue.of("uri", "/resource/{name}"),
-				KeyValue.of("method", "GET"));
-	}
-
-	@Test
-	void shouldNotPushAnyHighCardinalityKeyValue() {
-		assertThat(this.convention.getHighCardinalityKeyValues(this.context)).isEmpty();
-	}
-
-	static class OtherContext extends Observation.Context {
-
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/ClientObservationConventionAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/ClientObservationConventionAdapterTests.java
deleted file mode 100644
index cd73e29efba0..000000000000
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/ClientObservationConventionAdapterTests.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright 2012-2023 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.actuate.autoconfigure.observation.web.client;
-
-import java.net.URI;
-
-import io.micrometer.common.KeyValue;
-import io.micrometer.observation.Observation;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import org.springframework.boot.actuate.metrics.web.reactive.client.DefaultWebClientExchangeTagsProvider;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.HttpStatus;
-import org.springframework.web.reactive.function.client.ClientRequest;
-import org.springframework.web.reactive.function.client.ClientRequestObservationContext;
-import org.springframework.web.reactive.function.client.ClientResponse;
-import org.springframework.web.reactive.function.client.WebClient;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * Tests for {@link ClientObservationConventionAdapter}.
- *
- * @author Brian Clozel
- */
-@SuppressWarnings({ "deprecation", "removal" })
-class ClientObservationConventionAdapterTests {
-
-	private static final String TEST_METRIC_NAME = "test.metric.name";
-
-	private final ClientObservationConventionAdapter convention = new ClientObservationConventionAdapter(
-			TEST_METRIC_NAME, new DefaultWebClientExchangeTagsProvider());
-
-	private final ClientRequest.Builder requestBuilder = ClientRequest
-		.create(HttpMethod.GET, URI.create("/resource/test"))
-		.attribute(WebClient.class.getName() + ".uriTemplate", "/resource/{name}");
-
-	private final ClientResponse response = ClientResponse.create(HttpStatus.OK).body("foo").build();
-
-	private ClientRequestObservationContext context;
-
-	@BeforeEach
-	void setup() {
-		this.context = new ClientRequestObservationContext();
-		this.context.setCarrier(this.requestBuilder);
-		this.context.setResponse(this.response);
-		this.context.setUriTemplate("/resource/{name}");
-	}
-
-	@Test
-	void shouldUseConfiguredName() {
-		assertThat(this.convention.getName()).isEqualTo(TEST_METRIC_NAME);
-	}
-
-	@Test
-	void shouldOnlySupportClientObservationContext() {
-		assertThat(this.convention.supportsContext(this.context)).isTrue();
-		assertThat(this.convention.supportsContext(new OtherContext())).isFalse();
-	}
-
-	@Test
-	void shouldPushTagsAsLowCardinalityKeyValues() {
-		this.context.setRequest(this.requestBuilder.build());
-		assertThat(this.convention.getLowCardinalityKeyValues(this.context)).contains(KeyValue.of("status", "200"),
-				KeyValue.of("outcome", "SUCCESS"), KeyValue.of("uri", "/resource/{name}"),
-				KeyValue.of("method", "GET"));
-	}
-
-	@Test
-	void doesNotFailWithEmptyRequest() {
-		assertThat(this.convention.getLowCardinalityKeyValues(this.context)).contains(KeyValue.of("status", "200"),
-				KeyValue.of("outcome", "SUCCESS"), KeyValue.of("uri", "/resource/{name}"),
-				KeyValue.of("method", "GET"));
-	}
-
-	@Test
-	void shouldNotPushAnyHighCardinalityKeyValue() {
-		assertThat(this.convention.getHighCardinalityKeyValues(this.context)).isEmpty();
-	}
-
-	static class OtherContext extends Observation.Context {
-
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfigurationTests.java
index 6116649a14dc..92b4367cad0c 100644
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfigurationTests.java
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfigurationTests.java
@@ -18,8 +18,6 @@
 
 import io.micrometer.common.KeyValues;
 import io.micrometer.core.instrument.MeterRegistry;
-import io.micrometer.core.instrument.Tag;
-import io.micrometer.core.instrument.Tags;
 import io.micrometer.observation.ObservationRegistry;
 import io.micrometer.observation.tck.TestObservationRegistry;
 import io.micrometer.observation.tck.TestObservationRegistryAssert;
@@ -28,9 +26,7 @@
 
 import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun;
 import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
-import org.springframework.boot.actuate.metrics.web.client.DefaultRestTemplateExchangeTagsProvider;
 import org.springframework.boot.actuate.metrics.web.client.ObservationRestTemplateCustomizer;
-import org.springframework.boot.actuate.metrics.web.client.RestTemplateExchangeTagsProvider;
 import org.springframework.boot.autoconfigure.AutoConfigurations;
 import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
 import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
@@ -40,9 +36,7 @@
 import org.springframework.boot.web.client.RestTemplateBuilder;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
-import org.springframework.http.HttpRequest;
 import org.springframework.http.HttpStatus;
-import org.springframework.http.client.ClientHttpResponse;
 import org.springframework.http.client.observation.ClientRequestObservationContext;
 import org.springframework.http.client.observation.DefaultClientRequestObservationConvention;
 import org.springframework.test.web.client.MockRestServiceServer;
@@ -58,7 +52,6 @@
  * @author Brian Clozel
  */
 @ExtendWith(OutputCaptureExtension.class)
-@SuppressWarnings("removal")
 class RestTemplateObservationConfigurationTests {
 
 	private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
@@ -68,8 +61,7 @@ class RestTemplateObservationConfigurationTests {
 
 	@Test
 	void contributesCustomizerBean() {
-		this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ObservationRestTemplateCustomizer.class)
-			.doesNotHaveBean(DefaultRestTemplateExchangeTagsProvider.class));
+		this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ObservationRestTemplateCustomizer.class));
 	}
 
 	@Test
@@ -96,32 +88,6 @@ void restTemplateCreatedWithBuilderUsesCustomConventionName() {
 			});
 	}
 
-	@Test
-	void restTemplateCreatedWithBuilderUsesCustomMetricName() {
-		final String metricName = "test.metric.name";
-		this.contextRunner.withPropertyValues("management.metrics.web.client.request.metric-name=" + metricName)
-			.run((context) -> {
-				RestTemplate restTemplate = buildRestTemplate(context);
-				restTemplate.getForEntity("/projects/{project}", Void.class, "spring-boot");
-				TestObservationRegistry registry = context.getBean(TestObservationRegistry.class);
-				TestObservationRegistryAssert.assertThat(registry)
-					.hasObservationWithNameEqualToIgnoringCase(metricName);
-			});
-	}
-
-	@Test
-	void restTemplateCreatedWithBuilderUsesCustomTagsProvider() {
-		this.contextRunner.withUserConfiguration(CustomTagsConfiguration.class).run((context) -> {
-			RestTemplate restTemplate = buildRestTemplate(context);
-			restTemplate.getForEntity("/projects/{project}", Void.class, "spring-boot");
-			TestObservationRegistry registry = context.getBean(TestObservationRegistry.class);
-			TestObservationRegistryAssert.assertThat(registry)
-				.hasObservationWithNameEqualTo("http.client.requests")
-				.that()
-				.hasLowCardinalityKeyValue("project", "spring-boot");
-		});
-	}
-
 	@Test
 	void restTemplateCreatedWithBuilderUsesCustomConvention() {
 		this.contextRunner.withUserConfiguration(CustomConvention.class).run((context) -> {
@@ -163,8 +129,7 @@ void backsOffWhenRestTemplateBuilderIsMissing() {
 		new ApplicationContextRunner().with(MetricsRun.simple())
 			.withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class,
 					HttpClientObservationsAutoConfiguration.class))
-			.run((context) -> assertThat(context).doesNotHaveBean(DefaultRestTemplateExchangeTagsProvider.class)
-				.doesNotHaveBean(ObservationRestTemplateCustomizer.class));
+			.run((context) -> assertThat(context).doesNotHaveBean(ObservationRestTemplateCustomizer.class));
 	}
 
 	private RestTemplate buildRestTemplate(AssertableApplicationContext context) {
@@ -174,26 +139,6 @@ private RestTemplate buildRestTemplate(AssertableApplicationContext context) {
 		return restTemplate;
 	}
 
-	@Configuration(proxyBeanMethods = false)
-	static class CustomTagsConfiguration {
-
-		@Bean
-		CustomTagsProvider customTagsProvider() {
-			return new CustomTagsProvider();
-		}
-
-	}
-
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	static class CustomTagsProvider implements RestTemplateExchangeTagsProvider {
-
-		@Override
-		public Iterable getTags(String urlTemplate, HttpRequest request, ClientHttpResponse response) {
-			return Tags.of("project", "spring-boot");
-		}
-
-	}
-
 	@Configuration(proxyBeanMethods = false)
 	static class CustomConventionConfiguration {
 
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/WebClientObservationConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/WebClientObservationConfigurationTests.java
index 9a94712edc78..8a917298b0e0 100644
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/WebClientObservationConfigurationTests.java
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/WebClientObservationConfigurationTests.java
@@ -29,9 +29,7 @@
 
 import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun;
 import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
-import org.springframework.boot.actuate.metrics.web.reactive.client.DefaultWebClientExchangeTagsProvider;
 import org.springframework.boot.actuate.metrics.web.reactive.client.ObservationWebClientCustomizer;
-import org.springframework.boot.actuate.metrics.web.reactive.client.WebClientExchangeTagsProvider;
 import org.springframework.boot.autoconfigure.AutoConfigurations;
 import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration;
 import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
@@ -59,7 +57,6 @@
  * @author Stephane Nicoll
  */
 @ExtendWith(OutputCaptureExtension.class)
-@SuppressWarnings("removal")
 class WebClientObservationConfigurationTests {
 
 	private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple())
@@ -69,8 +66,7 @@ class WebClientObservationConfigurationTests {
 
 	@Test
 	void contributesCustomizerBean() {
-		this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ObservationWebClientCustomizer.class)
-			.doesNotHaveBean(DefaultWebClientExchangeTagsProvider.class));
+		this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ObservationWebClientCustomizer.class));
 	}
 
 	@Test
@@ -82,14 +78,6 @@ void webClientCreatedWithBuilderIsInstrumented() {
 		});
 	}
 
-	@Test
-	void shouldNotOverrideCustomTagsProvider() {
-		this.contextRunner.withUserConfiguration(CustomTagsProviderConfig.class)
-			.run((context) -> assertThat(context).getBeans(WebClientExchangeTagsProvider.class)
-				.hasSize(1)
-				.containsKey("customTagsProvider"));
-	}
-
 	@Test
 	void shouldUseCustomConventionIfAvailable() {
 		this.contextRunner.withUserConfiguration(CustomConvention.class).run((context) -> {
@@ -170,16 +158,6 @@ private WebClient mockWebClient(WebClient.Builder builder) {
 		return builder.clientConnector(connector).build();
 	}
 
-	@Configuration(proxyBeanMethods = false)
-	static class CustomTagsProviderConfig {
-
-		@Bean
-		WebClientExchangeTagsProvider customTagsProvider() {
-			return mock(WebClientExchangeTagsProvider.class);
-		}
-
-	}
-
 	@Configuration(proxyBeanMethods = false)
 	static class CustomConventionConfig {
 
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/ServerRequestObservationConventionAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/ServerRequestObservationConventionAdapterTests.java
deleted file mode 100644
index 9d32817dc80c..000000000000
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/ServerRequestObservationConventionAdapterTests.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.autoconfigure.observation.web.reactive;
-
-import java.util.Map;
-
-import io.micrometer.common.KeyValue;
-import org.junit.jupiter.api.Test;
-
-import org.springframework.boot.actuate.metrics.web.reactive.server.DefaultWebFluxTagsProvider;
-import org.springframework.http.server.reactive.observation.ServerRequestObservationContext;
-import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
-import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
-import org.springframework.web.reactive.HandlerMapping;
-import org.springframework.web.util.pattern.PathPatternParser;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * Tests for {@link ServerRequestObservationConventionAdapter}.
- *
- * @author Brian Clozel
- */
-@SuppressWarnings("removal")
-@Deprecated(since = "3.0.0", forRemoval = true)
-class ServerRequestObservationConventionAdapterTests {
-
-	private static final String TEST_METRIC_NAME = "test.metric.name";
-
-	private final ServerRequestObservationConventionAdapter convention = new ServerRequestObservationConventionAdapter(
-			TEST_METRIC_NAME, new DefaultWebFluxTagsProvider());
-
-	@Test
-	void shouldUseConfiguredName() {
-		assertThat(this.convention.getName()).isEqualTo(TEST_METRIC_NAME);
-	}
-
-	@Test
-	void shouldPushTagsAsLowCardinalityKeyValues() {
-		MockServerHttpRequest request = MockServerHttpRequest.get("/resource/test").build();
-		MockServerHttpResponse response = new MockServerHttpResponse();
-		ServerRequestObservationContext context = new ServerRequestObservationContext(request, response,
-				Map.of(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE,
-						PathPatternParser.defaultInstance.parse("/resource/{name}")));
-		assertThat(this.convention.getLowCardinalityKeyValues(context)).contains(KeyValue.of("status", "200"),
-				KeyValue.of("outcome", "SUCCESS"), KeyValue.of("uri", "/resource/{name}"),
-				KeyValue.of("method", "GET"));
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java
index d8197198eb47..939b754424a8 100644
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java
@@ -19,8 +19,6 @@
 import java.util.List;
 
 import io.micrometer.core.instrument.MeterRegistry;
-import io.micrometer.core.instrument.Tag;
-import io.micrometer.core.instrument.Tags;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import reactor.core.publisher.Mono;
@@ -29,9 +27,6 @@
 import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun;
 import org.springframework.boot.actuate.autoconfigure.metrics.web.TestController;
 import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
-import org.springframework.boot.actuate.metrics.web.reactive.server.DefaultWebFluxTagsProvider;
-import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsContributor;
-import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsProvider;
 import org.springframework.boot.autoconfigure.AutoConfigurations;
 import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
 import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext;
@@ -83,28 +78,6 @@ void shouldProvideWebFluxObservationFilterOrdered() {
 		});
 	}
 
-	@Test
-	void shouldUseConventionAdapterWhenCustomTagsProvider() {
-		this.contextRunner.withUserConfiguration(CustomTagsProviderConfiguration.class).run((context) -> {
-			assertThat(context).hasSingleBean(ServerHttpObservationFilter.class);
-			assertThat(context).hasSingleBean(WebFluxTagsProvider.class);
-			assertThat(context).getBean(ServerHttpObservationFilter.class)
-				.extracting("observationConvention")
-				.isInstanceOf(ServerRequestObservationConventionAdapter.class);
-		});
-	}
-
-	@Test
-	void shouldUseConventionAdapterWhenCustomTagsContributor() {
-		this.contextRunner.withUserConfiguration(CustomTagsContributorConfiguration.class).run((context) -> {
-			assertThat(context).hasSingleBean(ServerHttpObservationFilter.class);
-			assertThat(context).hasSingleBean(WebFluxTagsContributor.class);
-			assertThat(context).getBean(ServerHttpObservationFilter.class)
-				.extracting("observationConvention")
-				.isInstanceOf(ServerRequestObservationConventionAdapter.class);
-		});
-	}
-
 	@Test
 	void shouldUseCustomConventionWhenAvailable() {
 		this.contextRunner.withUserConfiguration(CustomConventionConfiguration.class).run((context) -> {
@@ -128,21 +101,6 @@ void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) {
 			});
 	}
 
-	@Test
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	void afterMaxUrisReachedFurtherUrisAreDeniedWhenUsingCustomMetricName(CapturedOutput output) {
-		this.contextRunner.withUserConfiguration(TestController.class)
-			.withConfiguration(AutoConfigurations.of(MetricsAutoConfiguration.class, ObservationAutoConfiguration.class,
-					WebFluxAutoConfiguration.class))
-			.withPropertyValues("management.metrics.web.server.max-uri-tags=2",
-					"management.metrics.web.server.request.metric-name=my.http.server.requests")
-			.run((context) -> {
-				MeterRegistry registry = getInitializedMeterRegistry(context);
-				assertThat(registry.get("my.http.server.requests").meters()).hasSizeLessThanOrEqualTo(2);
-				assertThat(output).contains("Reached the maximum number of URI tags for 'my.http.server.requests'");
-			});
-	}
-
 	@Test
 	void afterMaxUrisReachedFurtherUrisAreDeniedWhenUsingCustomObservationName(CapturedOutput output) {
 		this.contextRunner.withUserConfiguration(TestController.class)
@@ -194,37 +152,6 @@ private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicati
 		return context.getBean(MeterRegistry.class);
 	}
 
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	@Configuration(proxyBeanMethods = false)
-	static class CustomTagsProviderConfiguration {
-
-		@Bean
-		WebFluxTagsProvider tagsProvider() {
-			return new DefaultWebFluxTagsProvider();
-		}
-
-	}
-
-	@Configuration(proxyBeanMethods = false)
-	static class CustomTagsContributorConfiguration {
-
-		@Bean
-		WebFluxTagsContributor tagsContributor() {
-			return new CustomTagsContributor();
-		}
-
-	}
-
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	static class CustomTagsContributor implements WebFluxTagsContributor {
-
-		@Override
-		public Iterable httpRequestTags(ServerWebExchange exchange, Throwable ex) {
-			return Tags.of("custom", "testvalue");
-		}
-
-	}
-
 	@Configuration(proxyBeanMethods = false)
 	static class CustomConventionConfiguration {
 
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/ServerRequestObservationConventionAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/ServerRequestObservationConventionAdapterTests.java
deleted file mode 100644
index 9705829af336..000000000000
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/ServerRequestObservationConventionAdapterTests.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.autoconfigure.observation.web.servlet;
-
-import java.util.Collections;
-import java.util.List;
-
-import io.micrometer.common.KeyValue;
-import io.micrometer.core.instrument.Tag;
-import io.micrometer.core.instrument.Tags;
-import io.micrometer.observation.Observation;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import org.junit.jupiter.api.Test;
-
-import org.springframework.boot.actuate.metrics.web.servlet.DefaultWebMvcTagsProvider;
-import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsContributor;
-import org.springframework.http.server.observation.ServerRequestObservationContext;
-import org.springframework.mock.web.MockHttpServletRequest;
-import org.springframework.mock.web.MockHttpServletResponse;
-import org.springframework.web.servlet.HandlerMapping;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * Tests for {@link ServerRequestObservationConventionAdapter}
- *
- * @author Brian Clozel
- */
-@SuppressWarnings("removal")
-@Deprecated(since = "3.0.0", forRemoval = true)
-class ServerRequestObservationConventionAdapterTests {
-
-	private static final String TEST_METRIC_NAME = "test.metric.name";
-
-	private final ServerRequestObservationConventionAdapter convention = new ServerRequestObservationConventionAdapter(
-			TEST_METRIC_NAME, new DefaultWebMvcTagsProvider(), Collections.emptyList());
-
-	private final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/resource/test");
-
-	private final MockHttpServletResponse response = new MockHttpServletResponse();
-
-	private final ServerRequestObservationContext context = new ServerRequestObservationContext(this.request,
-			this.response);
-
-	@Test
-	void customNameIsUsed() {
-		assertThat(this.convention.getName()).isEqualTo(TEST_METRIC_NAME);
-	}
-
-	@Test
-	void onlySupportServerRequestObservationContext() {
-		assertThat(this.convention.supportsContext(this.context)).isTrue();
-		assertThat(this.convention.supportsContext(new OtherContext())).isFalse();
-	}
-
-	@Test
-	void pushTagsAsLowCardinalityKeyValues() {
-		this.request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, "/resource/{name}");
-		this.context.setPathPattern("/resource/{name}");
-		assertThat(this.convention.getLowCardinalityKeyValues(this.context)).contains(KeyValue.of("status", "200"),
-				KeyValue.of("outcome", "SUCCESS"), KeyValue.of("uri", "/resource/{name}"),
-				KeyValue.of("method", "GET"));
-	}
-
-	@Test
-	void doesNotPushAnyHighCardinalityKeyValue() {
-		assertThat(this.convention.getHighCardinalityKeyValues(this.context)).isEmpty();
-	}
-
-	@Test
-	void pushTagsFromContributors() {
-		ServerRequestObservationConventionAdapter convention = new ServerRequestObservationConventionAdapter(
-				TEST_METRIC_NAME, null, List.of(new CustomWebMvcContributor()));
-		assertThat(convention.getLowCardinalityKeyValues(this.context)).contains(KeyValue.of("custom", "value"));
-	}
-
-	static class OtherContext extends Observation.Context {
-
-	}
-
-	static class CustomWebMvcContributor implements WebMvcTagsContributor {
-
-		@Override
-		public Iterable getTags(HttpServletRequest request, HttpServletResponse response, Object handler,
-				Throwable exception) {
-			return Tags.of("custom", "value");
-		}
-
-		@Override
-		public Iterable getLongRequestTags(HttpServletRequest request, Object handler) {
-			return Collections.emptyList();
-		}
-
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java
index 7328d40b2b53..b996ba206430 100644
--- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java
+++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java
@@ -16,16 +16,12 @@
 
 package org.springframework.boot.actuate.autoconfigure.observation.web.servlet;
 
-import java.util.Collections;
 import java.util.EnumSet;
 
 import io.micrometer.core.instrument.MeterRegistry;
-import io.micrometer.core.instrument.Tag;
 import io.micrometer.observation.tck.TestObservationRegistry;
 import jakarta.servlet.DispatcherType;
 import jakarta.servlet.Filter;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 
@@ -33,9 +29,6 @@
 import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun;
 import org.springframework.boot.actuate.autoconfigure.metrics.web.TestController;
 import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
-import org.springframework.boot.actuate.metrics.web.servlet.DefaultWebMvcTagsProvider;
-import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsContributor;
-import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsProvider;
 import org.springframework.boot.autoconfigure.AutoConfigurations;
 import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
 import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
@@ -67,7 +60,6 @@
  * @author Moritz Halbritter
  */
 @ExtendWith(OutputCaptureExtension.class)
-@SuppressWarnings("removal")
 class WebMvcObservationAutoConfigurationTests {
 
 	private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
@@ -85,21 +77,12 @@ void backsOffWhenMeterRegistryIsMissing() {
 	@Test
 	void definesFilterWhenRegistryIsPresent() {
 		this.contextRunner.run((context) -> {
-			assertThat(context).doesNotHaveBean(DefaultWebMvcTagsProvider.class);
 			assertThat(context).hasSingleBean(FilterRegistrationBean.class);
 			assertThat(context.getBean(FilterRegistrationBean.class).getFilter())
 				.isInstanceOf(ServerHttpObservationFilter.class);
 		});
 	}
 
-	@Test
-	void adapterConventionWhenTagsProviderPresent() {
-		this.contextRunner.withUserConfiguration(TagsProviderConfiguration.class)
-			.run((context) -> assertThat(context.getBean(FilterRegistrationBean.class).getFilter())
-				.extracting("observationConvention")
-				.isInstanceOf(ServerRequestObservationConventionAdapter.class));
-	}
-
 	@Test
 	void customConventionWhenPresent() {
 		this.contextRunner.withUserConfiguration(CustomConventionConfiguration.class)
@@ -169,21 +152,6 @@ void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) {
 			});
 	}
 
-	@Test
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	void afterMaxUrisReachedFurtherUrisAreDeniedWhenUsingCustomMetricName(CapturedOutput output) {
-		this.contextRunner.withUserConfiguration(TestController.class)
-			.withConfiguration(AutoConfigurations.of(MetricsAutoConfiguration.class, ObservationAutoConfiguration.class,
-					WebMvcAutoConfiguration.class))
-			.withPropertyValues("management.metrics.web.server.max-uri-tags=2",
-					"management.metrics.web.server.request.metric-name=my.http.server.requests")
-			.run((context) -> {
-				MeterRegistry registry = getInitializedMeterRegistry(context);
-				assertThat(registry.get("my.http.server.requests").meters()).hasSizeLessThanOrEqualTo(2);
-				assertThat(output).contains("Reached the maximum number of URI tags for 'my.http.server.requests'");
-			});
-	}
-
 	@Test
 	void afterMaxUrisReachedFurtherUrisAreDeniedWhenUsingCustomObservationName(CapturedOutput output) {
 		this.contextRunner.withUserConfiguration(TestController.class)
@@ -211,14 +179,6 @@ void shouldNotDenyNorLogIfMaxUrisIsNotReached(CapturedOutput output) {
 			});
 	}
 
-	@Test
-	void whenTagContributorsAreDefinedThenTagsProviderUsesThem() {
-		this.contextRunner.withUserConfiguration(TagsContributorsConfiguration.class)
-			.run((context) -> assertThat(context.getBean(FilterRegistrationBean.class).getFilter())
-				.extracting("observationConvention")
-				.isInstanceOf(ServerRequestObservationConventionAdapter.class));
-	}
-
 	private MeterRegistry getInitializedMeterRegistry(AssertableWebApplicationContext context) throws Exception {
 		return getInitializedMeterRegistry(context, "/test0", "/test1", "/test2");
 	}
@@ -235,47 +195,6 @@ private MeterRegistry getInitializedMeterRegistry(AssertableWebApplicationContex
 		return context.getBean(MeterRegistry.class);
 	}
 
-	@Configuration(proxyBeanMethods = false)
-	static class TagsProviderConfiguration {
-
-		@Bean
-		TestWebMvcTagsProvider tagsProvider() {
-			return new TestWebMvcTagsProvider();
-		}
-
-	}
-
-	@Configuration(proxyBeanMethods = false)
-	static class TagsContributorsConfiguration {
-
-		@Bean
-		WebMvcTagsContributor tagContributorOne() {
-			return mock(WebMvcTagsContributor.class);
-		}
-
-		@Bean
-		WebMvcTagsContributor tagContributorTwo() {
-			return mock(WebMvcTagsContributor.class);
-		}
-
-	}
-
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	private static final class TestWebMvcTagsProvider implements WebMvcTagsProvider {
-
-		@Override
-		public Iterable getTags(HttpServletRequest request, HttpServletResponse response, Object handler,
-				Throwable exception) {
-			return Collections.emptyList();
-		}
-
-		@Override
-		public Iterable getLongRequestTags(HttpServletRequest request, Object handler) {
-			return Collections.emptyList();
-		}
-
-	}
-
 	@Configuration(proxyBeanMethods = false)
 	static class TestServerHttpObservationFilterRegistrationConfiguration {
 
diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManager.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManager.java
index f0a3453e70e1..da459eb0c28b 100644
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManager.java
+++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManager.java
@@ -141,7 +141,7 @@ private void shutdown(ShutdownOperation shutdownOperation) {
 		}
 		this.scheduled.cancel(false);
 		switch (shutdownOperation) {
-			case PUSH, POST -> post();
+			case POST -> post();
 			case PUT -> put();
 			case DELETE -> delete();
 		}
@@ -162,13 +162,6 @@ public enum ShutdownOperation {
 		 */
 		POST,
 
-		/**
-		 * Perform a POST before shutdown.
-		 * @deprecated since 3.0.0 for removal in 3.2.0 in favor of {@link #POST}.
-		 */
-		@Deprecated(since = "3.0.0", forRemoval = true)
-		PUSH,
-
 		/**
 		 * Perform a PUT before shutdown.
 		 */
diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/DefaultRestTemplateExchangeTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/DefaultRestTemplateExchangeTagsProvider.java
deleted file mode 100644
index ace3689e6862..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/DefaultRestTemplateExchangeTagsProvider.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.metrics.web.client;
-
-import java.util.Arrays;
-
-import io.micrometer.core.instrument.Tag;
-
-import org.springframework.http.HttpRequest;
-import org.springframework.http.client.ClientHttpResponse;
-import org.springframework.util.StringUtils;
-
-/**
- * Default implementation of {@link RestTemplateExchangeTagsProvider}.
- *
- * @author Jon Schneider
- * @author Nishant Raut
- * @since 2.0.0
- * @deprecated since 3.0.0 for removal in 3.2.0 in favor of
- * {@link org.springframework.http.client.observation.DefaultClientRequestObservationConvention}
- */
-@Deprecated(since = "3.0.0", forRemoval = true)
-@SuppressWarnings("removal")
-public class DefaultRestTemplateExchangeTagsProvider implements RestTemplateExchangeTagsProvider {
-
-	@Override
-	public Iterable getTags(String urlTemplate, HttpRequest request, ClientHttpResponse response) {
-		Tag uriTag = (StringUtils.hasText(urlTemplate) ? RestTemplateExchangeTags.uri(urlTemplate)
-				: RestTemplateExchangeTags.uri(request));
-		return Arrays.asList(RestTemplateExchangeTags.method(request), uriTag,
-				RestTemplateExchangeTags.status(response), RestTemplateExchangeTags.clientName(request),
-				RestTemplateExchangeTags.outcome(response));
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/RestTemplateExchangeTags.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/RestTemplateExchangeTags.java
deleted file mode 100644
index 5f17ee3d4f44..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/RestTemplateExchangeTags.java
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.metrics.web.client;
-
-import java.io.IOException;
-import java.net.URI;
-import java.util.regex.Pattern;
-
-import io.micrometer.core.instrument.Tag;
-
-import org.springframework.boot.actuate.metrics.http.Outcome;
-import org.springframework.http.HttpRequest;
-import org.springframework.http.client.ClientHttpResponse;
-import org.springframework.http.client.observation.DefaultClientRequestObservationConvention;
-import org.springframework.util.StringUtils;
-import org.springframework.web.client.RestTemplate;
-
-/**
- * Factory methods for creating {@link Tag Tags} related to a request-response exchange
- * performed by a {@link RestTemplate}.
- *
- * @author Andy Wilkinson
- * @author Jon Schneider
- * @author Nishant Raut
- * @author Brian Clozel
- * @since 2.0.0
- * @deprecated since 3.0.0 for removal in 3.2.0 in favor of
- * {@link DefaultClientRequestObservationConvention}
- */
-@Deprecated(since = "3.0.0", forRemoval = true)
-public final class RestTemplateExchangeTags {
-
-	private static final Pattern STRIP_URI_PATTERN = Pattern.compile("^https?://[^/]+/");
-
-	private RestTemplateExchangeTags() {
-	}
-
-	/**
-	 * Creates a {@code method} {@code Tag} for the {@link HttpRequest#getMethod() method}
-	 * of the given {@code request}.
-	 * @param request the request
-	 * @return the method tag
-	 */
-	public static Tag method(HttpRequest request) {
-		return Tag.of("method", request.getMethod().name());
-	}
-
-	/**
-	 * Creates a {@code uri} {@code Tag} for the URI of the given {@code request}.
-	 * @param request the request
-	 * @return the uri tag
-	 */
-	public static Tag uri(HttpRequest request) {
-		return Tag.of("uri", ensureLeadingSlash(stripUri(request.getURI().toString())));
-	}
-
-	/**
-	 * Creates a {@code uri} {@code Tag} from the given {@code uriTemplate}.
-	 * @param uriTemplate the template
-	 * @return the uri tag
-	 */
-	public static Tag uri(String uriTemplate) {
-		String uri = (StringUtils.hasText(uriTemplate) ? uriTemplate : "none");
-		return Tag.of("uri", ensureLeadingSlash(stripUri(uri)));
-	}
-
-	private static String stripUri(String uri) {
-		return STRIP_URI_PATTERN.matcher(uri).replaceAll("");
-	}
-
-	private static String ensureLeadingSlash(String url) {
-		return (url == null || url.startsWith("/")) ? url : "/" + url;
-	}
-
-	/**
-	 * Creates a {@code status} {@code Tag} derived from the
-	 * {@link ClientHttpResponse#getStatusCode() status} of the given {@code response}.
-	 * @param response the response
-	 * @return the status tag
-	 */
-	public static Tag status(ClientHttpResponse response) {
-		return Tag.of("status", getStatusMessage(response));
-	}
-
-	private static String getStatusMessage(ClientHttpResponse response) {
-		try {
-			if (response == null) {
-				return "CLIENT_ERROR";
-			}
-			return String.valueOf(response.getStatusCode().value());
-		}
-		catch (IOException ex) {
-			return "IO_ERROR";
-		}
-	}
-
-	/**
-	 * Create a {@code client.name} {@code Tag} derived from the {@link URI#getHost host}
-	 * of the {@link HttpRequest#getURI() URI} of the given {@code request}.
-	 * @param request the request
-	 * @return the client.name tag
-	 */
-	public static Tag clientName(HttpRequest request) {
-		String host = request.getURI().getHost();
-		if (host == null) {
-			host = "none";
-		}
-		return Tag.of("client.name", host);
-	}
-
-	/**
-	 * Creates an {@code outcome} {@code Tag} derived from the
-	 * {@link ClientHttpResponse#getStatusCode() status} of the given {@code response}.
-	 * @param response the response
-	 * @return the outcome tag
-	 * @since 2.2.0
-	 */
-	public static Tag outcome(ClientHttpResponse response) {
-		try {
-			if (response != null) {
-				return Outcome.forStatus(response.getStatusCode().value()).asTag();
-			}
-		}
-		catch (IOException ex) {
-			// Continue
-		}
-		return Outcome.UNKNOWN.asTag();
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/RestTemplateExchangeTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/RestTemplateExchangeTagsProvider.java
deleted file mode 100644
index ea3c05360ce3..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/RestTemplateExchangeTagsProvider.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.metrics.web.client;
-
-import io.micrometer.core.instrument.Tag;
-
-import org.springframework.http.HttpRequest;
-import org.springframework.http.client.ClientHttpResponse;
-import org.springframework.http.client.observation.ClientRequestObservationConvention;
-import org.springframework.web.client.RestTemplate;
-
-/**
- * Provides {@link Tag Tags} for an exchange performed by a {@link RestTemplate}.
- *
- * @author Jon Schneider
- * @author Andy Wilkinson
- * @since 2.0.0
- * @deprecated since 3.0.0 for removal in 3.2.0 in favor of
- * {@link ClientRequestObservationConvention}
- */
-@FunctionalInterface
-@Deprecated(since = "3.0.0", forRemoval = true)
-public interface RestTemplateExchangeTagsProvider {
-
-	/**
-	 * Provides the tags to be associated with metrics that are recorded for the given
-	 * {@code request} and {@code response} exchange.
-	 * @param urlTemplate the source URl template, if available
-	 * @param request the request
-	 * @param response the response (may be {@code null} if the exchange failed)
-	 * @return the tags
-	 */
-	Iterable getTags(String urlTemplate, HttpRequest request, ClientHttpResponse response);
-
-}
diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProvider.java
deleted file mode 100644
index aeae3222d5ab..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProvider.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.metrics.web.reactive.client;
-
-import java.util.Arrays;
-
-import io.micrometer.core.instrument.Tag;
-
-import org.springframework.web.reactive.function.client.ClientRequest;
-import org.springframework.web.reactive.function.client.ClientResponse;
-
-/**
- * Default implementation of {@link WebClientExchangeTagsProvider}.
- *
- * @author Brian Clozel
- * @author Nishant Raut
- * @since 2.1.0
- * @deprecated since 3.0.0 for removal in 3.2.0 in favor of
- * {@link org.springframework.web.reactive.function.client.ClientRequestObservationConvention}
- */
-@Deprecated(since = "3.0.0", forRemoval = true)
-@SuppressWarnings("removal")
-public class DefaultWebClientExchangeTagsProvider implements WebClientExchangeTagsProvider {
-
-	@Override
-	public Iterable tags(ClientRequest request, ClientResponse response, Throwable throwable) {
-		Tag method = WebClientExchangeTags.method(request);
-		Tag uri = WebClientExchangeTags.uri(request);
-		Tag clientName = WebClientExchangeTags.clientName(request);
-		Tag status = WebClientExchangeTags.status(response, throwable);
-		Tag outcome = WebClientExchangeTags.outcome(response);
-		return Arrays.asList(method, uri, clientName, status, outcome);
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTags.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTags.java
deleted file mode 100644
index c916188b6846..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTags.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.metrics.web.reactive.client;
-
-import java.io.IOException;
-import java.util.regex.Pattern;
-
-import io.micrometer.core.instrument.Tag;
-
-import org.springframework.boot.actuate.metrics.http.Outcome;
-import org.springframework.http.client.reactive.ClientHttpRequest;
-import org.springframework.web.reactive.function.client.ClientRequest;
-import org.springframework.web.reactive.function.client.ClientResponse;
-import org.springframework.web.reactive.function.client.WebClient;
-
-/**
- * Factory methods for creating {@link Tag Tags} related to a request-response exchange
- * performed by a {@link WebClient}.
- *
- * @author Brian Clozel
- * @author Nishant Raut
- * @since 2.1.0
- * @deprecated since 3.0.0 for removal in 3.2.0 in favor of
- * {@link org.springframework.web.reactive.function.client.DefaultClientRequestObservationConvention}
- */
-@Deprecated(since = "3.0.0", forRemoval = true)
-public final class WebClientExchangeTags {
-
-	private static final String URI_TEMPLATE_ATTRIBUTE = WebClient.class.getName() + ".uriTemplate";
-
-	private static final Tag IO_ERROR = Tag.of("status", "IO_ERROR");
-
-	private static final Tag CLIENT_ERROR = Tag.of("status", "CLIENT_ERROR");
-
-	private static final Pattern PATTERN_BEFORE_PATH = Pattern.compile("^https?://[^/]+/");
-
-	private static final Tag CLIENT_NAME_NONE = Tag.of("client.name", "none");
-
-	private WebClientExchangeTags() {
-	}
-
-	/**
-	 * Creates a {@code method} {@code Tag} for the {@link ClientHttpRequest#getMethod()
-	 * method} of the given {@code request}.
-	 * @param request the request
-	 * @return the method tag
-	 */
-	public static Tag method(ClientRequest request) {
-		return Tag.of("method", request.method().name());
-	}
-
-	/**
-	 * Creates a {@code uri} {@code Tag} for the URI path of the given {@code request}.
-	 * @param request the request
-	 * @return the uri tag
-	 */
-	public static Tag uri(ClientRequest request) {
-		String uri = (String) request.attribute(URI_TEMPLATE_ATTRIBUTE).orElseGet(() -> request.url().toString());
-		return Tag.of("uri", extractPath(uri));
-	}
-
-	private static String extractPath(String url) {
-		String path = PATTERN_BEFORE_PATH.matcher(url).replaceFirst("");
-		return (path.startsWith("/") ? path : "/" + path);
-	}
-
-	/**
-	 * Creates a {@code status} {@code Tag} derived from the
-	 * {@link ClientResponse#statusCode()} of the given {@code response} if available, the
-	 * thrown exception otherwise, or considers the request as Cancelled as a last resort.
-	 * @param response the response
-	 * @param throwable the exception
-	 * @return the status tag
-	 * @since 2.3.0
-	 */
-	public static Tag status(ClientResponse response, Throwable throwable) {
-		if (response != null) {
-			return Tag.of("status", String.valueOf(response.statusCode().value()));
-		}
-		if (throwable != null) {
-			return (throwable instanceof IOException) ? IO_ERROR : CLIENT_ERROR;
-		}
-		return CLIENT_ERROR;
-	}
-
-	/**
-	 * Create a {@code client.name} {@code Tag} derived from the
-	 * {@link java.net.URI#getHost host} of the {@link ClientRequest#url() URL} of the
-	 * given {@code request}.
-	 * @param request the request
-	 * @return the client.name tag
-	 */
-	public static Tag clientName(ClientRequest request) {
-		String host = request.url().getHost();
-		if (host == null) {
-			return CLIENT_NAME_NONE;
-		}
-		return Tag.of("client.name", host);
-	}
-
-	/**
-	 * Creates an {@code outcome} {@code Tag} derived from the
-	 * {@link ClientResponse#statusCode() status} of the given {@code response}.
-	 * @param response the response
-	 * @return the outcome tag
-	 * @since 2.2.0
-	 */
-	public static Tag outcome(ClientResponse response) {
-		Outcome outcome = (response != null) ? Outcome.forStatus(response.statusCode().value()) : Outcome.UNKNOWN;
-		return outcome.asTag();
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsProvider.java
deleted file mode 100644
index 7d522e48d2b3..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsProvider.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.metrics.web.reactive.client;
-
-import io.micrometer.core.instrument.Tag;
-
-import org.springframework.web.reactive.function.client.ClientRequest;
-import org.springframework.web.reactive.function.client.ClientResponse;
-
-/**
- * {@link Tag Tags} provider for an exchange performed by a
- * {@link org.springframework.web.reactive.function.client.WebClient}.
- *
- * @author Brian Clozel
- * @since 2.1.0
- * @deprecated since 3.0.0 for removal in 3.2.0 in favor of
- * {@link org.springframework.web.reactive.function.client.ClientRequestObservationConvention}
- */
-@FunctionalInterface
-@Deprecated(since = "3.0.0", forRemoval = true)
-public interface WebClientExchangeTagsProvider {
-
-	/**
-	 * Provide tags to be associated with metrics for the client exchange.
-	 * @param request the client request
-	 * @param response the server response (may be {@code null})
-	 * @param throwable the exception (may be {@code null})
-	 * @return tags to associate with metrics for the request and response exchange
-	 */
-	Iterable tags(ClientRequest request, ClientResponse response, Throwable throwable);
-
-}
diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProvider.java
deleted file mode 100644
index ef319dc065ad..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProvider.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.metrics.web.reactive.server;
-
-import java.util.Collections;
-import java.util.List;
-
-import io.micrometer.core.instrument.Tag;
-import io.micrometer.core.instrument.Tags;
-
-import org.springframework.web.server.ServerWebExchange;
-
-/**
- * Default implementation of {@link WebFluxTagsProvider}.
- *
- * @author Jon Schneider
- * @author Andy Wilkinson
- * @since 2.0.0
- * @deprecated since 3.0.0 for removal in 3.2.0 in favor of
- * {@link org.springframework.http.server.reactive.observation.ServerRequestObservationConvention}
- */
-@Deprecated(since = "3.0.0", forRemoval = true)
-@SuppressWarnings("removal")
-public class DefaultWebFluxTagsProvider implements WebFluxTagsProvider {
-
-	private final boolean ignoreTrailingSlash;
-
-	private final List contributors;
-
-	public DefaultWebFluxTagsProvider() {
-		this(false);
-	}
-
-	/**
-	 * Creates a new {@link DefaultWebFluxTagsProvider} that will provide tags from the
-	 * given {@code contributors} in addition to its own.
-	 * @param contributors the contributors that will provide additional tags
-	 * @since 2.3.0
-	 */
-	public DefaultWebFluxTagsProvider(List contributors) {
-		this(false, contributors);
-	}
-
-	public DefaultWebFluxTagsProvider(boolean ignoreTrailingSlash) {
-		this(ignoreTrailingSlash, Collections.emptyList());
-	}
-
-	/**
-	 * Creates a new {@link DefaultWebFluxTagsProvider} that will provide tags from the
-	 * given {@code contributors} in addition to its own.
-	 * @param ignoreTrailingSlash whether trailing slashes should be ignored when
-	 * determining the {@code uri} tag.
-	 * @param contributors the contributors that will provide additional tags
-	 * @since 2.3.0
-	 */
-	public DefaultWebFluxTagsProvider(boolean ignoreTrailingSlash, List contributors) {
-		this.ignoreTrailingSlash = ignoreTrailingSlash;
-		this.contributors = contributors;
-	}
-
-	@Override
-	public Iterable httpRequestTags(ServerWebExchange exchange, Throwable exception) {
-		Tags tags = Tags.empty();
-		tags = tags.and(WebFluxTags.method(exchange));
-		tags = tags.and(WebFluxTags.uri(exchange, this.ignoreTrailingSlash));
-		tags = tags.and(WebFluxTags.exception(exception));
-		tags = tags.and(WebFluxTags.status(exchange));
-		tags = tags.and(WebFluxTags.outcome(exchange, exception));
-		for (WebFluxTagsContributor contributor : this.contributors) {
-			tags = tags.and(contributor.httpRequestTags(exchange, exception));
-		}
-		return tags;
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTags.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTags.java
deleted file mode 100644
index ecada43258a4..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTags.java
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.metrics.web.reactive.server;
-
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.regex.Pattern;
-
-import io.micrometer.core.instrument.Tag;
-
-import org.springframework.boot.actuate.metrics.http.Outcome;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.HttpStatusCode;
-import org.springframework.util.StringUtils;
-import org.springframework.web.reactive.HandlerMapping;
-import org.springframework.web.server.ServerWebExchange;
-import org.springframework.web.util.pattern.PathPattern;
-
-/**
- * Factory methods for {@link Tag Tags} associated with a request-response exchange that
- * is handled by WebFlux.
- *
- * @author Jon Schneider
- * @author Andy Wilkinson
- * @author Michael McFadyen
- * @author Brian Clozel
- * @since 2.0.0
- * @deprecated since 3.0.0 for removal in 3.2.0 in favor of
- * {@link org.springframework.http.server.reactive.observation.ServerRequestObservationConvention}
- */
-@Deprecated(since = "3.0.0", forRemoval = true)
-public final class WebFluxTags {
-
-	private static final Tag URI_NOT_FOUND = Tag.of("uri", "NOT_FOUND");
-
-	private static final Tag URI_REDIRECTION = Tag.of("uri", "REDIRECTION");
-
-	private static final Tag URI_ROOT = Tag.of("uri", "root");
-
-	private static final Tag URI_UNKNOWN = Tag.of("uri", "UNKNOWN");
-
-	private static final Tag EXCEPTION_NONE = Tag.of("exception", "None");
-
-	private static final Pattern FORWARD_SLASHES_PATTERN = Pattern.compile("//+");
-
-	private static final Set DISCONNECTED_CLIENT_EXCEPTIONS = new HashSet<>(
-			Arrays.asList("AbortedException", "ClientAbortException", "EOFException", "EofException"));
-
-	private WebFluxTags() {
-	}
-
-	/**
-	 * Creates a {@code method} tag based on the
-	 * {@link org.springframework.http.server.reactive.ServerHttpRequest#getMethod()
-	 * method} of the {@link ServerWebExchange#getRequest()} request of the given
-	 * {@code exchange}.
-	 * @param exchange the exchange
-	 * @return the method tag whose value is a capitalized method (e.g. GET).
-	 */
-	public static Tag method(ServerWebExchange exchange) {
-		return Tag.of("method", exchange.getRequest().getMethod().name());
-	}
-
-	/**
-	 * Creates a {@code status} tag based on the response status of the given
-	 * {@code exchange}.
-	 * @param exchange the exchange
-	 * @return the status tag derived from the response status
-	 */
-	public static Tag status(ServerWebExchange exchange) {
-		HttpStatusCode status = exchange.getResponse().getStatusCode();
-		if (status == null) {
-			status = HttpStatus.OK;
-		}
-		return Tag.of("status", String.valueOf(status.value()));
-	}
-
-	/**
-	 * Creates a {@code uri} tag based on the URI of the given {@code exchange}. Uses the
-	 * {@link HandlerMapping#BEST_MATCHING_PATTERN_ATTRIBUTE} best matching pattern if
-	 * available. Falling back to {@code REDIRECTION} for 3xx responses, {@code NOT_FOUND}
-	 * for 404 responses, {@code root} for requests with no path info, and {@code UNKNOWN}
-	 * for all other requests.
-	 * @param exchange the exchange
-	 * @return the uri tag derived from the exchange
-	 */
-	public static Tag uri(ServerWebExchange exchange) {
-		return uri(exchange, false);
-	}
-
-	/**
-	 * Creates a {@code uri} tag based on the URI of the given {@code exchange}. Uses the
-	 * {@link HandlerMapping#BEST_MATCHING_PATTERN_ATTRIBUTE} best matching pattern if
-	 * available. Falling back to {@code REDIRECTION} for 3xx responses, {@code NOT_FOUND}
-	 * for 404 responses, {@code root} for requests with no path info, and {@code UNKNOWN}
-	 * for all other requests.
-	 * @param exchange the exchange
-	 * @param ignoreTrailingSlash whether to ignore the trailing slash
-	 * @return the uri tag derived from the exchange
-	 */
-	public static Tag uri(ServerWebExchange exchange, boolean ignoreTrailingSlash) {
-		PathPattern pathPattern = exchange.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
-		if (pathPattern != null) {
-			String patternString = pathPattern.getPatternString();
-			if (ignoreTrailingSlash && patternString.length() > 1) {
-				patternString = removeTrailingSlash(patternString);
-			}
-			if (patternString.isEmpty()) {
-				return URI_ROOT;
-			}
-			return Tag.of("uri", patternString);
-		}
-		HttpStatusCode status = exchange.getResponse().getStatusCode();
-		if (status != null) {
-			if (status.is3xxRedirection()) {
-				return URI_REDIRECTION;
-			}
-			if (status == HttpStatus.NOT_FOUND) {
-				return URI_NOT_FOUND;
-			}
-		}
-		String path = getPathInfo(exchange);
-		if (path.isEmpty()) {
-			return URI_ROOT;
-		}
-		return URI_UNKNOWN;
-	}
-
-	private static String getPathInfo(ServerWebExchange exchange) {
-		String path = exchange.getRequest().getPath().value();
-		String uri = StringUtils.hasText(path) ? path : "/";
-		String singleSlashes = FORWARD_SLASHES_PATTERN.matcher(uri).replaceAll("/");
-		return removeTrailingSlash(singleSlashes);
-	}
-
-	private static String removeTrailingSlash(String text) {
-		if (!StringUtils.hasLength(text)) {
-			return text;
-		}
-		return text.endsWith("/") ? text.substring(0, text.length() - 1) : text;
-	}
-
-	/**
-	 * Creates an {@code exception} tag based on the {@link Class#getSimpleName() simple
-	 * name} of the class of the given {@code exception}.
-	 * @param exception the exception, may be {@code null}
-	 * @return the exception tag derived from the exception
-	 */
-	public static Tag exception(Throwable exception) {
-		if (exception != null) {
-			String simpleName = exception.getClass().getSimpleName();
-			return Tag.of("exception", StringUtils.hasText(simpleName) ? simpleName : exception.getClass().getName());
-		}
-		return EXCEPTION_NONE;
-	}
-
-	/**
-	 * Creates an {@code outcome} tag based on the response status of the given
-	 * {@code exchange} and the exception thrown during request processing.
-	 * @param exchange the exchange
-	 * @param exception the termination signal sent by the publisher
-	 * @return the outcome tag derived from the response status
-	 * @since 2.5.0
-	 */
-	public static Tag outcome(ServerWebExchange exchange, Throwable exception) {
-		if (exception != null) {
-			if (DISCONNECTED_CLIENT_EXCEPTIONS.contains(exception.getClass().getSimpleName())) {
-				return Outcome.UNKNOWN.asTag();
-			}
-		}
-		HttpStatusCode statusCode = exchange.getResponse().getStatusCode();
-		Outcome outcome = (statusCode != null) ? Outcome.forStatus(statusCode.value()) : Outcome.SUCCESS;
-		return outcome.asTag();
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsContributor.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsContributor.java
deleted file mode 100644
index 6bd9958dfca3..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsContributor.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.metrics.web.reactive.server;
-
-import io.micrometer.core.instrument.Tag;
-
-import org.springframework.web.server.ServerWebExchange;
-
-/**
- * A contributor of {@link Tag Tags} for WebFlux-based request handling. Typically used by
- * a {@link WebFluxTagsProvider} to provide tags in addition to its defaults.
- *
- * @author Andy Wilkinson
- * @since 2.3.0
- * @deprecated since 3.0.0 for removal in 3.2.0 in favor of
- * {@link org.springframework.http.server.reactive.observation.ServerRequestObservationConvention}
- */
-@FunctionalInterface
-@Deprecated(since = "3.0.0", forRemoval = true)
-public interface WebFluxTagsContributor {
-
-	/**
-	 * Provides tags to be associated with metrics for the given {@code exchange}.
-	 * @param exchange the exchange
-	 * @param ex the current exception (may be {@code null})
-	 * @return tags to associate with metrics for the request and response exchange
-	 */
-	Iterable httpRequestTags(ServerWebExchange exchange, Throwable ex);
-
-}
diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsProvider.java
deleted file mode 100644
index 081c9598cc89..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsProvider.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.metrics.web.reactive.server;
-
-import io.micrometer.core.instrument.Tag;
-
-import org.springframework.web.server.ServerWebExchange;
-
-/**
- * Provides {@link Tag Tags} for WebFlux-based request handling.
- *
- * @author Jon Schneider
- * @author Andy Wilkinson
- * @since 2.0.0
- * @deprecated since 3.0.0 for removal in 3.2.0 in favor of
- * {@link org.springframework.http.server.reactive.observation.ServerRequestObservationConvention}
- */
-@FunctionalInterface
-@Deprecated(since = "3.0.0", forRemoval = true)
-public interface WebFluxTagsProvider {
-
-	/**
-	 * Provides tags to be associated with metrics for the given {@code exchange}.
-	 * @param exchange the exchange
-	 * @param ex the current exception (may be {@code null})
-	 * @return tags to associate with metrics for the request and response exchange
-	 */
-	Iterable httpRequestTags(ServerWebExchange exchange, Throwable ex);
-
-}
diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/package-info.java
deleted file mode 100644
index 1167af5ca302..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/package-info.java
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright 2012-2019 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.
- */
-
-/**
- * Actuator support for WebFlux metrics.
- */
-package org.springframework.boot.actuate.metrics.web.reactive.server;
diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/DefaultWebMvcTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/DefaultWebMvcTagsProvider.java
deleted file mode 100644
index ac50670ca326..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/DefaultWebMvcTagsProvider.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.metrics.web.servlet;
-
-import java.util.Collections;
-import java.util.List;
-
-import io.micrometer.core.instrument.Tag;
-import io.micrometer.core.instrument.Tags;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-
-/**
- * Default implementation of {@link WebMvcTagsProvider}.
- *
- * @author Jon Schneider
- * @since 2.0.0
- * @deprecated since 3.0.0 for removal in 3.2.0 in favor of
- * {@link org.springframework.http.server.observation.ServerRequestObservationConvention}
- */
-@Deprecated(since = "3.0.0", forRemoval = true)
-@SuppressWarnings("removal")
-public class DefaultWebMvcTagsProvider implements WebMvcTagsProvider {
-
-	private final boolean ignoreTrailingSlash;
-
-	private final List contributors;
-
-	public DefaultWebMvcTagsProvider() {
-		this(false);
-	}
-
-	/**
-	 * Creates a new {@link DefaultWebMvcTagsProvider} that will provide tags from the
-	 * given {@code contributors} in addition to its own.
-	 * @param contributors the contributors that will provide additional tags
-	 * @since 2.3.0
-	 */
-	public DefaultWebMvcTagsProvider(List contributors) {
-		this(false, contributors);
-	}
-
-	public DefaultWebMvcTagsProvider(boolean ignoreTrailingSlash) {
-		this(ignoreTrailingSlash, Collections.emptyList());
-	}
-
-	/**
-	 * Creates a new {@link DefaultWebMvcTagsProvider} that will provide tags from the
-	 * given {@code contributors} in addition to its own.
-	 * @param ignoreTrailingSlash whether trailing slashes should be ignored when
-	 * determining the {@code uri} tag.
-	 * @param contributors the contributors that will provide additional tags
-	 * @since 2.3.0
-	 */
-	public DefaultWebMvcTagsProvider(boolean ignoreTrailingSlash, List contributors) {
-		this.ignoreTrailingSlash = ignoreTrailingSlash;
-		this.contributors = contributors;
-	}
-
-	@Override
-	public Iterable getTags(HttpServletRequest request, HttpServletResponse response, Object handler,
-			Throwable exception) {
-		Tags tags = Tags.of(WebMvcTags.method(request), WebMvcTags.uri(request, response, this.ignoreTrailingSlash),
-				WebMvcTags.exception(exception), WebMvcTags.status(response), WebMvcTags.outcome(response));
-		for (WebMvcTagsContributor contributor : this.contributors) {
-			tags = tags.and(contributor.getTags(request, response, handler, exception));
-		}
-		return tags;
-	}
-
-	@Override
-	public Iterable getLongRequestTags(HttpServletRequest request, Object handler) {
-		Tags tags = Tags.of(WebMvcTags.method(request), WebMvcTags.uri(request, null, this.ignoreTrailingSlash));
-		for (WebMvcTagsContributor contributor : this.contributors) {
-			tags = tags.and(contributor.getLongRequestTags(request, handler));
-		}
-		return tags;
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTags.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTags.java
deleted file mode 100644
index db5c1f0e49c5..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTags.java
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.metrics.web.servlet;
-
-import java.util.regex.Pattern;
-
-import io.micrometer.core.instrument.Tag;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-
-import org.springframework.boot.actuate.metrics.http.Outcome;
-import org.springframework.http.HttpStatus;
-import org.springframework.util.StringUtils;
-import org.springframework.web.servlet.HandlerMapping;
-import org.springframework.web.util.pattern.PathPattern;
-
-/**
- * Factory methods for {@link Tag Tags} associated with a request-response exchange that
- * is handled by Spring MVC.
- *
- * @author Jon Schneider
- * @author Andy Wilkinson
- * @author Brian Clozel
- * @author Michael McFadyen
- * @since 2.0.0
- * @deprecated since 3.0.0 for removal in 3.2.0 in favor of
- * {@link org.springframework.http.server.observation.ServerRequestObservationConvention}
- */
-@Deprecated(since = "3.0.0", forRemoval = true)
-public final class WebMvcTags {
-
-	private static final String DATA_REST_PATH_PATTERN_ATTRIBUTE = "org.springframework.data.rest.webmvc.RepositoryRestHandlerMapping.EFFECTIVE_REPOSITORY_RESOURCE_LOOKUP_PATH";
-
-	private static final Tag URI_NOT_FOUND = Tag.of("uri", "NOT_FOUND");
-
-	private static final Tag URI_REDIRECTION = Tag.of("uri", "REDIRECTION");
-
-	private static final Tag URI_ROOT = Tag.of("uri", "root");
-
-	private static final Tag URI_UNKNOWN = Tag.of("uri", "UNKNOWN");
-
-	private static final Tag EXCEPTION_NONE = Tag.of("exception", "None");
-
-	private static final Tag STATUS_UNKNOWN = Tag.of("status", "UNKNOWN");
-
-	private static final Tag METHOD_UNKNOWN = Tag.of("method", "UNKNOWN");
-
-	private static final Pattern TRAILING_SLASH_PATTERN = Pattern.compile("/$");
-
-	private static final Pattern MULTIPLE_SLASH_PATTERN = Pattern.compile("//+");
-
-	private WebMvcTags() {
-	}
-
-	/**
-	 * Creates a {@code method} tag based on the {@link HttpServletRequest#getMethod()
-	 * method} of the given {@code request}.
-	 * @param request the request
-	 * @return the method tag whose value is a capitalized method (e.g. GET).
-	 */
-	public static Tag method(HttpServletRequest request) {
-		return (request != null) ? Tag.of("method", request.getMethod()) : METHOD_UNKNOWN;
-	}
-
-	/**
-	 * Creates a {@code status} tag based on the status of the given {@code response}.
-	 * @param response the HTTP response
-	 * @return the status tag derived from the status of the response
-	 */
-	public static Tag status(HttpServletResponse response) {
-		return (response != null) ? Tag.of("status", Integer.toString(response.getStatus())) : STATUS_UNKNOWN;
-	}
-
-	/**
-	 * Creates a {@code uri} tag based on the URI of the given {@code request}. Uses the
-	 * {@link HandlerMapping#BEST_MATCHING_PATTERN_ATTRIBUTE} best matching pattern if
-	 * available. Falling back to {@code REDIRECTION} for 3xx responses, {@code NOT_FOUND}
-	 * for 404 responses, {@code root} for requests with no path info, and {@code UNKNOWN}
-	 * for all other requests.
-	 * @param request the request
-	 * @param response the response
-	 * @return the uri tag derived from the request
-	 */
-	public static Tag uri(HttpServletRequest request, HttpServletResponse response) {
-		return uri(request, response, false);
-	}
-
-	/**
-	 * Creates a {@code uri} tag based on the URI of the given {@code request}. Uses the
-	 * {@link HandlerMapping#BEST_MATCHING_PATTERN_ATTRIBUTE} best matching pattern if
-	 * available. Falling back to {@code REDIRECTION} for 3xx responses, {@code NOT_FOUND}
-	 * for 404 responses, {@code root} for requests with no path info, and {@code UNKNOWN}
-	 * for all other requests.
-	 * @param request the request
-	 * @param response the response
-	 * @param ignoreTrailingSlash whether to ignore the trailing slash
-	 * @return the uri tag derived from the request
-	 */
-	public static Tag uri(HttpServletRequest request, HttpServletResponse response, boolean ignoreTrailingSlash) {
-		if (request != null) {
-			String pattern = getMatchingPattern(request);
-			if (pattern != null) {
-				if (ignoreTrailingSlash && pattern.length() > 1) {
-					pattern = TRAILING_SLASH_PATTERN.matcher(pattern).replaceAll("");
-				}
-				if (pattern.isEmpty()) {
-					return URI_ROOT;
-				}
-				return Tag.of("uri", pattern);
-			}
-			if (response != null) {
-				HttpStatus status = extractStatus(response);
-				if (status != null) {
-					if (status.is3xxRedirection()) {
-						return URI_REDIRECTION;
-					}
-					if (status == HttpStatus.NOT_FOUND) {
-						return URI_NOT_FOUND;
-					}
-				}
-			}
-			String pathInfo = getPathInfo(request);
-			if (pathInfo.isEmpty()) {
-				return URI_ROOT;
-			}
-		}
-		return URI_UNKNOWN;
-	}
-
-	private static HttpStatus extractStatus(HttpServletResponse response) {
-		try {
-			return HttpStatus.valueOf(response.getStatus());
-		}
-		catch (IllegalArgumentException ex) {
-			return null;
-		}
-	}
-
-	private static String getMatchingPattern(HttpServletRequest request) {
-		PathPattern dataRestPathPattern = (PathPattern) request.getAttribute(DATA_REST_PATH_PATTERN_ATTRIBUTE);
-		if (dataRestPathPattern != null) {
-			return dataRestPathPattern.getPatternString();
-		}
-		return (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
-	}
-
-	private static String getPathInfo(HttpServletRequest request) {
-		String pathInfo = request.getPathInfo();
-		String uri = StringUtils.hasText(pathInfo) ? pathInfo : "/";
-		uri = MULTIPLE_SLASH_PATTERN.matcher(uri).replaceAll("/");
-		return TRAILING_SLASH_PATTERN.matcher(uri).replaceAll("");
-	}
-
-	/**
-	 * Creates an {@code exception} tag based on the {@link Class#getSimpleName() simple
-	 * name} of the class of the given {@code exception}.
-	 * @param exception the exception, may be {@code null}
-	 * @return the exception tag derived from the exception
-	 */
-	public static Tag exception(Throwable exception) {
-		if (exception != null) {
-			String simpleName = exception.getClass().getSimpleName();
-			return Tag.of("exception", StringUtils.hasText(simpleName) ? simpleName : exception.getClass().getName());
-		}
-		return EXCEPTION_NONE;
-	}
-
-	/**
-	 * Creates an {@code outcome} tag based on the status of the given {@code response}.
-	 * @param response the HTTP response
-	 * @return the outcome tag derived from the status of the response
-	 * @since 2.1.0
-	 */
-	public static Tag outcome(HttpServletResponse response) {
-		Outcome outcome = (response != null) ? Outcome.forStatus(response.getStatus()) : Outcome.UNKNOWN;
-		return outcome.asTag();
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTagsContributor.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTagsContributor.java
deleted file mode 100644
index f27b2115af67..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTagsContributor.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.metrics.web.servlet;
-
-import io.micrometer.core.instrument.LongTaskTimer;
-import io.micrometer.core.instrument.Tag;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-
-/**
- * A contributor of {@link Tag Tags} for Spring MVC-based request handling. Typically used
- * by a {@link WebMvcTagsProvider} to provide tags in addition to its defaults.
- *
- * @author Andy Wilkinson
- * @since 2.3.0
- * @deprecated since 3.0.0 for removal in 3.2.0 in favor of
- * {@link org.springframework.http.server.observation.ServerRequestObservationConvention}
- */
-@Deprecated(since = "3.0.0", forRemoval = true)
-public interface WebMvcTagsContributor {
-
-	/**
-	 * Provides tags to be associated with metrics for the given {@code request} and
-	 * {@code response} exchange.
-	 * @param request the request
-	 * @param response the response
-	 * @param handler the handler for the request or {@code null} if the handler is
-	 * unknown
-	 * @param exception the current exception, if any
-	 * @return tags to associate with metrics for the request and response exchange
-	 */
-	Iterable getTags(HttpServletRequest request, HttpServletResponse response, Object handler,
-			Throwable exception);
-
-	/**
-	 * Provides tags to be used by {@link LongTaskTimer long task timers}.
-	 * @param request the HTTP request
-	 * @param handler the handler for the request or {@code null} if the handler is
-	 * unknown
-	 * @return tags to associate with metrics recorded for the request
-	 */
-	Iterable getLongRequestTags(HttpServletRequest request, Object handler);
-
-}
diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTagsProvider.java
deleted file mode 100644
index 09206f727b1b..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTagsProvider.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.metrics.web.servlet;
-
-import io.micrometer.core.instrument.LongTaskTimer;
-import io.micrometer.core.instrument.Tag;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-
-/**
- * Provides {@link Tag Tags} for Spring MVC-based request handling.
- *
- * @author Jon Schneider
- * @author Andy Wilkinson
- * @since 2.0.0
- * @deprecated since 3.0.0 for removal in 3.2.0 in favor of
- * {@link org.springframework.http.server.observation.ServerRequestObservationConvention}
- */
-@Deprecated(since = "3.0.0", forRemoval = true)
-public interface WebMvcTagsProvider {
-
-	/**
-	 * Provides tags to be associated with metrics for the given {@code request} and
-	 * {@code response} exchange.
-	 * @param request the request
-	 * @param response the response
-	 * @param handler the handler for the request or {@code null} if the handler is
-	 * unknown
-	 * @param exception the current exception, if any
-	 * @return tags to associate with metrics for the request and response exchange
-	 */
-	Iterable getTags(HttpServletRequest request, HttpServletResponse response, Object handler,
-			Throwable exception);
-
-	/**
-	 * Provides tags to be used by {@link LongTaskTimer long task timers}.
-	 * @param request the HTTP request
-	 * @param handler the handler for the request or {@code null} if the handler is
-	 * unknown
-	 * @return tags to associate with metrics recorded for the request
-	 */
-	Iterable getLongRequestTags(HttpServletRequest request, Object handler);
-
-}
diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/package-info.java
deleted file mode 100644
index 22bbf429f87a..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/package-info.java
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright 2012-2019 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.
- */
-
-/**
- * Actuator support for Spring MVC metrics.
- */
-package org.springframework.boot.actuate.metrics.web.servlet;
diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/DefaultWebMvcTagsProviderTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/DefaultWebMvcTagsProviderTests.java
deleted file mode 100644
index 4bdcb94ce427..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/DefaultWebMvcTagsProviderTests.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright 2012-2023 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.actuate.endpoint.web.servlet;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-import java.util.stream.StreamSupport;
-
-import io.micrometer.core.instrument.Tag;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import org.junit.jupiter.api.Test;
-
-import org.springframework.boot.actuate.metrics.web.servlet.DefaultWebMvcTagsProvider;
-import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsContributor;
-import org.springframework.mock.web.MockHttpServletRequest;
-import org.springframework.web.servlet.HandlerMapping;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * Tests for {@link DefaultWebMvcTagsProvider}.
- *
- * @author Andy Wilkinson
- */
-@SuppressWarnings("removal")
-@Deprecated(since = "3.0.0", forRemoval = true)
-class DefaultWebMvcTagsProviderTests {
-
-	@Test
-	void whenTagsAreProvidedThenDefaultTagsArePresent() {
-		Map tags = asMap(new DefaultWebMvcTagsProvider().getTags(null, null, null, null));
-		assertThat(tags).containsOnlyKeys("exception", "method", "outcome", "status", "uri");
-	}
-
-	@Test
-	void givenSomeContributorsWhenTagsAreProvidedThenDefaultTagsAndContributedTagsArePresent() {
-		Map tags = asMap(
-				new DefaultWebMvcTagsProvider(Arrays.asList(new TestWebMvcTagsContributor("alpha"),
-						new TestWebMvcTagsContributor("bravo", "charlie")))
-					.getTags(null, null, null, null));
-		assertThat(tags).containsOnlyKeys("exception", "method", "outcome", "status", "uri", "alpha", "bravo",
-				"charlie");
-	}
-
-	@Test
-	void whenLongRequestTagsAreProvidedThenDefaultTagsArePresent() {
-		Map tags = asMap(new DefaultWebMvcTagsProvider().getLongRequestTags(null, null));
-		assertThat(tags).containsOnlyKeys("method", "uri");
-	}
-
-	@Test
-	void givenSomeContributorsWhenLongRequestTagsAreProvidedThenDefaultTagsAndContributedTagsArePresent() {
-		Map tags = asMap(
-				new DefaultWebMvcTagsProvider(Arrays.asList(new TestWebMvcTagsContributor("alpha"),
-						new TestWebMvcTagsContributor("bravo", "charlie")))
-					.getLongRequestTags(null, null));
-		assertThat(tags).containsOnlyKeys("method", "uri", "alpha", "bravo", "charlie");
-	}
-
-	@Test
-	void trailingSlashIsIncludedByDefault() {
-		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/the/uri/");
-		request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, "{one}/{two}/");
-		Map tags = asMap(new DefaultWebMvcTagsProvider().getTags(request, null, null, null));
-		assertThat(tags.get("uri").getValue()).isEqualTo("{one}/{two}/");
-	}
-
-	@Test
-	void trailingSlashCanBeIgnored() {
-		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/the/uri/");
-		request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, "{one}/{two}/");
-		Map tags = asMap(new DefaultWebMvcTagsProvider(true).getTags(request, null, null, null));
-		assertThat(tags.get("uri").getValue()).isEqualTo("{one}/{two}");
-	}
-
-	private Map asMap(Iterable tags) {
-		return StreamSupport.stream(tags.spliterator(), false)
-			.collect(Collectors.toMap(Tag::getKey, Function.identity()));
-	}
-
-	private static final class TestWebMvcTagsContributor implements WebMvcTagsContributor {
-
-		private final List tagNames;
-
-		private TestWebMvcTagsContributor(String... tagNames) {
-			this.tagNames = Arrays.asList(tagNames);
-		}
-
-		@Override
-		public Iterable getTags(HttpServletRequest request, HttpServletResponse response, Object handler,
-				Throwable exception) {
-			return this.tagNames.stream().map((name) -> Tag.of(name, "value")).toList();
-		}
-
-		@Override
-		public Iterable getLongRequestTags(HttpServletRequest request, Object handler) {
-			return this.tagNames.stream().map((name) -> Tag.of(name, "value")).toList();
-		}
-
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcTagsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcTagsTests.java
deleted file mode 100644
index 7097ee9dfd79..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcTagsTests.java
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.endpoint.web.servlet;
-
-import io.micrometer.core.instrument.Tag;
-import org.junit.jupiter.api.Test;
-
-import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTags;
-import org.springframework.mock.web.MockHttpServletRequest;
-import org.springframework.mock.web.MockHttpServletResponse;
-import org.springframework.web.servlet.HandlerMapping;
-import org.springframework.web.util.pattern.PathPatternParser;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * Tests for {@link WebMvcTags}.
- *
- * @author Andy Wilkinson
- * @author Brian Clozel
- * @author Michael McFadyen
- */
-@SuppressWarnings("removal")
-@Deprecated(since = "3.0.0", forRemoval = true)
-class WebMvcTagsTests {
-
-	private final MockHttpServletRequest request = new MockHttpServletRequest();
-
-	private final MockHttpServletResponse response = new MockHttpServletResponse();
-
-	@Test
-	void uriTagIsDataRestsEffectiveRepositoryLookupPathWhenAvailable() {
-		this.request.setAttribute(
-				"org.springframework.data.rest.webmvc.RepositoryRestHandlerMapping.EFFECTIVE_REPOSITORY_RESOURCE_LOOKUP_PATH",
-				new PathPatternParser().parse("/api/cities"));
-		this.request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, "/api/{repository}");
-		Tag tag = WebMvcTags.uri(this.request, this.response);
-		assertThat(tag.getValue()).isEqualTo("/api/cities");
-	}
-
-	@Test
-	void uriTagValueIsBestMatchingPatternWhenAvailable() {
-		this.request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, "/spring/");
-		this.response.setStatus(301);
-		Tag tag = WebMvcTags.uri(this.request, this.response);
-		assertThat(tag.getValue()).isEqualTo("/spring/");
-	}
-
-	@Test
-	void uriTagValueIsRootWhenBestMatchingPatternIsEmpty() {
-		this.request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, "");
-		this.response.setStatus(301);
-		Tag tag = WebMvcTags.uri(this.request, this.response);
-		assertThat(tag.getValue()).isEqualTo("root");
-	}
-
-	@Test
-	void uriTagValueWithBestMatchingPatternAndIgnoreTrailingSlashRemoveTrailingSlash() {
-		this.request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, "/spring/");
-		Tag tag = WebMvcTags.uri(this.request, this.response, true);
-		assertThat(tag.getValue()).isEqualTo("/spring");
-	}
-
-	@Test
-	void uriTagValueWithBestMatchingPatternAndIgnoreTrailingSlashKeepSingleSlash() {
-		this.request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, "/");
-		Tag tag = WebMvcTags.uri(this.request, this.response, true);
-		assertThat(tag.getValue()).isEqualTo("/");
-	}
-
-	@Test
-	void uriTagValueIsRootWhenRequestHasNoPatternOrPathInfo() {
-		assertThat(WebMvcTags.uri(this.request, null).getValue()).isEqualTo("root");
-	}
-
-	@Test
-	void uriTagValueIsRootWhenRequestHasNoPatternAndSlashPathInfo() {
-		this.request.setPathInfo("/");
-		assertThat(WebMvcTags.uri(this.request, null).getValue()).isEqualTo("root");
-	}
-
-	@Test
-	void uriTagValueIsUnknownWhenRequestHasNoPatternAndNonRootPathInfo() {
-		this.request.setPathInfo("/example");
-		assertThat(WebMvcTags.uri(this.request, null).getValue()).isEqualTo("UNKNOWN");
-	}
-
-	@Test
-	void uriTagValueIsRedirectionWhenResponseStatusIs3xx() {
-		this.response.setStatus(301);
-		Tag tag = WebMvcTags.uri(this.request, this.response);
-		assertThat(tag.getValue()).isEqualTo("REDIRECTION");
-	}
-
-	@Test
-	void uriTagValueIsNotFoundWhenResponseStatusIs404() {
-		this.response.setStatus(404);
-		Tag tag = WebMvcTags.uri(this.request, this.response);
-		assertThat(tag.getValue()).isEqualTo("NOT_FOUND");
-	}
-
-	@Test
-	void uriTagToleratesCustomResponseStatus() {
-		this.response.setStatus(601);
-		Tag tag = WebMvcTags.uri(this.request, this.response);
-		assertThat(tag.getValue()).isEqualTo("root");
-	}
-
-	@Test
-	void uriTagIsUnknownWhenRequestIsNull() {
-		Tag tag = WebMvcTags.uri(null, null);
-		assertThat(tag.getValue()).isEqualTo("UNKNOWN");
-	}
-
-	@Test
-	void outcomeTagIsUnknownWhenResponseIsNull() {
-		Tag tag = WebMvcTags.outcome(null);
-		assertThat(tag.getValue()).isEqualTo("UNKNOWN");
-	}
-
-	@Test
-	void outcomeTagIsInformationalWhenResponseIs1xx() {
-		this.response.setStatus(100);
-		Tag tag = WebMvcTags.outcome(this.response);
-		assertThat(tag.getValue()).isEqualTo("INFORMATIONAL");
-	}
-
-	@Test
-	void outcomeTagIsSuccessWhenResponseIs2xx() {
-		this.response.setStatus(200);
-		Tag tag = WebMvcTags.outcome(this.response);
-		assertThat(tag.getValue()).isEqualTo("SUCCESS");
-	}
-
-	@Test
-	void outcomeTagIsRedirectionWhenResponseIs3xx() {
-		this.response.setStatus(301);
-		Tag tag = WebMvcTags.outcome(this.response);
-		assertThat(tag.getValue()).isEqualTo("REDIRECTION");
-	}
-
-	@Test
-	void outcomeTagIsClientErrorWhenResponseIs4xx() {
-		this.response.setStatus(400);
-		Tag tag = WebMvcTags.outcome(this.response);
-		assertThat(tag.getValue()).isEqualTo("CLIENT_ERROR");
-	}
-
-	@Test
-	void outcomeTagIsClientErrorWhenResponseIsNonStandardInClientSeries() {
-		this.response.setStatus(490);
-		Tag tag = WebMvcTags.outcome(this.response);
-		assertThat(tag.getValue()).isEqualTo("CLIENT_ERROR");
-	}
-
-	@Test
-	void outcomeTagIsServerErrorWhenResponseIs5xx() {
-		this.response.setStatus(500);
-		Tag tag = WebMvcTags.outcome(this.response);
-		assertThat(tag.getValue()).isEqualTo("SERVER_ERROR");
-	}
-
-	@Test
-	void outcomeTagIsUnknownWhenResponseStatusIsInUnknownSeries() {
-		this.response.setStatus(701);
-		Tag tag = WebMvcTags.outcome(this.response);
-		assertThat(tag.getValue()).isEqualTo("UNKNOWN");
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManagerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManagerTests.java
index ca8d61d3108c..6960ea722752 100644
--- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManagerTests.java
+++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManagerTests.java
@@ -140,18 +140,6 @@ void shutdownWhenDoesNotOwnSchedulerDoesNotShutdownScheduler() {
 		then(otherScheduler).should(never()).shutdown();
 	}
 
-	@Test
-	@SuppressWarnings("removal")
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	void shutdownWhenShutdownOperationIsPushPerformsPushAddOnShutdown() throws Exception {
-		givenScheduleAtFixedRateWithReturnFuture();
-		PrometheusPushGatewayManager manager = new PrometheusPushGatewayManager(this.pushGateway, this.registry,
-				this.scheduler, this.pushRate, "job", this.groupingKey, ShutdownOperation.PUSH);
-		manager.shutdown();
-		then(this.future).should().cancel(false);
-		then(this.pushGateway).should().pushAdd(this.registry, "job", this.groupingKey);
-	}
-
 	@Test
 	void shutdownWhenShutdownOperationIsPostPerformsPushAddOnShutdown() throws Exception {
 		givenScheduleAtFixedRateWithReturnFuture();
diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/client/RestTemplateExchangeTagsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/client/RestTemplateExchangeTagsTests.java
deleted file mode 100644
index 7dd9a2322b37..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/client/RestTemplateExchangeTagsTests.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright 2012-2022 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.actuate.metrics.web.client;
-
-import java.io.IOException;
-import java.net.URI;
-
-import io.micrometer.core.instrument.Tag;
-import org.junit.jupiter.api.Test;
-
-import org.springframework.http.HttpStatus;
-import org.springframework.http.HttpStatusCode;
-import org.springframework.http.client.ClientHttpRequest;
-import org.springframework.http.client.ClientHttpResponse;
-import org.springframework.mock.http.client.MockClientHttpResponse;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.BDDMockito.given;
-import static org.mockito.Mockito.mock;
-
-/**
- * Tests for {@link RestTemplateExchangeTags}.
- *
- * @author Nishant Raut
- * @author Brian Clozel
- */
-@SuppressWarnings({ "removal" })
-@Deprecated(since = "3.0.0", forRemoval = true)
-class RestTemplateExchangeTagsTests {
-
-	@Test
-	void outcomeTagIsUnknownWhenResponseIsNull() {
-		Tag tag = RestTemplateExchangeTags.outcome(null);
-		assertThat(tag.getValue()).isEqualTo("UNKNOWN");
-	}
-
-	@Test
-	void outcomeTagIsInformationalWhenResponseIs1xx() {
-		ClientHttpResponse response = new MockClientHttpResponse("foo".getBytes(), HttpStatus.CONTINUE);
-		Tag tag = RestTemplateExchangeTags.outcome(response);
-		assertThat(tag.getValue()).isEqualTo("INFORMATIONAL");
-	}
-
-	@Test
-	void outcomeTagIsSuccessWhenResponseIs2xx() {
-		ClientHttpResponse response = new MockClientHttpResponse("foo".getBytes(), HttpStatus.OK);
-		Tag tag = RestTemplateExchangeTags.outcome(response);
-		assertThat(tag.getValue()).isEqualTo("SUCCESS");
-	}
-
-	@Test
-	void outcomeTagIsRedirectionWhenResponseIs3xx() {
-		ClientHttpResponse response = new MockClientHttpResponse("foo".getBytes(), HttpStatus.MOVED_PERMANENTLY);
-		Tag tag = RestTemplateExchangeTags.outcome(response);
-		assertThat(tag.getValue()).isEqualTo("REDIRECTION");
-	}
-
-	@Test
-	void outcomeTagIsClientErrorWhenResponseIs4xx() {
-		ClientHttpResponse response = new MockClientHttpResponse("foo".getBytes(), HttpStatus.BAD_REQUEST);
-		Tag tag = RestTemplateExchangeTags.outcome(response);
-		assertThat(tag.getValue()).isEqualTo("CLIENT_ERROR");
-	}
-
-	@Test
-	void outcomeTagIsServerErrorWhenResponseIs5xx() {
-		ClientHttpResponse response = new MockClientHttpResponse("foo".getBytes(), HttpStatus.BAD_GATEWAY);
-		Tag tag = RestTemplateExchangeTags.outcome(response);
-		assertThat(tag.getValue()).isEqualTo("SERVER_ERROR");
-	}
-
-	@Test
-	void outcomeTagIsUnknownWhenResponseThrowsIOException() throws Exception {
-		ClientHttpResponse response = mock(ClientHttpResponse.class);
-		given(response.getStatusCode()).willThrow(IOException.class);
-		Tag tag = RestTemplateExchangeTags.outcome(response);
-		assertThat(tag.getValue()).isEqualTo("UNKNOWN");
-	}
-
-	@Test
-	void outcomeTagIsClientErrorWhenResponseIsNonStandardInClientSeries() throws IOException {
-		ClientHttpResponse response = mock(ClientHttpResponse.class);
-		given(response.getStatusCode()).willReturn(HttpStatusCode.valueOf(490));
-		Tag tag = RestTemplateExchangeTags.outcome(response);
-		assertThat(tag.getValue()).isEqualTo("CLIENT_ERROR");
-	}
-
-	@Test
-	void outcomeTagIsUnknownWhenResponseStatusIsInUnknownSeries() throws IOException {
-		ClientHttpResponse response = mock(ClientHttpResponse.class);
-		given(response.getStatusCode()).willReturn(HttpStatusCode.valueOf(701));
-		Tag tag = RestTemplateExchangeTags.outcome(response);
-		assertThat(tag.getValue()).isEqualTo("UNKNOWN");
-	}
-
-	@Test
-	void clientNameTagIsHostOfRequestUri() {
-		ClientHttpRequest request = mock(ClientHttpRequest.class);
-		given(request.getURI()).willReturn(URI.create("https://example.org"));
-		Tag tag = RestTemplateExchangeTags.clientName(request);
-		assertThat(tag).isEqualTo(Tag.of("client.name", "example.org"));
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProviderTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProviderTests.java
deleted file mode 100644
index c04846e71696..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProviderTests.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright 2012-2023 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.actuate.metrics.web.reactive.client;
-
-import java.io.IOException;
-import java.net.URI;
-
-import io.micrometer.core.instrument.Tag;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import org.springframework.http.HttpMethod;
-import org.springframework.http.HttpStatus;
-import org.springframework.web.reactive.function.client.ClientRequest;
-import org.springframework.web.reactive.function.client.ClientResponse;
-import org.springframework.web.reactive.function.client.WebClient;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.BDDMockito.given;
-import static org.mockito.Mockito.mock;
-
-/**
- * Tests for {@link DefaultWebClientExchangeTagsProvider}
- *
- * @author Brian Clozel
- * @author Nishant Raut
- */
-@SuppressWarnings({ "deprecation", "removal" })
-class DefaultWebClientExchangeTagsProviderTests {
-
-	private static final String URI_TEMPLATE_ATTRIBUTE = WebClient.class.getName() + ".uriTemplate";
-
-	private final WebClientExchangeTagsProvider tagsProvider = new DefaultWebClientExchangeTagsProvider();
-
-	private ClientRequest request;
-
-	private ClientResponse response;
-
-	@BeforeEach
-	void setup() {
-		this.request = ClientRequest.create(HttpMethod.GET, URI.create("https://example.org/projects/spring-boot"))
-			.attribute(URI_TEMPLATE_ATTRIBUTE, "https://example.org/projects/{project}")
-			.build();
-		this.response = mock(ClientResponse.class);
-		given(this.response.statusCode()).willReturn(HttpStatus.OK);
-	}
-
-	@Test
-	void tagsShouldBePopulated() {
-		Iterable tags = this.tagsProvider.tags(this.request, this.response, null);
-		assertThat(tags).containsExactlyInAnyOrder(Tag.of("method", "GET"), Tag.of("uri", "/projects/{project}"),
-				Tag.of("client.name", "example.org"), Tag.of("status", "200"), Tag.of("outcome", "SUCCESS"));
-	}
-
-	@Test
-	void tagsWhenNoUriTemplateShouldProvideUriPath() {
-		ClientRequest request = ClientRequest
-			.create(HttpMethod.GET, URI.create("https://example.org/projects/spring-boot"))
-			.build();
-		Iterable tags = this.tagsProvider.tags(request, this.response, null);
-		assertThat(tags).containsExactlyInAnyOrder(Tag.of("method", "GET"), Tag.of("uri", "/projects/spring-boot"),
-				Tag.of("client.name", "example.org"), Tag.of("status", "200"), Tag.of("outcome", "SUCCESS"));
-	}
-
-	@Test
-	void tagsWhenIoExceptionShouldReturnIoErrorStatus() {
-		Iterable tags = this.tagsProvider.tags(this.request, null, new IOException());
-		assertThat(tags).containsExactlyInAnyOrder(Tag.of("method", "GET"), Tag.of("uri", "/projects/{project}"),
-				Tag.of("client.name", "example.org"), Tag.of("status", "IO_ERROR"), Tag.of("outcome", "UNKNOWN"));
-	}
-
-	@Test
-	void tagsWhenExceptionShouldReturnClientErrorStatus() {
-		Iterable tags = this.tagsProvider.tags(this.request, null, new IllegalArgumentException());
-		assertThat(tags).containsExactlyInAnyOrder(Tag.of("method", "GET"), Tag.of("uri", "/projects/{project}"),
-				Tag.of("client.name", "example.org"), Tag.of("status", "CLIENT_ERROR"), Tag.of("outcome", "UNKNOWN"));
-	}
-
-	@Test
-	void tagsWhenCancelledRequestShouldReturnClientErrorStatus() {
-		Iterable tags = this.tagsProvider.tags(this.request, null, null);
-		assertThat(tags).containsExactlyInAnyOrder(Tag.of("method", "GET"), Tag.of("uri", "/projects/{project}"),
-				Tag.of("client.name", "example.org"), Tag.of("status", "CLIENT_ERROR"), Tag.of("outcome", "UNKNOWN"));
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsTests.java
deleted file mode 100644
index fbc3860fb4bc..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsTests.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * Copyright 2012-2023 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.actuate.metrics.web.reactive.client;
-
-import java.io.IOException;
-import java.net.URI;
-
-import io.micrometer.core.instrument.Tag;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import org.springframework.http.HttpMethod;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.HttpStatusCode;
-import org.springframework.web.reactive.function.client.ClientRequest;
-import org.springframework.web.reactive.function.client.ClientResponse;
-import org.springframework.web.reactive.function.client.WebClient;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.BDDMockito.given;
-import static org.mockito.Mockito.mock;
-
-/**
- * Tests for {@link WebClientExchangeTags}.
- *
- * @author Brian Clozel
- * @author Nishant Raut
- */
-@SuppressWarnings({ "deprecation", "removal" })
-class WebClientExchangeTagsTests {
-
-	private static final String URI_TEMPLATE_ATTRIBUTE = WebClient.class.getName() + ".uriTemplate";
-
-	private ClientRequest request;
-
-	private ClientResponse response;
-
-	@BeforeEach
-	void setup() {
-		this.request = ClientRequest.create(HttpMethod.GET, URI.create("https://example.org/projects/spring-boot"))
-			.attribute(URI_TEMPLATE_ATTRIBUTE, "https://example.org/projects/{project}")
-			.build();
-		this.response = mock(ClientResponse.class);
-	}
-
-	@Test
-	void method() {
-		assertThat(WebClientExchangeTags.method(this.request)).isEqualTo(Tag.of("method", "GET"));
-	}
-
-	@Test
-	void uriWhenAbsoluteTemplateIsAvailableShouldReturnTemplate() {
-		assertThat(WebClientExchangeTags.uri(this.request)).isEqualTo(Tag.of("uri", "/projects/{project}"));
-	}
-
-	@Test
-	void uriWhenRelativeTemplateIsAvailableShouldReturnTemplate() {
-		this.request = ClientRequest.create(HttpMethod.GET, URI.create("https://example.org/projects/spring-boot"))
-			.attribute(URI_TEMPLATE_ATTRIBUTE, "/projects/{project}")
-			.build();
-		assertThat(WebClientExchangeTags.uri(this.request)).isEqualTo(Tag.of("uri", "/projects/{project}"));
-	}
-
-	@Test
-	void uriWhenTemplateIsMissingShouldReturnPath() {
-		this.request = ClientRequest.create(HttpMethod.GET, URI.create("https://example.org/projects/spring-boot"))
-			.build();
-		assertThat(WebClientExchangeTags.uri(this.request)).isEqualTo(Tag.of("uri", "/projects/spring-boot"));
-	}
-
-	@Test
-	void uriWhenTemplateIsMissingShouldReturnPathWithQueryParams() {
-		this.request = ClientRequest
-			.create(HttpMethod.GET, URI.create("https://example.org/projects/spring-boot?section=docs"))
-			.build();
-		assertThat(WebClientExchangeTags.uri(this.request))
-			.isEqualTo(Tag.of("uri", "/projects/spring-boot?section=docs"));
-	}
-
-	@Test
-	void clientName() {
-		assertThat(WebClientExchangeTags.clientName(this.request)).isEqualTo(Tag.of("client.name", "example.org"));
-	}
-
-	@Test
-	void status() {
-		given(this.response.statusCode()).willReturn(HttpStatus.OK);
-		assertThat(WebClientExchangeTags.status(this.response, null)).isEqualTo(Tag.of("status", "200"));
-	}
-
-	@Test
-	void statusWhenIOException() {
-		assertThat(WebClientExchangeTags.status(null, new IOException())).isEqualTo(Tag.of("status", "IO_ERROR"));
-	}
-
-	@Test
-	void statusWhenClientException() {
-		assertThat(WebClientExchangeTags.status(null, new IllegalArgumentException()))
-			.isEqualTo(Tag.of("status", "CLIENT_ERROR"));
-	}
-
-	@Test
-	void statusWhenNonStandard() {
-		given(this.response.statusCode()).willReturn(HttpStatusCode.valueOf(490));
-		assertThat(WebClientExchangeTags.status(this.response, null)).isEqualTo(Tag.of("status", "490"));
-	}
-
-	@Test
-	void statusWhenCancelled() {
-		assertThat(WebClientExchangeTags.status(null, null)).isEqualTo(Tag.of("status", "CLIENT_ERROR"));
-	}
-
-	@Test
-	void outcomeTagIsUnknownWhenResponseIsNull() {
-		Tag tag = WebClientExchangeTags.outcome(null);
-		assertThat(tag.getValue()).isEqualTo("UNKNOWN");
-	}
-
-	@Test
-	void outcomeTagIsInformationalWhenResponseIs1xx() {
-		given(this.response.statusCode()).willReturn(HttpStatus.CONTINUE);
-		Tag tag = WebClientExchangeTags.outcome(this.response);
-		assertThat(tag.getValue()).isEqualTo("INFORMATIONAL");
-	}
-
-	@Test
-	void outcomeTagIsSuccessWhenResponseIs2xx() {
-		given(this.response.statusCode()).willReturn(HttpStatus.OK);
-		Tag tag = WebClientExchangeTags.outcome(this.response);
-		assertThat(tag.getValue()).isEqualTo("SUCCESS");
-	}
-
-	@Test
-	void outcomeTagIsRedirectionWhenResponseIs3xx() {
-		given(this.response.statusCode()).willReturn(HttpStatus.MOVED_PERMANENTLY);
-		Tag tag = WebClientExchangeTags.outcome(this.response);
-		assertThat(tag.getValue()).isEqualTo("REDIRECTION");
-	}
-
-	@Test
-	void outcomeTagIsClientErrorWhenResponseIs4xx() {
-		given(this.response.statusCode()).willReturn(HttpStatus.BAD_REQUEST);
-		Tag tag = WebClientExchangeTags.outcome(this.response);
-		assertThat(tag.getValue()).isEqualTo("CLIENT_ERROR");
-	}
-
-	@Test
-	void outcomeTagIsServerErrorWhenResponseIs5xx() {
-		given(this.response.statusCode()).willReturn(HttpStatus.BAD_GATEWAY);
-		Tag tag = WebClientExchangeTags.outcome(this.response);
-		assertThat(tag.getValue()).isEqualTo("SERVER_ERROR");
-	}
-
-	@Test
-	void outcomeTagIsClientErrorWhenResponseIsNonStandardInClientSeries() {
-		given(this.response.statusCode()).willReturn(HttpStatusCode.valueOf(490));
-		Tag tag = WebClientExchangeTags.outcome(this.response);
-		assertThat(tag.getValue()).isEqualTo("CLIENT_ERROR");
-	}
-
-	@Test
-	void outcomeTagIsUnknownWhenResponseStatusIsInUnknownSeries() {
-		given(this.response.statusCode()).willReturn(HttpStatusCode.valueOf(701));
-		Tag tag = WebClientExchangeTags.outcome(this.response);
-		assertThat(tag.getValue()).isEqualTo("UNKNOWN");
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProviderTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProviderTests.java
deleted file mode 100644
index 39240b2fd341..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProviderTests.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright 2012-2023 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.actuate.metrics.web.reactive.server;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-import java.util.stream.StreamSupport;
-
-import io.micrometer.core.instrument.Tag;
-import org.junit.jupiter.api.Test;
-
-import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
-import org.springframework.mock.web.server.MockServerWebExchange;
-import org.springframework.web.server.ServerWebExchange;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * Tests for {@link DefaultWebFluxTagsProvider}.
- *
- * @author Andy Wilkinson
- */
-@SuppressWarnings("removal")
-@Deprecated(since = "3.0.0", forRemoval = true)
-class DefaultWebFluxTagsProviderTests {
-
-	@Test
-	void whenTagsAreProvidedThenDefaultTagsArePresent() {
-		ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/test"));
-		Map tags = asMap(new DefaultWebFluxTagsProvider().httpRequestTags(exchange, null));
-		assertThat(tags).containsOnlyKeys("exception", "method", "outcome", "status", "uri");
-	}
-
-	@Test
-	void givenSomeContributorsWhenTagsAreProvidedThenDefaultTagsAndContributedTagsArePresent() {
-		ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/test"));
-		Map tags = asMap(
-				new DefaultWebFluxTagsProvider(Arrays.asList(new TestWebFluxTagsContributor("alpha"),
-						new TestWebFluxTagsContributor("bravo", "charlie")))
-					.httpRequestTags(exchange, null));
-		assertThat(tags).containsOnlyKeys("exception", "method", "outcome", "status", "uri", "alpha", "bravo",
-				"charlie");
-	}
-
-	private Map asMap(Iterable tags) {
-		return StreamSupport.stream(tags.spliterator(), false)
-			.collect(Collectors.toMap(Tag::getKey, Function.identity()));
-	}
-
-	private static final class TestWebFluxTagsContributor implements WebFluxTagsContributor {
-
-		private final List tagNames;
-
-		private TestWebFluxTagsContributor(String... tagNames) {
-			this.tagNames = Arrays.asList(tagNames);
-		}
-
-		@Override
-		public Iterable httpRequestTags(ServerWebExchange exchange, Throwable ex) {
-			return this.tagNames.stream().map((name) -> Tag.of(name, "value")).toList();
-		}
-
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsTests.java
deleted file mode 100644
index 03d7a1030258..000000000000
--- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsTests.java
+++ /dev/null
@@ -1,220 +0,0 @@
-/*
- * Copyright 2012-2023 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.actuate.metrics.web.reactive.server;
-
-import java.io.EOFException;
-
-import io.micrometer.core.instrument.Tag;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import org.springframework.http.HttpMethod;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.server.reactive.ServerHttpRequest;
-import org.springframework.http.server.reactive.ServerHttpResponse;
-import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
-import org.springframework.mock.web.server.MockServerWebExchange;
-import org.springframework.web.reactive.HandlerMapping;
-import org.springframework.web.server.ServerWebExchange;
-import org.springframework.web.util.pattern.PathPatternParser;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.BDDMockito.given;
-import static org.mockito.Mockito.mock;
-
-/**
- * Tests for {@link WebFluxTags}.
- *
- * @author Brian Clozel
- * @author Michael McFadyen
- * @author Madhura Bhave
- * @author Stephane Nicoll
- */
-@SuppressWarnings("removal")
-@Deprecated(since = "3.0.0", forRemoval = true)
-class WebFluxTagsTests {
-
-	private MockServerWebExchange exchange;
-
-	private final PathPatternParser parser = new PathPatternParser();
-
-	@BeforeEach
-	void setup() {
-		this.exchange = MockServerWebExchange.from(MockServerHttpRequest.get(""));
-	}
-
-	@Test
-	void uriTagValueIsBestMatchingPatternWhenAvailable() {
-		this.exchange.getAttributes()
-			.put(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, this.parser.parse("/spring/"));
-		this.exchange.getResponse().setStatusCode(HttpStatus.MOVED_PERMANENTLY);
-		Tag tag = WebFluxTags.uri(this.exchange);
-		assertThat(tag.getValue()).isEqualTo("/spring/");
-	}
-
-	@Test
-	void uriTagValueIsRootWhenBestMatchingPatternIsEmpty() {
-		this.exchange.getAttributes().put(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, this.parser.parse(""));
-		this.exchange.getResponse().setStatusCode(HttpStatus.MOVED_PERMANENTLY);
-		Tag tag = WebFluxTags.uri(this.exchange);
-		assertThat(tag.getValue()).isEqualTo("root");
-	}
-
-	@Test
-	void uriTagValueWithBestMatchingPatternAndIgnoreTrailingSlashRemoveTrailingSlash() {
-		this.exchange.getAttributes()
-			.put(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, this.parser.parse("/spring/"));
-		Tag tag = WebFluxTags.uri(this.exchange, true);
-		assertThat(tag.getValue()).isEqualTo("/spring");
-	}
-
-	@Test
-	void uriTagValueWithBestMatchingPatternAndIgnoreTrailingSlashKeepSingleSlash() {
-		this.exchange.getAttributes().put(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, this.parser.parse("/"));
-		Tag tag = WebFluxTags.uri(this.exchange, true);
-		assertThat(tag.getValue()).isEqualTo("/");
-	}
-
-	@Test
-	void uriTagValueIsRedirectionWhenResponseStatusIs3xx() {
-		this.exchange.getResponse().setStatusCode(HttpStatus.MOVED_PERMANENTLY);
-		Tag tag = WebFluxTags.uri(this.exchange);
-		assertThat(tag.getValue()).isEqualTo("REDIRECTION");
-	}
-
-	@Test
-	void uriTagValueIsNotFoundWhenResponseStatusIs404() {
-		this.exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND);
-		Tag tag = WebFluxTags.uri(this.exchange);
-		assertThat(tag.getValue()).isEqualTo("NOT_FOUND");
-	}
-
-	@Test
-	void uriTagToleratesCustomResponseStatus() {
-		this.exchange.getResponse().setRawStatusCode(601);
-		Tag tag = WebFluxTags.uri(this.exchange);
-		assertThat(tag.getValue()).isEqualTo("root");
-	}
-
-	@Test
-	void uriTagValueIsRootWhenRequestHasNoPatternOrPathInfo() {
-		Tag tag = WebFluxTags.uri(this.exchange);
-		assertThat(tag.getValue()).isEqualTo("root");
-	}
-
-	@Test
-	void uriTagValueIsRootWhenRequestHasNoPatternAndSlashPathInfo() {
-		MockServerHttpRequest request = MockServerHttpRequest.get("/").build();
-		ServerWebExchange exchange = MockServerWebExchange.from(request);
-		Tag tag = WebFluxTags.uri(exchange);
-		assertThat(tag.getValue()).isEqualTo("root");
-	}
-
-	@Test
-	void uriTagValueIsUnknownWhenRequestHasNoPatternAndNonRootPathInfo() {
-		MockServerHttpRequest request = MockServerHttpRequest.get("/example").build();
-		ServerWebExchange exchange = MockServerWebExchange.from(request);
-		Tag tag = WebFluxTags.uri(exchange);
-		assertThat(tag.getValue()).isEqualTo("UNKNOWN");
-	}
-
-	@Test
-	void methodTagToleratesNonStandardHttpMethods() {
-		ServerWebExchange exchange = mock(ServerWebExchange.class);
-		ServerHttpRequest request = mock(ServerHttpRequest.class);
-		given(exchange.getRequest()).willReturn(request);
-		given(request.getMethod()).willReturn(HttpMethod.valueOf("CUSTOM"));
-		Tag tag = WebFluxTags.method(exchange);
-		assertThat(tag.getValue()).isEqualTo("CUSTOM");
-	}
-
-	@Test
-	void outcomeTagIsSuccessWhenResponseStatusIsNull() {
-		this.exchange.getResponse().setStatusCode(null);
-		Tag tag = WebFluxTags.outcome(this.exchange, null);
-		assertThat(tag.getValue()).isEqualTo("SUCCESS");
-	}
-
-	@Test
-	void outcomeTagIsSuccessWhenResponseStatusIsAvailableFromUnderlyingServer() {
-		ServerWebExchange exchange = mock(ServerWebExchange.class);
-		ServerHttpRequest request = mock(ServerHttpRequest.class);
-		ServerHttpResponse response = mock(ServerHttpResponse.class);
-		given(response.getStatusCode()).willReturn(HttpStatus.OK);
-		given(response.getStatusCode().value()).willReturn(null);
-		given(exchange.getRequest()).willReturn(request);
-		given(exchange.getResponse()).willReturn(response);
-		Tag tag = WebFluxTags.outcome(exchange, null);
-		assertThat(tag.getValue()).isEqualTo("SUCCESS");
-	}
-
-	@Test
-	void outcomeTagIsInformationalWhenResponseIs1xx() {
-		this.exchange.getResponse().setStatusCode(HttpStatus.CONTINUE);
-		Tag tag = WebFluxTags.outcome(this.exchange, null);
-		assertThat(tag.getValue()).isEqualTo("INFORMATIONAL");
-	}
-
-	@Test
-	void outcomeTagIsSuccessWhenResponseIs2xx() {
-		this.exchange.getResponse().setStatusCode(HttpStatus.OK);
-		Tag tag = WebFluxTags.outcome(this.exchange, null);
-		assertThat(tag.getValue()).isEqualTo("SUCCESS");
-	}
-
-	@Test
-	void outcomeTagIsRedirectionWhenResponseIs3xx() {
-		this.exchange.getResponse().setStatusCode(HttpStatus.MOVED_PERMANENTLY);
-		Tag tag = WebFluxTags.outcome(this.exchange, null);
-		assertThat(tag.getValue()).isEqualTo("REDIRECTION");
-	}
-
-	@Test
-	void outcomeTagIsClientErrorWhenResponseIs4xx() {
-		this.exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
-		Tag tag = WebFluxTags.outcome(this.exchange, null);
-		assertThat(tag.getValue()).isEqualTo("CLIENT_ERROR");
-	}
-
-	@Test
-	void outcomeTagIsServerErrorWhenResponseIs5xx() {
-		this.exchange.getResponse().setStatusCode(HttpStatus.BAD_GATEWAY);
-		Tag tag = WebFluxTags.outcome(this.exchange, null);
-		assertThat(tag.getValue()).isEqualTo("SERVER_ERROR");
-	}
-
-	@Test
-	void outcomeTagIsClientErrorWhenResponseIsNonStandardInClientSeries() {
-		this.exchange.getResponse().setRawStatusCode(490);
-		Tag tag = WebFluxTags.outcome(this.exchange, null);
-		assertThat(tag.getValue()).isEqualTo("CLIENT_ERROR");
-	}
-
-	@Test
-	void outcomeTagIsUnknownWhenResponseStatusIsInUnknownSeries() {
-		this.exchange.getResponse().setRawStatusCode(701);
-		Tag tag = WebFluxTags.outcome(this.exchange, null);
-		assertThat(tag.getValue()).isEqualTo("UNKNOWN");
-	}
-
-	@Test
-	void outcomeTagIsUnknownWhenExceptionIsDisconnectedClient() {
-		Tag tag = WebFluxTags.outcome(this.exchange, new EOFException("broken pipe"));
-		assertThat(tag.getValue()).isEqualTo("UNKNOWN");
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java
index 0c5928ed27c3..e65ef1fa54e5 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java
@@ -127,16 +127,6 @@ PropertiesFlywayConnectionDetails flywayConnectionDetails(FlywayProperties prope
 			return new PropertiesFlywayConnectionDetails(properties);
 		}
 
-		@Deprecated(since = "3.0.0", forRemoval = true)
-		public Flyway flyway(FlywayProperties properties, ResourceLoader resourceLoader,
-				ObjectProvider dataSource, ObjectProvider flywayDataSource,
-				ObjectProvider fluentConfigurationCustomizers,
-				ObjectProvider javaMigrations, ObjectProvider callbacks) {
-			return flyway(properties, new PropertiesFlywayConnectionDetails(properties), resourceLoader, dataSource,
-					flywayDataSource, fluentConfigurationCustomizers, javaMigrations, callbacks,
-					new ResourceProviderCustomizer());
-		}
-
 		@Bean
 		Flyway flyway(FlywayProperties properties, FlywayConnectionDetails connectionDetails,
 				ResourceLoader resourceLoader, ObjectProvider dataSource,
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java
index 478897d053d9..c14779f3305e 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java
@@ -22,7 +22,6 @@
 import liquibase.integration.spring.SpringLiquibase;
 
 import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
 import org.springframework.util.Assert;
 
 /**
@@ -257,17 +256,6 @@ public void setLabelFilter(String labelFilter) {
 		this.labelFilter = labelFilter;
 	}
 
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	@DeprecatedConfigurationProperty(replacement = "spring.liquibase.label-filter")
-	public String getLabels() {
-		return getLabelFilter();
-	}
-
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	public void setLabels(String labels) {
-		setLabelFilter(labels);
-	}
-
 	public Map getParameters() {
 		return this.parameters;
 	}
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionAutoConfiguration.java
index 4d6329462831..b5e36068a8a3 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionAutoConfiguration.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionAutoConfiguration.java
@@ -41,8 +41,8 @@
 import org.springframework.boot.autoconfigure.web.reactive.WebSessionIdResolverAutoConfiguration;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.boot.context.properties.PropertyMapper;
+import org.springframework.boot.web.server.Cookie;
 import org.springframework.boot.web.server.Cookie.SameSite;
-import org.springframework.boot.web.servlet.server.Session.Cookie;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Conditional;
 import org.springframework.context.annotation.Configuration;
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java
index 681bd7d8384a..1a053cd529b0 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java
@@ -154,17 +154,6 @@ public void setServerHeader(String serverHeader) {
 		this.serverHeader = serverHeader;
 	}
 
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	@DeprecatedConfigurationProperty
-	public DataSize getMaxHttpHeaderSize() {
-		return getMaxHttpRequestHeaderSize();
-	}
-
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	public void setMaxHttpHeaderSize(DataSize maxHttpHeaderSize) {
-		setMaxHttpRequestHeaderSize(maxHttpHeaderSize);
-	}
-
 	public DataSize getMaxHttpRequestHeaderSize() {
 		return this.maxHttpRequestHeaderSize;
 	}
@@ -475,7 +464,7 @@ public static class Tomcat {
 		/**
 		 * Whether to reject requests with illegal header names or values.
 		 */
-		@Deprecated(since = "2.7.12", forRemoval = true)
+		@Deprecated(since = "2.7.12", forRemoval = true) // Remove in 3.3
 		private boolean rejectIllegalHeader = true;
 
 		/**
@@ -1409,11 +1398,6 @@ public static class Netty {
 		 */
 		private DataSize initialBufferSize = DataSize.ofBytes(128);
 
-		/**
-		 * Maximum chunk size that can be decoded for an HTTP request.
-		 */
-		private DataSize maxChunkSize = DataSize.ofKilobytes(8);
-
 		/**
 		 * Maximum length that can be decoded for an HTTP request's initial line.
 		 */
@@ -1460,17 +1444,6 @@ public void setInitialBufferSize(DataSize initialBufferSize) {
 			this.initialBufferSize = initialBufferSize;
 		}
 
-		@Deprecated(since = "3.0.0", forRemoval = true)
-		@DeprecatedConfigurationProperty(reason = "Deprecated for removal in Reactor Netty")
-		public DataSize getMaxChunkSize() {
-			return this.maxChunkSize;
-		}
-
-		@Deprecated(since = "3.0.0", forRemoval = true)
-		public void setMaxChunkSize(DataSize maxChunkSize) {
-			this.maxChunkSize = maxChunkSize;
-		}
-
 		public DataSize getMaxInitialLineLength() {
 			return this.maxInitialLineLength;
 		}
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java
index c1d6ba684b8d..acfade5b98d6 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java
@@ -19,7 +19,6 @@
 import java.time.Duration;
 
 import io.netty.channel.ChannelOption;
-import reactor.netty.http.server.HttpRequestDecoderSpec;
 
 import org.springframework.boot.autoconfigure.web.ServerProperties;
 import org.springframework.boot.cloud.CloudPlatform;
@@ -87,7 +86,6 @@ private void customizeRequestDecoder(NettyReactiveWebServerFactory factory, Prop
 				.to((maxHttpRequestHeader) -> httpRequestDecoderSpec
 					.maxHeaderSize((int) maxHttpRequestHeader.toBytes()));
 			ServerProperties.Netty nettyProperties = this.serverProperties.getNetty();
-			maxChunkSize(propertyMapper, httpRequestDecoderSpec, nettyProperties);
 			propertyMapper.from(nettyProperties.getMaxInitialLineLength())
 				.whenNonNull()
 				.to((maxInitialLineLength) -> httpRequestDecoderSpec
@@ -106,14 +104,6 @@ private void customizeRequestDecoder(NettyReactiveWebServerFactory factory, Prop
 		}));
 	}
 
-	@SuppressWarnings({ "deprecation", "removal" })
-	private void maxChunkSize(PropertyMapper propertyMapper, HttpRequestDecoderSpec httpRequestDecoderSpec,
-			ServerProperties.Netty nettyProperties) {
-		propertyMapper.from(nettyProperties.getMaxChunkSize())
-			.whenNonNull()
-			.to((maxChunkSize) -> httpRequestDecoderSpec.maxChunkSize((int) maxChunkSize.toBytes()));
-	}
-
 	private void customizeIdleTimeout(NettyReactiveWebServerFactory factory, Duration idleTimeout) {
 		factory.addServerCustomizers((httpServer) -> httpServer.idleTimeout(idleTimeout));
 	}
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java
index e92256d6004a..f3e8097248fc 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java
@@ -31,7 +31,6 @@
 import org.springframework.beans.factory.ListableBeanFactory;
 import org.springframework.beans.factory.NoSuchBeanDefinitionException;
 import org.springframework.beans.factory.ObjectProvider;
-import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.AutoConfigureOrder;
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@@ -403,24 +402,6 @@ public EnableWebMvcConfiguration(WebMvcProperties mvcProperties, WebProperties w
 			this.beanFactory = beanFactory;
 		}
 
-		@Bean
-		@Override
-		public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
-				@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
-				@Qualifier("mvcConversionService") FormattingConversionService conversionService,
-				@Qualifier("mvcValidator") Validator validator) {
-			RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter(contentNegotiationManager,
-					conversionService, validator);
-			setIgnoreDefaultModelOnRedirect(adapter);
-			return adapter;
-		}
-
-		@SuppressWarnings({ "deprecation", "removal" })
-		private void setIgnoreDefaultModelOnRedirect(RequestMappingHandlerAdapter adapter) {
-			adapter.setIgnoreDefaultModelOnRedirect(
-					this.mvcProperties == null || this.mvcProperties.isIgnoreDefaultModelOnRedirect());
-		}
-
 		@Override
 		protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
 			if (this.mvcRegistrations != null) {
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java
index 6037fa974a84..0c73524c8f94 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java
@@ -21,7 +21,6 @@
 import java.util.Map;
 
 import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
 import org.springframework.http.MediaType;
 import org.springframework.util.Assert;
 import org.springframework.validation.DefaultMessageCodesResolver;
@@ -57,12 +56,6 @@ public class WebMvcProperties {
 	 */
 	private boolean dispatchOptionsRequest = true;
 
-	/**
-	 * Whether the content of the "default" model should be ignored during redirect
-	 * scenarios.
-	 */
-	private boolean ignoreDefaultModelOnRedirect = true;
-
 	/**
 	 * Whether to publish a ServletRequestHandledEvent at the end of each request.
 	 */
@@ -120,17 +113,6 @@ public Format getFormat() {
 		return this.format;
 	}
 
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	@DeprecatedConfigurationProperty(reason = "Deprecated for removal in Spring MVC")
-	public boolean isIgnoreDefaultModelOnRedirect() {
-		return this.ignoreDefaultModelOnRedirect;
-	}
-
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	public void setIgnoreDefaultModelOnRedirect(boolean ignoreDefaultModelOnRedirect) {
-		this.ignoreDefaultModelOnRedirect = ignoreDefaultModelOnRedirect;
-	}
-
 	public boolean isPublishRequestHandledEvents() {
 		return this.publishRequestHandledEvents;
 	}
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 dae3a6e6cdf0..be821293afaa 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
@@ -115,6 +115,13 @@
         "level": "error"
       }
     },
+    {
+      "name": "server.max-http-header-size",
+      "deprecation": {
+        "replacement": "server.max-http-request-header-size",
+        "level": "error"
+      }
+    },
     {
       "name": "server.max-http-post-size",
       "type": "java.lang.Integer",
@@ -125,6 +132,13 @@
         "level": "error"
       }
     },
+    {
+      "name": "server.netty.max-chunk-size",
+      "deprecation": {
+        "reason": "Deprecated for removal in Reactor Netty.",
+        "level": "error"
+      }
+    },
     {
       "name": "server.port",
       "defaultValue": 8080
@@ -210,7 +224,10 @@
     },
     {
       "name": "server.servlet.session.cookie.comment",
-      "description": "Comment for the cookie."
+      "description": "Comment for the cookie.",
+      "deprecation": {
+        "level": "error"
+      }
     },
     {
       "name": "server.servlet.session.cookie.domain",
@@ -2043,6 +2060,13 @@
         "level": "error"
       }
     },
+    {
+      "name": "spring.liquibase.labels",
+      "deprecation": {
+        "replacement": "spring.liquibase.label-filter",
+        "level": "error"
+      }
+    },
     {
       "name": "spring.mail.test-connection",
       "description": "Whether to test that the mail server is available on startup.",
@@ -2105,6 +2129,13 @@
       "description": "Whether to enable Spring's HiddenHttpMethodFilter.",
       "defaultValue": false
     },
+    {
+      "name": "spring.mvc.ignore-default-model-on-redirect",
+      "deprecation": {
+        "reason": "Deprecated for removal in Spring MVC.",
+        "level": "error"
+      }
+    },
     {
       "name": "spring.mvc.locale",
       "type": "java.util.Locale",
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java
index b05e9b0f010d..3c66a6009707 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java
@@ -379,14 +379,6 @@ void overrideLabelFilter() {
 			.run(assertLiquibase((liquibase) -> assertThat(liquibase.getLabelFilter()).isEqualTo("test, production")));
 	}
 
-	@Test
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	void overrideLabelFilterWithDeprecatedLabelsProperty() {
-		this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class)
-			.withPropertyValues("spring.liquibase.labels:test, production")
-			.run(assertLiquibase((liquibase) -> assertThat(liquibase.getLabelFilter()).isEqualTo("test, production")));
-	}
-
 	@Test
 	@SuppressWarnings("unchecked")
 	void testOverrideParameters() {
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java
index eaf0b45c2b0a..379aa6e32ce4 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java
@@ -205,24 +205,6 @@ void testCustomizeUriEncoding() {
 		assertThat(this.properties.getTomcat().getUriEncoding()).isEqualTo(StandardCharsets.US_ASCII);
 	}
 
-	@Test
-	@SuppressWarnings("removal")
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	void testCustomizeHeaderSize() {
-		bind("server.max-http-header-size", "1MB");
-		assertThat(this.properties.getMaxHttpHeaderSize()).isEqualTo(DataSize.ofMegabytes(1));
-		assertThat(this.properties.getMaxHttpRequestHeaderSize()).isEqualTo(DataSize.ofMegabytes(1));
-	}
-
-	@Test
-	@SuppressWarnings("removal")
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	void testCustomizeHeaderSizeUseBytesByDefault() {
-		bind("server.max-http-header-size", "1024");
-		assertThat(this.properties.getMaxHttpHeaderSize()).isEqualTo(DataSize.ofKilobytes(1));
-		assertThat(this.properties.getMaxHttpRequestHeaderSize()).isEqualTo(DataSize.ofKilobytes(1));
-	}
-
 	@Test
 	void testCustomizeMaxHttpRequestHeaderSize() {
 		bind("server.max-http-request-header-size", "1MB");
@@ -538,14 +520,6 @@ void undertowMaxHttpPostSizeMatchesDefault() {
 			.isEqualTo(UndertowOptions.DEFAULT_MAX_ENTITY_SIZE);
 	}
 
-	@Test
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	@SuppressWarnings("removal")
-	void nettyMaxChunkSizeMatchesHttpDecoderSpecDefault() {
-		assertThat(this.properties.getNetty().getMaxChunkSize().toBytes())
-			.isEqualTo(HttpDecoderSpec.DEFAULT_MAX_CHUNK_SIZE);
-	}
-
 	@Test
 	void nettyMaxInitialLineLengthMatchesHttpDecoderSpecDefault() {
 		assertThat(this.properties.getNetty().getMaxInitialLineLength().toBytes())
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java
index c024fc15c6cf..a4367e150642 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java
@@ -263,30 +263,6 @@ void setUseForwardHeaders() {
 		then(factory).should().setUseForwardHeaders(true);
 	}
 
-	@Test
-	void customizeMaxHttpHeaderSize() {
-		bind("server.max-http-header-size=2048");
-		JettyWebServer server = customizeAndGetServer();
-		List requestHeaderSizes = getRequestHeaderSizes(server);
-		assertThat(requestHeaderSizes).containsOnly(2048);
-	}
-
-	@Test
-	void customMaxHttpHeaderSizeIgnoredIfNegative() {
-		bind("server.max-http-header-size=-1");
-		JettyWebServer server = customizeAndGetServer();
-		List requestHeaderSizes = getRequestHeaderSizes(server);
-		assertThat(requestHeaderSizes).containsOnly(8192);
-	}
-
-	@Test
-	void customMaxHttpHeaderSizeIgnoredIfZero() {
-		bind("server.max-http-header-size=0");
-		JettyWebServer server = customizeAndGetServer();
-		List requestHeaderSizes = getRequestHeaderSizes(server);
-		assertThat(requestHeaderSizes).containsOnly(8192);
-	}
-
 	@Test
 	void customizeMaxRequestHttpHeaderSize() {
 		bind("server.max-http-request-header-size=2048");
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java
index e2267a005a74..51945e666a5a 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java
@@ -132,7 +132,6 @@ void configureHttpRequestDecoder() {
 		nettyProperties.setValidateHeaders(false);
 		nettyProperties.setInitialBufferSize(DataSize.ofBytes(512));
 		nettyProperties.setH2cMaxContentLength(DataSize.ofKilobytes(1));
-		setMaxChunkSize(nettyProperties);
 		nettyProperties.setMaxInitialLineLength(DataSize.ofKilobytes(32));
 		NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class);
 		this.customizer.customize(factory);
@@ -143,20 +142,9 @@ void configureHttpRequestDecoder() {
 		assertThat(decoder.validateHeaders()).isFalse();
 		assertThat(decoder.initialBufferSize()).isEqualTo(nettyProperties.getInitialBufferSize().toBytes());
 		assertThat(decoder.h2cMaxContentLength()).isEqualTo(nettyProperties.getH2cMaxContentLength().toBytes());
-		assertMaxChunkSize(nettyProperties, decoder);
 		assertThat(decoder.maxInitialLineLength()).isEqualTo(nettyProperties.getMaxInitialLineLength().toBytes());
 	}
 
-	@SuppressWarnings("removal")
-	private void setMaxChunkSize(ServerProperties.Netty nettyProperties) {
-		nettyProperties.setMaxChunkSize(DataSize.ofKilobytes(16));
-	}
-
-	@SuppressWarnings({ "deprecation", "removal" })
-	private void assertMaxChunkSize(ServerProperties.Netty nettyProperties, HttpRequestDecoderSpec decoder) {
-		assertThat(decoder.maxChunkSize()).isEqualTo(nettyProperties.getMaxChunkSize().toBytes());
-	}
-
 	private void verifyConnectionTimeout(NettyReactiveWebServerFactory factory, Integer expected) {
 		if (expected == null) {
 			then(factory).should(never()).addServerCustomizers(any(NettyServerCustomizer.class));
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java
index c24b4f179cde..f885699393ad 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java
@@ -176,47 +176,6 @@ void customMaxHttpFormPostSize() {
 				(server) -> assertThat(server.getTomcat().getConnector().getMaxPostSize()).isEqualTo(10000));
 	}
 
-	@Test
-	void customMaxHttpHeaderSize() {
-		bind("server.max-http-header-size=1KB");
-		customizeAndRunServer((server) -> assertThat(
-				((AbstractHttp11Protocol) server.getTomcat().getConnector().getProtocolHandler())
-					.getMaxHttpRequestHeaderSize())
-			.isEqualTo(DataSize.ofKilobytes(1).toBytes()));
-	}
-
-	@Test
-	void customMaxHttpHeaderSizeWithHttp2() {
-		bind("server.max-http-header-size=1KB", "server.http2.enabled=true");
-		customizeAndRunServer((server) -> {
-			AbstractHttp11Protocol protocolHandler = (AbstractHttp11Protocol) server.getTomcat()
-				.getConnector()
-				.getProtocolHandler();
-			long expectedSize = DataSize.ofKilobytes(1).toBytes();
-			assertThat(protocolHandler.getMaxHttpRequestHeaderSize()).isEqualTo(expectedSize);
-			assertThat(((Http2Protocol) protocolHandler.getUpgradeProtocol("h2c")).getMaxHeaderSize())
-				.isEqualTo(expectedSize);
-		});
-	}
-
-	@Test
-	void customMaxHttpHeaderSizeIgnoredIfNegative() {
-		bind("server.max-http-header-size=-1");
-		customizeAndRunServer((server) -> assertThat(
-				((AbstractHttp11Protocol) server.getTomcat().getConnector().getProtocolHandler())
-					.getMaxHttpRequestHeaderSize())
-			.isEqualTo(DataSize.ofKilobytes(8).toBytes()));
-	}
-
-	@Test
-	void customMaxHttpHeaderSizeIgnoredIfZero() {
-		bind("server.max-http-header-size=0");
-		customizeAndRunServer((server) -> assertThat(
-				((AbstractHttp11Protocol) server.getTomcat().getConnector().getProtocolHandler())
-					.getMaxHttpRequestHeaderSize())
-			.isEqualTo(DataSize.ofKilobytes(8).toBytes()));
-	}
-
 	@Test
 	void defaultMaxHttpRequestHeaderSize() {
 		customizeAndRunServer((server) -> assertThat(
@@ -436,16 +395,6 @@ void disableRemoteIpValve() {
 		assertThat(factory.getEngineValves()).isEmpty();
 	}
 
-	@Test
-	@Deprecated(since = "2.7.12", forRemoval = true)
-	void testCustomizeRejectIllegalHeader() {
-		bind("server.tomcat.reject-illegal-header=false");
-		customizeAndRunServer((server) -> assertThat(
-				((AbstractHttp11Protocol) server.getTomcat().getConnector().getProtocolHandler())
-					.getRejectIllegalHeader())
-			.isFalse());
-	}
-
 	@Test
 	void errorReportValveIsConfiguredToNotReportStackTraces() {
 		TomcatWebServer server = customizeAndGetServer();
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizerTests.java
index 15289d60e41b..e5a2c95e8271 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizerTests.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizerTests.java
@@ -86,20 +86,20 @@ void customizeUndertowAccessLog() {
 	}
 
 	@Test
-	void customMaxHttpHeaderSize() {
-		bind("server.max-http-header-size=2048");
+	void customMaxHttpRequestHeaderSize() {
+		bind("server.max-http-request-header-size=2048");
 		assertThat(boundServerOption(UndertowOptions.MAX_HEADER_SIZE)).isEqualTo(2048);
 	}
 
 	@Test
-	void customMaxHttpHeaderSizeIgnoredIfNegative() {
-		bind("server.max-http-header-size=-1");
+	void customMaxHttpRequestHeaderSizeIgnoredIfNegative() {
+		bind("server.max-http-request-header-size=-1");
 		assertThat(boundServerOption(UndertowOptions.MAX_HEADER_SIZE)).isNull();
 	}
 
 	@Test
-	void customMaxHttpHeaderSizeIgnoredIfZero() {
-		bind("server.max-http-header-size=0");
+	void customMaxHttpRequestHeaderSizeIgnoredIfZero() {
+		bind("server.max-http-request-header-size=0");
 		assertThat(boundServerOption(UndertowOptions.MAX_HEADER_SIZE)).isNull();
 	}
 
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizerTests.java
index 7cff13798441..c428ff17819f 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizerTests.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizerTests.java
@@ -28,11 +28,11 @@
 import org.springframework.boot.context.properties.bind.Binder;
 import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
 import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
+import org.springframework.boot.web.server.Cookie;
 import org.springframework.boot.web.server.Shutdown;
 import org.springframework.boot.web.server.Ssl;
 import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
 import org.springframework.boot.web.servlet.server.Jsp;
-import org.springframework.boot.web.servlet.server.Session.Cookie;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.any;
@@ -97,7 +97,6 @@ void testCustomizeJsp() {
 	}
 
 	@Test
-	@SuppressWarnings("removal")
 	void customizeSessionProperties() {
 		Map map = new HashMap<>();
 		map.put("server.servlet.session.timeout", "123");
@@ -105,7 +104,6 @@ void customizeSessionProperties() {
 		map.put("server.servlet.session.cookie.name", "testname");
 		map.put("server.servlet.session.cookie.domain", "testdomain");
 		map.put("server.servlet.session.cookie.path", "/testpath");
-		map.put("server.servlet.session.cookie.comment", "testcomment");
 		map.put("server.servlet.session.cookie.http-only", "true");
 		map.put("server.servlet.session.cookie.secure", "true");
 		map.put("server.servlet.session.cookie.max-age", "60");
@@ -118,7 +116,6 @@ void customizeSessionProperties() {
 			assertThat(cookie.getName()).isEqualTo("testname");
 			assertThat(cookie.getDomain()).isEqualTo("testdomain");
 			assertThat(cookie.getPath()).isEqualTo("/testpath");
-			assertThat(cookie.getComment()).isEqualTo("testcomment");
 			assertThat(cookie.getHttpOnly()).isTrue();
 			assertThat(cookie.getMaxAge()).hasSeconds(60);
 		}));
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java
index 4b8737d0424c..c2ebbb6d1588 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java
@@ -380,29 +380,6 @@ void customLocaleResolverWithDifferentNameDoesNotReplaceAutoConfiguredLocaleReso
 			});
 	}
 
-	@Test
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	@SuppressWarnings("deprecation")
-	void customThemeResolverWithMatchingNameReplacesDefaultThemeResolver() {
-		this.contextRunner.withBean("themeResolver", CustomThemeResolver.class, CustomThemeResolver::new)
-			.run((context) -> {
-				assertThat(context).hasSingleBean(org.springframework.web.servlet.ThemeResolver.class);
-				assertThat(context.getBean("themeResolver")).isInstanceOf(CustomThemeResolver.class);
-			});
-	}
-
-	@Test
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	@SuppressWarnings("deprecation")
-	void customThemeResolverWithDifferentNameDoesNotReplaceDefaultThemeResolver() {
-		this.contextRunner.withBean("customThemeResolver", CustomThemeResolver.class, CustomThemeResolver::new)
-			.run((context) -> {
-				assertThat(context.getBean("customThemeResolver")).isInstanceOf(CustomThemeResolver.class);
-				assertThat(context.getBean("themeResolver"))
-					.isInstanceOf(org.springframework.web.servlet.theme.FixedThemeResolver.class);
-			});
-	}
-
 	@Test
 	void customFlashMapManagerWithMatchingNameReplacesDefaultFlashMapManager() {
 		this.contextRunner.withBean("flashMapManager", CustomFlashMapManager.class, CustomFlashMapManager::new)
@@ -493,21 +470,6 @@ void overrideMessageCodesFormat() {
 				.isNotNull());
 	}
 
-	@Test
-	void ignoreDefaultModelOnRedirectIsTrue() {
-		this.contextRunner.run((context) -> assertThat(context.getBean(RequestMappingHandlerAdapter.class))
-			.extracting("ignoreDefaultModelOnRedirect")
-			.isEqualTo(true));
-	}
-
-	@Test
-	void overrideIgnoreDefaultModelOnRedirect() {
-		this.contextRunner.withPropertyValues("spring.mvc.ignore-default-model-on-redirect:false")
-			.run((context) -> assertThat(context.getBean(RequestMappingHandlerAdapter.class))
-				.extracting("ignoreDefaultModelOnRedirect")
-				.isEqualTo(false));
-	}
-
 	@Test
 	void customViewResolver() {
 		this.contextRunner.withUserConfiguration(CustomViewResolver.class)
@@ -1464,20 +1426,6 @@ public void setLocale(HttpServletRequest request, HttpServletResponse response,
 
 	}
 
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	static class CustomThemeResolver implements org.springframework.web.servlet.ThemeResolver {
-
-		@Override
-		public String resolveThemeName(HttpServletRequest request) {
-			return "custom";
-		}
-
-		@Override
-		public void setThemeName(HttpServletRequest request, HttpServletResponse response, String themeName) {
-		}
-
-	}
-
 	static class CustomFlashMapManager extends AbstractFlashMapManager {
 
 		@Override
diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/SpringBootDependencyInjectionTestExecutionListener.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/SpringBootDependencyInjectionTestExecutionListener.java
deleted file mode 100644
index e3fbb4d61b96..000000000000
--- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/SpringBootDependencyInjectionTestExecutionListener.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright 2012-2023 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.test.autoconfigure;
-
-import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport;
-import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportMessage;
-import org.springframework.context.ApplicationContext;
-import org.springframework.context.ConfigurableApplicationContext;
-import org.springframework.test.context.ApplicationContextFailureProcessor;
-import org.springframework.test.context.TestContext;
-import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
-
-/**
- * Since 3.0.0 this class has been replaced by
- * {@link ConditionReportApplicationContextFailureProcessor} and is not used internally.
- *
- * @author Phillip Webb
- * @since 1.4.1
- * @deprecated since 3.0.0 for removal in 3.2.0 in favor of
- * {@link ApplicationContextFailureProcessor}
- */
-@Deprecated(since = "3.0.0", forRemoval = true)
-public class SpringBootDependencyInjectionTestExecutionListener extends DependencyInjectionTestExecutionListener {
-
-	@Override
-	public void prepareTestInstance(TestContext testContext) throws Exception {
-		try {
-			super.prepareTestInstance(testContext);
-		}
-		catch (Exception ex) {
-			outputConditionEvaluationReport(testContext);
-			throw ex;
-		}
-	}
-
-	private void outputConditionEvaluationReport(TestContext testContext) {
-		try {
-			ApplicationContext context = testContext.getApplicationContext();
-			if (context instanceof ConfigurableApplicationContext configurableContext) {
-				ConditionEvaluationReport report = ConditionEvaluationReport.get(configurableContext.getBeanFactory());
-				System.err.println(new ConditionEvaluationReportMessage(report));
-			}
-		}
-		catch (Exception ex) {
-			// Allow original failure to be reported
-		}
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetrics.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetrics.java
deleted file mode 100644
index 44ec896236d4..000000000000
--- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetrics.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright 2012-2022 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.test.autoconfigure.actuate.metrics;
-
-import java.lang.annotation.Documented;
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Inherited;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-import org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability;
-
-/**
- * Annotation that can be applied to a test class to enable auto-configuration for metrics
- * exporters.
- *
- * @author Chris Bono
- * @since 2.4.0
- * @deprecated since 3.0.0 for removal in 3.2.0 in favor of
- * {@link AutoConfigureObservability @AutoConfigureObservability}
- */
-@Target(ElementType.TYPE)
-@Retention(RetentionPolicy.RUNTIME)
-@Documented
-@Inherited
-@Deprecated(since = "3.0.0", forRemoval = true)
-@AutoConfigureObservability(tracing = false)
-public @interface AutoConfigureMetrics {
-
-}
diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/metrics/package-info.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/metrics/package-info.java
deleted file mode 100644
index 6dfcc180e669..000000000000
--- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/metrics/package-info.java
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright 2012-2020 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.
- */
-
-/**
- * Auto-configuration for handling metrics in tests.
- */
-package org.springframework.boot.test.autoconfigure.actuate.metrics;
diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetricsMissingIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetricsMissingIntegrationTests.java
deleted file mode 100644
index ff4e924a993a..000000000000
--- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetricsMissingIntegrationTests.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright 2012-2022 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.test.autoconfigure.actuate.metrics;
-
-import io.micrometer.core.instrument.MeterRegistry;
-import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
-import io.micrometer.prometheus.PrometheusMeterRegistry;
-import org.junit.jupiter.api.Test;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.context.ApplicationContext;
-import org.springframework.core.env.Environment;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * Integration test to verify behaviour when
- * {@link AutoConfigureMetrics @AutoConfigureMetrics} is not present on the test class.
- *
- * @author Chris Bono
- */
-@SpringBootTest
-class AutoConfigureMetricsMissingIntegrationTests {
-
-	@Test
-	void customizerRunsAndOnlyEnablesSimpleMeterRegistryWhenNoAnnotationPresent(
-			@Autowired ApplicationContext applicationContext) {
-		assertThat(applicationContext.getBean(MeterRegistry.class)).isInstanceOf(SimpleMeterRegistry.class);
-		assertThat(applicationContext.getBeansOfType(PrometheusMeterRegistry.class)).isEmpty();
-	}
-
-	@Test
-	void customizerRunsAndSetsExclusionPropertiesWhenNoAnnotationPresent(@Autowired Environment environment) {
-		assertThat(environment.getProperty("management.defaults.metrics.export.enabled")).isEqualTo("false");
-		assertThat(environment.getProperty("management.simple.metrics.export.enabled")).isEqualTo("true");
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetricsPresentIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetricsPresentIntegrationTests.java
deleted file mode 100644
index dfdef02bdb2c..000000000000
--- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetricsPresentIntegrationTests.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright 2012-2022 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.test.autoconfigure.actuate.metrics;
-
-import io.micrometer.prometheus.PrometheusMeterRegistry;
-import org.junit.jupiter.api.Test;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.context.ApplicationContext;
-import org.springframework.core.env.Environment;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-/**
- * Integration test to verify behaviour when
- * {@link AutoConfigureMetrics @AutoConfigureMetrics} is present on the test class.
- *
- * @author Chris Bono
- */
-@SuppressWarnings("removal")
-@SpringBootTest
-@AutoConfigureMetrics
-@Deprecated(since = "3.0.0", forRemoval = true)
-class AutoConfigureMetricsPresentIntegrationTests {
-
-	@Test
-	void customizerDoesNotDisableAvailableMeterRegistriesWhenAnnotationPresent(
-			@Autowired ApplicationContext applicationContext) {
-		assertThat(applicationContext.getBeansOfType(PrometheusMeterRegistry.class)).hasSize(1);
-	}
-
-	@Test
-	void customizerDoesNotSetExclusionPropertiesWhenAnnotationPresent(@Autowired Environment environment) {
-		assertThat(environment.containsProperty("management.defaults.metrics.export.enabled")).isFalse();
-		assertThat(environment.containsProperty("management.simple.metrics.export.enabled")).isFalse();
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetricsSpringBootApplication.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetricsSpringBootApplication.java
deleted file mode 100644
index 31c699a44b8d..000000000000
--- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/metrics/AutoConfigureMetricsSpringBootApplication.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 2012-2022 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.test.autoconfigure.actuate.metrics;
-
-import org.springframework.boot.SpringBootConfiguration;
-import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration;
-import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
-import org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration;
-
-/**
- * Example {@link SpringBootApplication @SpringBootApplication} for use with
- * {@link AutoConfigureMetrics @AutoConfigureMetrics} tests.
- *
- * @author Chris Bono
- */
-@SpringBootConfiguration
-@EnableAutoConfiguration(exclude = { CassandraAutoConfiguration.class, MongoAutoConfiguration.class,
-		MongoReactiveAutoConfiguration.class })
-class AutoConfigureMetricsSpringBootApplication {
-
-}
diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/DefaultTestExecutionListenersPostProcessor.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/DefaultTestExecutionListenersPostProcessor.java
deleted file mode 100644
index 695c3f460f6b..000000000000
--- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/DefaultTestExecutionListenersPostProcessor.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright 2012-2022 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.test.context;
-
-import java.util.List;
-
-import org.springframework.test.context.ApplicationContextFailureProcessor;
-import org.springframework.test.context.TestExecutionListener;
-
-/**
- * Callback interface trigger from {@link SpringBootTestContextBootstrapper} that can be
- * used to post-process the list of default {@link TestExecutionListener
- * TestExecutionListeners} to be used by a test. Can be used to add or remove existing
- * listeners.
- *
- * @author Phillip Webb
- * @since 1.4.1
- * @deprecated since 3.0.0 removal in 3.2.0 in favor of
- * {@link ApplicationContextFailureProcessor}
- */
-@FunctionalInterface
-@Deprecated(since = "3.0.0", forRemoval = true)
-public interface DefaultTestExecutionListenersPostProcessor {
-
-	/**
-	 * Post process the list of default {@link TestExecutionListener listeners} to be
-	 * used.
-	 * @param listeners the source listeners
-	 * @return the actual listeners that should be used
-	 * @since 3.0.0
-	 */
-	List postProcessDefaultTestExecutionListeners(List listeners);
-
-}
diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java
index 0f812a30ebba..ce07386b1a76 100644
--- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java
+++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java
@@ -38,7 +38,6 @@
 import org.springframework.core.annotation.MergedAnnotations;
 import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
 import org.springframework.core.env.Environment;
-import org.springframework.core.io.support.SpringFactoriesLoader;
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.ContextConfigurationAttributes;
 import org.springframework.test.context.ContextCustomizer;
@@ -48,7 +47,6 @@
 import org.springframework.test.context.TestContext;
 import org.springframework.test.context.TestContextAnnotationUtils;
 import org.springframework.test.context.TestContextBootstrapper;
-import org.springframework.test.context.TestExecutionListener;
 import org.springframework.test.context.aot.AotTestAttributes;
 import org.springframework.test.context.support.DefaultTestContextBootstrapper;
 import org.springframework.test.context.support.TestPropertySourceUtils;
@@ -122,18 +120,6 @@ else if (webEnvironment != null && webEnvironment.isEmbedded()) {
 		return context;
 	}
 
-	@Override
-	@SuppressWarnings("removal")
-	protected List getDefaultTestExecutionListeners() {
-		List listeners = new ArrayList<>(super.getDefaultTestExecutionListeners());
-		List postProcessors = SpringFactoriesLoader
-			.loadFactories(DefaultTestExecutionListenersPostProcessor.class, getClass().getClassLoader());
-		for (DefaultTestExecutionListenersPostProcessor postProcessor : postProcessors) {
-			listeners = postProcessor.postProcessDefaultTestExecutionListeners(listeners);
-		}
-		return listeners;
-	}
-
 	@Override
 	protected ContextLoader resolveContextLoader(Class testClass,
 			List configAttributesList) {
diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/SpringBootTestContextBootstrapperIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/SpringBootTestContextBootstrapperIntegrationTests.java
index 38edac15537b..fb275a756e0c 100644
--- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/SpringBootTestContextBootstrapperIntegrationTests.java
+++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/SpringBootTestContextBootstrapperIntegrationTests.java
@@ -45,8 +45,6 @@ class SpringBootTestContextBootstrapperIntegrationTests {
 	@Autowired
 	private SpringBootTestContextBootstrapperExampleConfig config;
 
-	boolean defaultTestExecutionListenersPostProcessorCalled = false;
-
 	@Test
 	void findConfigAutomatically() {
 		assertThat(this.config).isNotNull();
@@ -62,11 +60,6 @@ void testConfigurationWasApplied() {
 		assertThat(this.context.getBean(ExampleBean.class)).isNotNull();
 	}
 
-	@Test
-	void defaultTestExecutionListenersPostProcessorShouldBeCalled() {
-		assertThat(this.defaultTestExecutionListenersPostProcessorCalled).isTrue();
-	}
-
 	@TestConfiguration(proxyBeanMethods = false)
 	static class TestConfig {
 
diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/TestDefaultTestExecutionListenersPostProcessor.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/TestDefaultTestExecutionListenersPostProcessor.java
deleted file mode 100644
index 7587c23f93f5..000000000000
--- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/TestDefaultTestExecutionListenersPostProcessor.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright 2012-2022 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.test.context.bootstrap;
-
-import java.util.List;
-
-import org.springframework.boot.test.context.DefaultTestExecutionListenersPostProcessor;
-import org.springframework.test.context.TestContext;
-import org.springframework.test.context.TestExecutionListener;
-import org.springframework.test.context.support.AbstractTestExecutionListener;
-
-/**
- * Test {@link DefaultTestExecutionListenersPostProcessor}.
- *
- * @author Phillip Webb
- */
-@SuppressWarnings("removal")
-public class TestDefaultTestExecutionListenersPostProcessor implements DefaultTestExecutionListenersPostProcessor {
-
-	@Override
-	public List postProcessDefaultTestExecutionListeners(List listeners) {
-		listeners.add(new ExampleTestExecutionListener());
-		return listeners;
-	}
-
-	static class ExampleTestExecutionListener extends AbstractTestExecutionListener {
-
-		@Override
-		public void prepareTestInstance(TestContext testContext) throws Exception {
-			Object testInstance = testContext.getTestInstance();
-			if (testInstance instanceof SpringBootTestContextBootstrapperIntegrationTests test) {
-				test.defaultTestExecutionListenersPostProcessorCalled = true;
-			}
-		}
-
-	}
-
-}
diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolverTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolverTests.java
index 9f85011de406..4f45a734cb1c 100644
--- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolverTests.java
+++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolverTests.java
@@ -146,17 +146,6 @@ void propertiesWithMultiConstructor() {
 			.allMatch((predicate) -> predicate instanceof ConstructorParameterPropertyDescriptor)));
 	}
 
-	@Test
-	@Deprecated(since = "3.0.0", forRemoval = true)
-	@SuppressWarnings("removal")
-	void propertiesWithMultiConstructorAndDeprecatedAnnotation() {
-		process(org.springframework.boot.configurationsample.immutable.DeprecatedImmutableMultiConstructorProperties.class,
-				propertyNames((stream) -> assertThat(stream).containsExactly("name", "description")));
-		process(org.springframework.boot.configurationsample.immutable.DeprecatedImmutableMultiConstructorProperties.class,
-				properties((stream) -> assertThat(stream)
-					.allMatch((predicate) -> predicate instanceof ConstructorParameterPropertyDescriptor)));
-	}
-
 	@Test
 	void propertiesWithMultiConstructorNoDirective() {
 		process(TwoConstructorsExample.class, propertyNames((stream) -> assertThat(stream).containsExactly("name")));
diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/DeprecatedConstructorBinding.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/DeprecatedConstructorBinding.java
deleted file mode 100644
index ee1ae440f81c..000000000000
--- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/DeprecatedConstructorBinding.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright 2012-2023 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.configurationsample;
-
-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;
-
-/**
- * Alternative to Spring Boot's deprecated
- * {@code @org.springframework.boot.context.properties.ConstructorBinding} for testing
- * (removes the need for a dependency on the real annotation).
- *
- * @author Stephane Nicoll
- */
-@Target(ElementType.CONSTRUCTOR)
-@Retention(RetentionPolicy.RUNTIME)
-@Documented
-@ConstructorBinding
-@Deprecated(since = "3.0.0", forRemoval = true)
-public @interface DeprecatedConstructorBinding {
-
-}
diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/DeprecatedImmutableMultiConstructorProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/DeprecatedImmutableMultiConstructorProperties.java
deleted file mode 100644
index d2e0305fc149..000000000000
--- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/immutable/DeprecatedImmutableMultiConstructorProperties.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2012-2022 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.configurationsample.immutable;
-
-/**
- * Simple immutable properties with several constructors.
- *
- * @author Stephane Nicoll
- */
-@SuppressWarnings("unused")
-@Deprecated(since = "3.0.0", forRemoval = true)
-public class DeprecatedImmutableMultiConstructorProperties {
-
-	private final String name;
-
-	/**
-	 * Test description.
-	 */
-	private final String description;
-
-	public DeprecatedImmutableMultiConstructorProperties(String name) {
-		this(name, null);
-	}
-
-	@SuppressWarnings("removal")
-	@org.springframework.boot.configurationsample.DeprecatedConstructorBinding
-	public DeprecatedImmutableMultiConstructorProperties(String name, String description) {
-		this.name = name;
-		this.description = description;
-	}
-
-}
diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationProperties.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationProperties.java
index 86510c2b53a9..7c8c41e05ad3 100644
--- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationProperties.java
+++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationProperties.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2021 the original author or authors.
+ * Copyright 2012-2023 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.
@@ -22,6 +22,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
+import org.springframework.boot.context.properties.bind.ConstructorBinding;
 import org.springframework.core.annotation.AliasFor;
 import org.springframework.stereotype.Indexed;
 
diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConstructorBinding.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConstructorBinding.java
deleted file mode 100644
index 49c95249ee97..000000000000
--- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConstructorBinding.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright 2012-2022 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.context.properties;
-
-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;
-
-/**
- * Annotation that can be used to indicate which constructor to use when binding
- * configuration properties using constructor arguments rather than by calling setters. A
- * single parameterized constructor implicitly indicates that constructor binding should
- * be used unless the constructor is annotated with `@Autowired`.
- * 

- * Note: To use constructor binding the class must be enabled using - * {@link EnableConfigurationProperties @EnableConfigurationProperties} or configuration - * property scanning. Constructor binding cannot be used with beans that are created by - * the regular Spring mechanisms (e.g. - * {@link org.springframework.stereotype.Component @Component} beans, beans created via - * {@link org.springframework.context.annotation.Bean @Bean} methods or beans loaded using - * {@link org.springframework.context.annotation.Import @Import}). - * - * @author Phillip Webb - * @since 2.2.0 - * @see ConfigurationProperties - * @deprecated since 3.0.0 for removal in 3.2.0 in favor of - * {@link org.springframework.boot.context.properties.bind.ConstructorBinding} - */ -@Target({ ElementType.CONSTRUCTOR, ElementType.ANNOTATION_TYPE }) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Deprecated(since = "3.0.0", forRemoval = true) -@org.springframework.boot.context.properties.bind.ConstructorBinding -public @interface ConstructorBinding { - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConstructorBound.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConstructorBound.java index 4b1a4fea9e47..bd5fe6d92bd4 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConstructorBound.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConstructorBound.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 the original author or authors. + * Copyright 2019-2023 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. @@ -17,6 +17,7 @@ package org.springframework.boot.context.properties; import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.context.properties.bind.ConstructorBinding; /** * Helper class to programmatically bind configuration properties that use constructor diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinModule.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinModule.java index 09ed12dd40b7..9364c73bf70a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinModule.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinModule.java @@ -16,14 +16,9 @@ package org.springframework.boot.jackson; -import java.util.Collection; - import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.module.SimpleModule; -import org.springframework.context.ApplicationContext; -import org.springframework.util.Assert; - /** * Spring Bean and Jackson {@link Module} to find and * {@link SimpleModule#setMixInAnnotation(Class, Class) register} @@ -36,22 +31,6 @@ */ public class JsonMixinModule extends SimpleModule { - public JsonMixinModule() { - } - - /** - * Create a new {@link JsonMixinModule} instance. - * @param context the source application context - * @param basePackages the packages to check for annotated classes - * @deprecated since 3.0.0 in favor of - * {@link #registerEntries(JsonMixinModuleEntries, ClassLoader)} - */ - @Deprecated(since = "3.0.0", forRemoval = true) - public JsonMixinModule(ApplicationContext context, Collection basePackages) { - Assert.notNull(context, "Context must not be null"); - registerEntries(JsonMixinModuleEntries.scan(context, basePackages), context.getClassLoader()); - } - /** * Register the specified {@link JsonMixinModuleEntries entries}. * @param entries the entries to register to this instance diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactorySupplier.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactorySupplier.java deleted file mode 100644 index 7fce4e31cf0f..000000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactorySupplier.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2012-2022 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.web.client; - -import java.util.function.Supplier; - -import org.springframework.http.client.ClientHttpRequestFactory; - -/** - * A supplier for {@link ClientHttpRequestFactory} that detects the preferred candidate - * based on the available implementations on the classpath. - * - * @author Stephane Nicoll - * @author Moritz Halbritter - * @since 2.1.0 - * @deprecated since 3.0.0 for removal in 3.2.0 in favor of - * {@link ClientHttpRequestFactories} - */ -@Deprecated(since = "3.0.0", forRemoval = true) -public class ClientHttpRequestFactorySupplier implements Supplier { - - @Override - public ClientHttpRequestFactory get() { - return ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS); - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilder.java index 93e4e1b91840..7f61703632b4 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilder.java @@ -325,8 +325,9 @@ public RestTemplateBuilder requestFactory(Supplier req } /** - * Set the {@link ClientHttpRequestFactorySupplier} that should be called each time we - * {@link #build()} a new {@link RestTemplate} instance. + * Set the request factory function that should be called to provide a + * {@link ClientHttpRequestFactory} each time we {@link #build()} a new + * {@link RestTemplate} instance. * @param requestFactoryFunction the settings to request factory function * @return a new builder instance * @since 3.0.0 diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java index 349212d7e6c0..04f622135008 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java @@ -39,6 +39,7 @@ import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.web.server.AbstractConfigurableWebServerFactory; +import org.springframework.boot.web.server.Cookie; import org.springframework.boot.web.server.MimeMappings; import org.springframework.boot.web.servlet.ServletContextInitializer; import org.springframework.util.Assert; @@ -335,14 +336,12 @@ public void onStartup(ServletContext servletContext) throws ServletException { configureSessionCookie(servletContext.getSessionCookieConfig()); } - @SuppressWarnings("removal") private void configureSessionCookie(SessionCookieConfig config) { - Session.Cookie cookie = this.session.getCookie(); + Cookie cookie = this.session.getCookie(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(cookie::getName).to(config::setName); map.from(cookie::getDomain).to(config::setDomain); map.from(cookie::getPath).to(config::setPath); - map.from(cookie::getComment).to(config::setComment); map.from(cookie::getHttpOnly).to(config::setHttpOnly); map.from(cookie::getSecure).to(config::setSecure); map.from(cookie::getMaxAge).asInt(Duration::getSeconds).to(config::setMaxAge); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/Session.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/Session.java index 336c03ced541..df95dc474795 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/Session.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/Session.java @@ -21,8 +21,9 @@ import java.time.temporal.ChronoUnit; import java.util.Set; -import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; +import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.boot.convert.DurationUnit; +import org.springframework.boot.web.server.Cookie; /** * Session properties. @@ -44,6 +45,7 @@ public class Session { */ private File storeDir; + @NestedConfigurationProperty private final Cookie cookie = new Cookie(); private final SessionStoreDirectory sessionStoreDirectory = new SessionStoreDirectory(); @@ -101,34 +103,6 @@ SessionStoreDirectory getSessionStoreDirectory() { return this.sessionStoreDirectory; } - /** - * Session cookie properties. - */ - public static class Cookie extends org.springframework.boot.web.server.Cookie { - - /** - * Comment for the session cookie. - */ - private String comment; - - /** - * Return the comment for the session cookie. - * @return the session cookie comment - * @deprecated since 3.0.0 without replacement - */ - @Deprecated(since = "3.0.0", forRemoval = true) - @DeprecatedConfigurationProperty - public String getComment() { - return this.comment; - } - - @Deprecated(since = "3.0.0", forRemoval = true) - public void setComment(String comment) { - this.comment = comment; - } - - } - /** * Available session tracking modes (mirrors * {@link jakarta.servlet.SessionTrackingMode}. diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanTests.java index f48d5be9527b..7e361c87471b 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanTests.java @@ -233,24 +233,6 @@ void forValueObjectWithConstructorBindingAnnotatedClassReturnsBean() { .isNotNull(); } - @Test - void forValueObjectWithDeprecatedConstructorBindingAnnotatedClassReturnsBean() { - ConfigurationPropertiesBean propertiesBean = ConfigurationPropertiesBean - .forValueObject(DeprecatedConstructorBindingOnConstructor.class, "valueObjectBean"); - assertThat(propertiesBean.getName()).isEqualTo("valueObjectBean"); - assertThat(propertiesBean.getInstance()).isNull(); - assertThat(propertiesBean.getType()).isEqualTo(DeprecatedConstructorBindingOnConstructor.class); - assertThat(propertiesBean.asBindTarget().getBindMethod()).isEqualTo(BindMethod.VALUE_OBJECT); - assertThat(propertiesBean.getAnnotation()).isNotNull(); - Bindable target = propertiesBean.asBindTarget(); - assertThat(target.getType()) - .isEqualTo(ResolvableType.forClass(DeprecatedConstructorBindingOnConstructor.class)); - assertThat(target.getValue()).isNull(); - assertThat(BindConstructorProvider.DEFAULT.getBindConstructor(DeprecatedConstructorBindingOnConstructor.class, - false)) - .isNotNull(); - } - @Test void forValueObjectWithRecordReturnsBean() { Class implicitConstructorBinding = new ByteBuddy(ClassFileVersion.JAVA_V16).makeRecord() @@ -558,20 +540,6 @@ static class ConstructorBindingOnConstructor { } - @ConfigurationProperties - @SuppressWarnings("removal") - static class DeprecatedConstructorBindingOnConstructor { - - DeprecatedConstructorBindingOnConstructor(String name) { - this(name, -1); - } - - @org.springframework.boot.context.properties.ConstructorBinding - DeprecatedConstructorBindingOnConstructor(String name, int age) { - } - - } - @ConfigurationProperties static class ConstructorBindingOnMultipleConstructors { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonMixinModuleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonMixinModuleTests.java index 42bf7a8f8181..768a1c0b8da4 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonMixinModuleTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jackson/JsonMixinModuleTests.java @@ -17,7 +17,6 @@ package org.springframework.boot.jackson; import java.util.Arrays; -import java.util.Collections; import java.util.List; import com.fasterxml.jackson.databind.Module; @@ -35,7 +34,6 @@ import org.springframework.util.ClassUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** * Tests for {@link JsonMixinModule}. @@ -53,14 +51,6 @@ void closeContext() { } } - @Test - @Deprecated(since = "3.0.0", forRemoval = true) - @SuppressWarnings("removal") - void createWhenContextIsNullShouldThrowException() { - assertThatIllegalArgumentException().isThrownBy(() -> new JsonMixinModule(null, Collections.emptyList())) - .withMessageContaining("Context must not be null"); - } - @Test void jsonWithModuleWithRenameMixInClassShouldBeMixedIn() throws Exception { load(RenameMixInClass.class); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java index 6e1851e72dc6..46e8eb59ebfe 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java @@ -120,11 +120,6 @@ void restoreTccl() { Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); } - @Override - protected boolean isCookieCommentSupported() { - return false; - } - // JMX MBean names clash if you get more than one Engine with the same name... @Test void tomcatEngineNames() { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java index adadee3b8a0f..12a9aea44ccc 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java @@ -204,10 +204,6 @@ void tearDown() { } } - protected boolean isCookieCommentSupported() { - return true; - } - @Test void startServlet() throws Exception { AbstractServletWebServerFactory factory = getFactory(); @@ -870,13 +866,11 @@ void getValidSessionStoreWhenSessionStoreReferencesFile() throws Exception { } @Test - @SuppressWarnings("removal") void sessionCookieConfiguration() { AbstractServletWebServerFactory factory = getFactory(); factory.getSession().getCookie().setName("testname"); factory.getSession().getCookie().setDomain("testdomain"); factory.getSession().getCookie().setPath("/testpath"); - factory.getSession().getCookie().setComment("testcomment"); factory.getSession().getCookie().setHttpOnly(true); factory.getSession().getCookie().setSecure(true); factory.getSession().getCookie().setMaxAge(Duration.ofSeconds(60)); @@ -886,9 +880,6 @@ void sessionCookieConfiguration() { assertThat(sessionCookieConfig.getName()).isEqualTo("testname"); assertThat(sessionCookieConfig.getDomain()).isEqualTo("testdomain"); assertThat(sessionCookieConfig.getPath()).isEqualTo("/testpath"); - if (isCookieCommentSupported()) { - assertThat(sessionCookieConfig.getComment()).isEqualTo("testcomment"); - } assertThat(sessionCookieConfig.isHttpOnly()).isTrue(); assertThat(sessionCookieConfig.isSecure()).isTrue(); assertThat(sessionCookieConfig.getMaxAge()).isEqualTo(60); @@ -1143,7 +1134,6 @@ public void destroy() { } @Test - @SuppressWarnings("removal") void sessionConfiguration() { AbstractServletWebServerFactory factory = getFactory(); factory.getSession().setTimeout(Duration.ofSeconds(123)); @@ -1151,7 +1141,6 @@ void sessionConfiguration() { factory.getSession().getCookie().setName("testname"); factory.getSession().getCookie().setDomain("testdomain"); factory.getSession().getCookie().setPath("/testpath"); - factory.getSession().getCookie().setComment("testcomment"); factory.getSession().getCookie().setHttpOnly(true); factory.getSession().getCookie().setSecure(true); factory.getSession().getCookie().setMaxAge(Duration.ofMinutes(1)); @@ -1163,9 +1152,6 @@ void sessionConfiguration() { assertThat(servletContext.getSessionCookieConfig().getName()).isEqualTo("testname"); assertThat(servletContext.getSessionCookieConfig().getDomain()).isEqualTo("testdomain"); assertThat(servletContext.getSessionCookieConfig().getPath()).isEqualTo("/testpath"); - if (isCookieCommentSupported()) { - assertThat(servletContext.getSessionCookieConfig().getComment()).isEqualTo("testcomment"); - } assertThat(servletContext.getSessionCookieConfig().isHttpOnly()).isTrue(); assertThat(servletContext.getSessionCookieConfig().isSecure()).isTrue(); assertThat(servletContext.getSessionCookieConfig().getMaxAge()).isEqualTo(60); From 493987fc1a2dc894ecb9563c11a16e95032a39c8 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Fri, 23 Jun 2023 14:40:42 -0600 Subject: [PATCH 0067/1215] Allow key password to be set for a PemSslStoreBundle Closes gh-35983 --- .../boot/ssl/pem/PemSslStoreBundle.java | 18 ++++++++++++- .../boot/ssl/pem/PemSslStoreBundleTests.java | 26 +++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java index dee4651852af..251ff0b52799 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java @@ -42,6 +42,8 @@ public class PemSslStoreBundle implements SslStoreBundle { private final String keyAlias; + private final String keyPassword; + /** * Create a new {@link PemSslStoreBundle} instance. * @param keyStoreDetails the key store details @@ -59,9 +61,22 @@ public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails */ public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String keyAlias) { + this(keyStoreDetails, trustStoreDetails, keyAlias, null); + } + + /** + * Create a new {@link PemSslStoreBundle} instance. + * @param keyStoreDetails the key store details + * @param trustStoreDetails the trust store details + * @param keyAlias the key alias to use or {@code null} to use a default alias + * @param keyPassword the password to use for the key + */ + public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String keyAlias, + String keyPassword) { this.keyAlias = keyAlias; this.keyStoreDetails = keyStoreDetails; this.trustStoreDetails = trustStoreDetails; + this.keyPassword = keyPassword; } @Override @@ -104,7 +119,8 @@ private void addCertificates(KeyStore keyStore, X509Certificate[] certificates, throws KeyStoreException { String alias = (this.keyAlias != null) ? this.keyAlias : DEFAULT_KEY_ALIAS; if (privateKey != null) { - keyStore.setKeyEntry(alias, privateKey, null, certificates); + keyStore.setKeyEntry(alias, privateKey, (this.keyPassword != null) ? this.keyPassword.toCharArray() : null, + certificates); } else { for (int index = 0; index < certificates.length; index++) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java index 61f5c4983bd0..29c22a27e0ce 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java @@ -33,6 +33,8 @@ */ class PemSslStoreBundleTests { + private static final char[] EMPTY_KEY_PASSWORD = new char[] {}; + @Test void whenNullStores() { PemSslStoreDetails keyStoreDetails = null; @@ -117,6 +119,18 @@ void whenHasStoreType() { assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("PKCS12", "ssl")); } + @Test + void whenHasKeyStoreDetailsAndTrustStoreDetailsAndKeyPassword() { + PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem") + .withPrivateKey("classpath:test-key.pem"); + PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem") + .withPrivateKey("classpath:test-key.pem"); + PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, "test-alias", "keysecret"); + assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias", "keysecret".toCharArray())); + assertThat(bundle.getTrustStore()) + .satisfies(storeContainingCertAndKey("test-alias", "keysecret".toCharArray())); + } + private Consumer storeContainingCert(String keyAlias) { return storeContainingCert(KeyStore.getDefaultType(), keyAlias); } @@ -127,7 +141,7 @@ private Consumer storeContainingCert(String keyStoreType, String keyAl assertThat(keyStore.getType()).isEqualTo(keyStoreType); assertThat(keyStore.containsAlias(keyAlias)).isTrue(); assertThat(keyStore.getCertificate(keyAlias)).isNotNull(); - assertThat(keyStore.getKey(keyAlias, new char[] {})).isNull(); + assertThat(keyStore.getKey(keyAlias, EMPTY_KEY_PASSWORD)).isNull(); }); } @@ -136,12 +150,20 @@ private Consumer storeContainingCertAndKey(String keyAlias) { } private Consumer storeContainingCertAndKey(String keyStoreType, String keyAlias) { + return storeContainingCertAndKey(keyStoreType, keyAlias, EMPTY_KEY_PASSWORD); + } + + private Consumer storeContainingCertAndKey(String keyAlias, char[] keyPassword) { + return storeContainingCertAndKey(KeyStore.getDefaultType(), keyAlias, keyPassword); + } + + private Consumer storeContainingCertAndKey(String keyStoreType, String keyAlias, char[] keyPassword) { return ThrowingConsumer.of((keyStore) -> { assertThat(keyStore).isNotNull(); assertThat(keyStore.getType()).isEqualTo(keyStoreType); assertThat(keyStore.containsAlias(keyAlias)).isTrue(); assertThat(keyStore.getCertificate(keyAlias)).isNotNull(); - assertThat(keyStore.getKey(keyAlias, new char[] {})).isNotNull(); + assertThat(keyStore.getKey(keyAlias, keyPassword)).isNotNull(); }); } From dbb24286ff2a9e2b159d33029f792cdf10397924 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 31 May 2023 12:25:01 +0100 Subject: [PATCH 0068/1215] Separate stopping and destruction so web server can be restarted Previously, when a Servlet-based WebServer was stopped it would also stop the ServletContext. This led to problems as Tomcat and Undertow would then not allow a restart. Jetty would allow a restart but duplicate servlet registrations would then be attempted. This commit modifies the WebServer lifecycle to separate stopping and destruction for both servlet and reactive web servers. This allows a WebServer's stop() implementation to leave some components running so that they can be restarted. To completely shut down a WebServer destroy() must now be called. Both Tomcat and Jetty WebServers have been updated to stop their network connections when stop() is called but leave other components running. This works with both servlet and reactive web servers. Note that an Undertow-based Servlet web server does not support stop and restart. Once stopped, a Servlet Deployment cannot be restarted and it does not appear to be possible to separate the lifecycle of its network connections and a Servlet deployment. Reactor Netty and Undertow-based reactive web servers can now also be stopped and then restarted. Calling stop() stops the whole server but this does not cause a problem as there's no (application-exposed) ServletContext involved. There may be room to optimize this in the future if the need arises. Closes gh-34955 --- .../web/embedded/jetty/JettyWebServer.java | 16 +++++++- .../web/embedded/tomcat/GracefulShutdown.java | 14 +++---- .../web/embedded/tomcat/TomcatWebServer.java | 26 ++++++++----- .../embedded/undertow/UndertowWebServer.java | 6 +-- .../boot/web/server/WebServer.java | 10 ++++- .../ServletWebServerApplicationContext.java | 7 +++- .../JettyServletWebServerFactoryTests.java | 4 +- .../TomcatServletWebServerFactoryTests.java | 4 +- .../UndertowServletWebServerFactoryTests.java | 15 ++++++++ ...AbstractReactiveWebServerFactoryTests.java | 33 ++++++++++++++++- ...rvletWebServerApplicationContextTests.java | 28 +++++++++++++- .../AbstractServletWebServerFactoryTests.java | 37 +++++++++++++++++-- .../boot/loaderapp/LoaderTestApplication.java | 4 +- 13 files changed, 169 insertions(+), 35 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java index 95ae2e3c2b67..21f1052e6ed2 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java @@ -238,7 +238,9 @@ public void stop() { this.gracefulShutdown.abort(); } try { - this.server.stop(); + for (Connector connector : this.server.getConnectors()) { + connector.stop(); + } } catch (InterruptedException ex) { Thread.currentThread().interrupt(); @@ -249,6 +251,18 @@ public void stop() { } } + @Override + public void destroy() { + synchronized (this.monitor) { + try { + this.server.stop(); + } + catch (Exception ex) { + throw new WebServerException("Unable to destroy embedded Jetty server", ex); + } + } + } + @Override public int getPort() { Connector[] connectors = this.server.getConnectors(); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/GracefulShutdown.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/GracefulShutdown.java index c921cf5c94aa..3215a0de8609 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/GracefulShutdown.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/GracefulShutdown.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. @@ -60,14 +60,14 @@ private void doShutdown(GracefulShutdownCallback callback) { try { for (Container host : this.tomcat.getEngine().findChildren()) { for (Container context : host.findChildren()) { - while (isActive(context)) { - if (this.aborted) { - logger.info("Graceful shutdown aborted with one or more requests still active"); - callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE); - return; - } + while (!this.aborted && isActive(context)) { Thread.sleep(50); } + if (this.aborted) { + logger.info("Graceful shutdown aborted with one or more requests still active"); + callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE); + return; + } } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java index 7c05aa77f3c2..c2dfde03e9e7 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java @@ -209,6 +209,7 @@ public void start() throws WebServerException { if (this.started) { return; } + try { addPreviouslyRemovedConnectors(); Connector connector = this.tomcat.getConnector(); @@ -324,16 +325,10 @@ public void stop() throws WebServerException { boolean wasStarted = this.started; try { this.started = false; - try { - if (this.gracefulShutdown != null) { - this.gracefulShutdown.abort(); - } - stopTomcat(); - this.tomcat.destroy(); - } - catch (LifecycleException ex) { - // swallow and continue + if (this.gracefulShutdown != null) { + this.gracefulShutdown.abort(); } + removeServiceConnectors(); } catch (Exception ex) { throw new WebServerException("Unable to stop embedded Tomcat", ex); @@ -346,6 +341,19 @@ public void stop() throws WebServerException { } } + public void destroy() throws WebServerException { + try { + stopTomcat(); + this.tomcat.destroy(); + } + catch (LifecycleException ex) { + // Swallow and continue + } + catch (Exception ex) { + throw new WebServerException("Unable to destroy embedded Tomcat", ex); + } + } + private String getPortsDescription(boolean localPort) { StringBuilder ports = new StringBuilder(); for (Connector connector : this.tomcat.getService().findConnectors()) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java index dd7f887bfb69..563b2fc4b197 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java @@ -132,13 +132,13 @@ public void start() throws WebServerException { throw new WebServerException("Unable to start embedded Undertow", ex); } finally { - stopSilently(); + destroySilently(); } } } } - private void stopSilently() { + private void destroySilently() { try { if (this.undertow != null) { this.undertow.stop(); @@ -274,7 +274,7 @@ public void stop() throws WebServerException { } } catch (Exception ex) { - throw new WebServerException("Unable to stop undertow", ex); + throw new WebServerException("Unable to stop Undertow", ex); } } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServer.java index 6b70c02ca7b0..e5c02a86f801 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. @@ -61,4 +61,12 @@ default void shutDownGracefully(GracefulShutdownCallback callback) { callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE); } + /** + * Destroys the web server such that it cannot be started again. + * @since 3.2.0 + */ + default void destroy() { + stop(); + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContext.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContext.java index 534237cc7728..d6513792d21b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContext.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -149,6 +149,7 @@ public final void refresh() throws BeansException, IllegalStateException { WebServer webServer = this.webServer; if (webServer != null) { webServer.stop(); + webServer.destroy(); } throw ex; } @@ -171,6 +172,10 @@ protected void doClose() { AvailabilityChangeEvent.publish(this, ReadinessState.REFUSING_TRAFFIC); } super.doClose(); + WebServer webServer = this.webServer; + if (webServer != null) { + webServer.destroy(); + } } private void createWebServer() { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java index f037202ca4e2..9dc3ec21c541 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java @@ -234,10 +234,10 @@ void sslCiphersConfiguration() { } @Test - void stopCalledWithoutStart() { + void destroyCalledWithoutStart() { JettyServletWebServerFactory factory = getFactory(); this.webServer = factory.getWebServer(exampleServletRegistration()); - this.webServer.stop(); + this.webServer.destroy(); Server server = ((JettyWebServer) this.webServer).getServer(); assertThat(server.isStopped()).isTrue(); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java index 46e8eb59ebfe..ba8c07b6c0e9 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java @@ -340,10 +340,10 @@ void startupFailureDoesNotResultInUnstoppedThreadsBeingReported(CapturedOutput o } @Test - void stopCalledWithoutStart() { + void destroyCalledWithoutStart() { TomcatServletWebServerFactory factory = getFactory(); this.webServer = factory.getWebServer(exampleServletRegistration()); - this.webServer.stop(); + this.webServer.destroy(); Tomcat tomcat = ((TomcatWebServer) this.webServer).getTomcat(); assertThat(tomcat.getServer().getState()).isSameAs(LifecycleState.DESTROYED); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactoryTests.java index dd42e2bf9966..f0c5f7d4bd55 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactoryTests.java @@ -40,6 +40,7 @@ import org.apache.jasper.servlet.JspServlet; import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.InOrder; @@ -211,6 +212,20 @@ void whenServerIsShuttingDownGracefullyThenRequestsAreRejectedWithServiceUnavail this.webServer.stop(); } + @Test + @Override + @Disabled("Restart after stop is not supported with Undertow") + protected void restartAfterStop() { + + } + + @Test + @Override + @Disabled("Undertow's architecture prevents separating stop and destroy") + protected void servletContextListenerContextDestroyedIsNotCalledWhenContainerIsStopped() { + + } + private void testAccessLog(String prefix, String suffix, String expectedFile) throws IOException, URISyntaxException { UndertowServletWebServerFactory factory = getFactory(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java index 2985bf9d34d3..2fb586580812 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java @@ -77,6 +77,7 @@ import org.springframework.web.reactive.function.client.WebClientRequestException; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -95,6 +96,12 @@ void tearDown() { if (this.webServer != null) { try { this.webServer.stop(); + try { + this.webServer.destroy(); + } + catch (Exception ex) { + // Ignore + } } catch (Exception ex) { // Ignore @@ -124,13 +131,37 @@ void specificPort() throws Exception { assertThat(this.webServer.getPort()).isEqualTo(specificPort); } + @Test + protected void restartAfterStop() throws Exception { + AbstractReactiveWebServerFactory factory = getFactory(); + this.webServer = factory.getWebServer(new EchoHandler()); + this.webServer.start(); + int port = this.webServer.getPort(); + assertThat(getResponse(port, "/test")).isEqualTo("Hello World"); + this.webServer.stop(); + assertThatException().isThrownBy(() -> getResponse(port, "/test")); + this.webServer.start(); + assertThat(getResponse(this.webServer.getPort(), "/test")).isEqualTo("Hello World"); + } + + private String getResponse(int port, String uri) { + WebClient webClient = getWebClient(port).build(); + Mono result = webClient.post() + .uri(uri) + .contentType(MediaType.TEXT_PLAIN) + .body(BodyInserters.fromValue("Hello World")) + .retrieve() + .bodyToMono(String.class); + return result.block(Duration.ofSeconds(30)); + } + @Test void portIsMinusOneWhenConnectionIsClosed() { AbstractReactiveWebServerFactory factory = getFactory(); this.webServer = factory.getWebServer(new EchoHandler()); this.webServer.start(); assertThat(this.webServer.getPort()).isGreaterThan(0); - this.webServer.stop(); + this.webServer.destroy(); assertThat(this.webServer.getPort()).isEqualTo(-1); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContextTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContextTests.java index 4bde93562782..059b6224c294 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContextTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContextTests.java @@ -84,6 +84,7 @@ import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.withSettings; /** @@ -156,12 +157,35 @@ void localPortIsAvailable() { } @Test - void stopOnClose() { + void stopOnStop() { addWebServerFactoryBean(); this.context.refresh(); MockServletWebServerFactory factory = getWebServerFactory(); - this.context.close(); + then(factory.getWebServer()).should().start(); + this.context.stop(); + then(factory.getWebServer()).should().stop(); + } + + @Test + void startOnStartAfterStop() { + addWebServerFactoryBean(); + this.context.refresh(); + MockServletWebServerFactory factory = getWebServerFactory(); + then(factory.getWebServer()).should().start(); + this.context.stop(); then(factory.getWebServer()).should().stop(); + this.context.start(); + then(factory.getWebServer()).should(times(2)).start(); + } + + @Test + void stopAndDestroyOnClose() { + addWebServerFactoryBean(); + this.context.refresh(); + MockServletWebServerFactory factory = getWebServerFactory(); + this.context.close(); + then(factory.getWebServer()).should(times(2)).stop(); + then(factory.getWebServer()).should().destroy(); } @Test diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java index 12a9aea44ccc..c6c53781daf6 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java @@ -167,6 +167,7 @@ import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; /** * Base for testing classes that extends {@link AbstractServletWebServerFactory}. @@ -197,6 +198,12 @@ void tearDown() { if (this.webServer != null) { try { this.webServer.stop(); + try { + this.webServer.destroy(); + } + catch (Exception ex) { + // Ignore + } } catch (Exception ex) { // Ignore @@ -233,6 +240,19 @@ void stopCalledTwice() { this.webServer.stop(); } + @Test + protected void restartAfterStop() throws IOException, URISyntaxException { + AbstractServletWebServerFactory factory = getFactory(); + this.webServer = factory.getWebServer(exampleServletRegistration()); + this.webServer.start(); + assertThat(getResponse(getLocalUrl("/hello"))).isEqualTo("Hello World"); + int port = this.webServer.getPort(); + this.webServer.stop(); + assertThatIOException().isThrownBy(() -> getResponse(getLocalUrl(port, "/hello"))); + this.webServer.start(); + assertThat(getResponse(getLocalUrl("/hello"))).isEqualTo("Hello World"); + } + @Test void emptyServerWhenPortIsMinusOne() { AbstractServletWebServerFactory factory = getFactory(); @@ -295,7 +315,7 @@ void portIsMinusOneWhenConnectionIsClosed() { this.webServer = factory.getWebServer(); this.webServer.start(); assertThat(this.webServer.getPort()).isGreaterThan(0); - this.webServer.stop(); + this.webServer.destroy(); assertThat(this.webServer.getPort()).isEqualTo(-1); } @@ -814,7 +834,7 @@ void persistSession() throws Exception { this.webServer.start(); String s1 = getResponse(getLocalUrl("/session")); String s2 = getResponse(getLocalUrl("/session")); - this.webServer.stop(); + this.webServer.destroy(); this.webServer = factory.getWebServer(sessionServletRegistration()); this.webServer.start(); String s3 = getResponse(getLocalUrl("/session")); @@ -833,7 +853,7 @@ void persistSessionInSpecificSessionStoreDir() throws Exception { this.webServer = factory.getWebServer(sessionServletRegistration()); this.webServer.start(); getResponse(getLocalUrl("/session")); - this.webServer.stop(); + this.webServer.destroy(); File[] dirContents = sessionStoreDir.listFiles((dir, name) -> !(".".equals(name) || "..".equals(name))); assertThat(dirContents).isNotEmpty(); } @@ -1158,11 +1178,20 @@ void sessionConfiguration() { } @Test - void servletContextListenerContextDestroyedIsCalledWhenContainerIsStopped() throws Exception { + protected void servletContextListenerContextDestroyedIsNotCalledWhenContainerIsStopped() throws Exception { ServletContextListener listener = mock(ServletContextListener.class); this.webServer = getFactory().getWebServer((servletContext) -> servletContext.addListener(listener)); this.webServer.start(); this.webServer.stop(); + then(listener).should(times(0)).contextDestroyed(any(ServletContextEvent.class)); + } + + @Test + void servletContextListenerContextDestroyedIsCalledWhenContainerIsDestroyed() throws Exception { + ServletContextListener listener = mock(ServletContextListener.class); + this.webServer = getFactory().getWebServer((servletContext) -> servletContext.addListener(listener)); + this.webServer.start(); + this.webServer.destroy(); then(listener).should().contextDestroyed(any(ServletContextEvent.class)); } diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java index 81b7c41cbd3a..0c9d429350d8 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -53,7 +53,7 @@ public CommandLineRunner commandLineRunner(ServletContext servletContext) { } public static void main(String[] args) { - SpringApplication.run(LoaderTestApplication.class, args).stop(); + SpringApplication.run(LoaderTestApplication.class, args).close(); } } From 98d459d76cb887f4624133a0aa953ec8e2c477de Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 27 Jun 2023 14:16:20 +0100 Subject: [PATCH 0069/1215] Revert "Merge branch '3.1.x'" See gh-36092 --- ...ndertowWebServerFactoryCustomizerTests.java | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizerTests.java index c9c2547b62a4..e5a2c95e8271 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizerTests.java @@ -103,24 +103,6 @@ void customMaxHttpRequestHeaderSizeIgnoredIfZero() { assertThat(boundServerOption(UndertowOptions.MAX_HEADER_SIZE)).isNull(); } - @Test - void customMaxHttpRequestHeaderSize() { - bind("server.max-http-request-header-size=2048"); - assertThat(boundServerOption(UndertowOptions.MAX_HEADER_SIZE)).isEqualTo(2048); - } - - @Test - void customMaxHttpRequestHeaderSizeIgnoredIfNegative() { - bind("server.max-http-request-header-size=-1"); - assertThat(boundServerOption(UndertowOptions.MAX_HEADER_SIZE)).isNull(); - } - - @Test - void customMaxHttpRequestHeaderSizeIgnoredIfZero() { - bind("server.max-http-request-header-size=0"); - assertThat(boundServerOption(UndertowOptions.MAX_HEADER_SIZE)).isNull(); - } - @Test void customMaxHttpPostSize() { bind("server.undertow.max-http-post-size=256"); From 0b39429f96dca3c0a027d149e2c7d403c140a17e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 28 Jun 2023 14:11:37 +0100 Subject: [PATCH 0070/1215] Remove containers after use in Docker Compose integration tests Closes gh-36104 --- .../connection/test/AbstractDockerComposeIntegrationTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/test/AbstractDockerComposeIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/test/AbstractDockerComposeIntegrationTests.java index f142ba86df74..d59742b86314 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/test/AbstractDockerComposeIntegrationTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/test/AbstractDockerComposeIntegrationTests.java @@ -58,6 +58,7 @@ protected final T run(Class type) { properties.put("spring.docker.compose.skip.in-tests", "false"); properties.put("spring.docker.compose.file", ThrowingSupplier.of(this.composeResource::getFile).get().getAbsolutePath()); + properties.put("spring.docker.compose.stop.command", "down"); application.setDefaultProperties(properties); return application.run().getBean(type); } From b32697b3cebdf2fa8b7e229fe60e2e9df7030984 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 29 Jun 2023 16:53:36 +0100 Subject: [PATCH 0071/1215] Add support to @ClassPathExclusions for excluding packages Closes gh-36120 --- .../classpath/ClassPathExclusions.java | 27 +++++++++++++++++-- .../ModifiedClassPathClassLoader.java | 25 +++++++++++++++-- ...fiedClassPathExtensionExclusionsTests.java | 22 ++++++++++++--- 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ClassPathExclusions.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ClassPathExclusions.java index f7c809f94704..8f124bcd02ee 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ClassPathExclusions.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ClassPathExclusions.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -25,6 +25,8 @@ import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.core.annotation.AliasFor; + /** * Annotation used to exclude entries from the classpath. * @@ -37,13 +39,34 @@ @ExtendWith(ModifiedClassPathExtension.class) public @interface ClassPathExclusions { + /** + * Alias for {@code files}. + *

+ * One or more Ant-style patterns that identify entries to be excluded from the class + * path. Matching is performed against an entry's {@link File#getName() file name}. + * For example, to exclude Hibernate Validator from the classpath, + * {@code "hibernate-validator-*.jar"} can be used. + * @return the exclusion patterns + */ + @AliasFor("files") + String[] value() default {}; + /** * One or more Ant-style patterns that identify entries to be excluded from the class * path. Matching is performed against an entry's {@link File#getName() file name}. * For example, to exclude Hibernate Validator from the classpath, * {@code "hibernate-validator-*.jar"} can be used. * @return the exclusion patterns + * @since 3.2.0 + */ + @AliasFor("value") + String[] files() default {}; + + /** + * One or more packages that should be excluded from the classpath. + * @return the excluded packages + * @since 3.2.0 */ - String[] value(); + String[] packages() default {}; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathClassLoader.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathClassLoader.java index fdd5f0c91594..acb91585bea2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathClassLoader.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathClassLoader.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -56,6 +57,7 @@ import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.util.AntPathMatcher; +import org.springframework.util.ClassUtils; import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -74,10 +76,14 @@ final class ModifiedClassPathClassLoader extends URLClassLoader { private static final int MAX_RESOLUTION_ATTEMPTS = 5; + private final Set excludedPackages; + private final ClassLoader junitLoader; - ModifiedClassPathClassLoader(URL[] urls, ClassLoader parent, ClassLoader junitLoader) { + ModifiedClassPathClassLoader(URL[] urls, Set excludedPackages, ClassLoader parent, + ClassLoader junitLoader) { super(urls, parent); + this.excludedPackages = excludedPackages; this.junitLoader = junitLoader; } @@ -87,6 +93,10 @@ public Class loadClass(String name) throws ClassNotFoundException { || name.startsWith("io.netty.internal.tcnative")) { return Class.forName(name, false, this.junitLoader); } + String packageName = ClassUtils.getPackageName(name); + if (this.excludedPackages.contains(packageName)) { + throw new ClassNotFoundException(); + } return super.loadClass(name); } @@ -130,7 +140,7 @@ private static ModifiedClassPathClassLoader compute(ClassLoader classLoader, .map((source) -> MergedAnnotations.from(source, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY)) .toList(); return new ModifiedClassPathClassLoader(processUrls(extractUrls(classLoader), annotations), - classLoader.getParent(), classLoader); + excludedPackages(annotations), classLoader.getParent(), classLoader); } private static URL[] extractUrls(ClassLoader classLoader) { @@ -269,6 +279,17 @@ private static List createDependencies(String[] allCoordinates) { return dependencies; } + private static Set excludedPackages(List annotations) { + Set excludedPackages = new HashSet<>(); + for (MergedAnnotations candidate : annotations) { + MergedAnnotation annotation = candidate.get(ClassPathExclusions.class); + if (annotation.isPresent()) { + excludedPackages.addAll(Arrays.asList(annotation.getStringArray("packages"))); + } + } + return excludedPackages; + } + /** * Filter for class path entries. */ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtensionExclusionsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtensionExclusionsTests.java index 5cfdd53c0af0..0e0741bd2ace 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtensionExclusionsTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathExtensionExclusionsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -19,6 +19,8 @@ import org.hamcrest.Matcher; import org.junit.jupiter.api.Test; +import org.springframework.util.ClassUtils; + import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.isA; @@ -26,22 +28,34 @@ * Tests for {@link ModifiedClassPathExtension} excluding entries from the class path. * * @author Christoph Dreis + * @author Andy Wilkinson */ -@ClassPathExclusions("hibernate-validator-*.jar") +@ClassPathExclusions(files = "hibernate-validator-*.jar", packages = "java.net.http") class ModifiedClassPathExtensionExclusionsTests { private static final String EXCLUDED_RESOURCE = "META-INF/services/jakarta.validation.spi.ValidationProvider"; @Test - void entriesAreFilteredFromTestClassClassLoader() { + void fileExclusionsAreFilteredFromTestClassClassLoader() { assertThat(getClass().getClassLoader().getResource(EXCLUDED_RESOURCE)).isNull(); } @Test - void entriesAreFilteredFromThreadContextClassLoader() { + void fileExclusionsAreFilteredFromThreadContextClassLoader() { assertThat(Thread.currentThread().getContextClassLoader().getResource(EXCLUDED_RESOURCE)).isNull(); } + @Test + void packageExclusionsAreFilteredFromTestClassClassLoader() { + assertThat(ClassUtils.isPresent("java.net.http.HttpClient", getClass().getClassLoader())).isFalse(); + } + + @Test + void packageExclusionsAreFilteredFromThreadContextClassLoader() { + assertThat(ClassUtils.isPresent("java.net.http.HttpClient", Thread.currentThread().getContextClassLoader())) + .isFalse(); + } + @Test void testsThatUseHamcrestWorkCorrectly() { Matcher matcher = isA(IllegalStateException.class); From 32b7b312f059e446297e6d87398bc616d20a4f65 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 16 Jun 2023 12:29:14 +0100 Subject: [PATCH 0072/1215] Add config metadata changelog generator to main build Closes gh-21486 --- settings.gradle | 1 + .../build.gradle | 58 +++++ ...nfigurationMetadataChangelogGenerator.java | 56 +++++ .../ConfigurationMetadataChangelogWriter.java | 204 ++++++++++++++++++ .../changelog/ConfigurationMetadataDiff.java | 109 ++++++++++ .../NamedConfigurationMetadataRepository.java | 80 +++++++ .../changelog/package-info.java | 20 ++ .../ConfigurationMetadataDiffTests.java | 92 ++++++++ .../src/test/resources/sample-1.0.json | 31 +++ .../src/test/resources/sample-2.0.json | 34 +++ src/checkstyle/checkstyle-suppressions.xml | 1 + 11 files changed, 686 insertions(+) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/build.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataChangelogGenerator.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataChangelogWriter.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataDiff.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/NamedConfigurationMetadataRepository.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataDiffTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-1.0.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-2.0.json diff --git a/settings.gradle b/settings.gradle index 101c44479d93..48b547ef1c4e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -53,6 +53,7 @@ include "spring-boot-project:spring-boot-tools:spring-boot-autoconfigure-process include "spring-boot-project:spring-boot-tools:spring-boot-buildpack-platform" include "spring-boot-project:spring-boot-tools:spring-boot-cli" include "spring-boot-project:spring-boot-tools:spring-boot-configuration-metadata" +include "spring-boot-project:spring-boot-tools:spring-boot-configuration-metadata-changelog-generator" include "spring-boot-project:spring-boot-tools:spring-boot-configuration-processor" include "spring-boot-project:spring-boot-tools:spring-boot-gradle-plugin" include "spring-boot-project:spring-boot-tools:spring-boot-gradle-test-support" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/build.gradle new file mode 100644 index 000000000000..4d5c517e9040 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/build.gradle @@ -0,0 +1,58 @@ +plugins { + id "java" + id "org.springframework.boot.conventions" +} + +description = "Spring Boot Configuration Metadata Changelog Generator" + +configurations { + oldMetadata + newMetadata +} + +dependencies { + implementation(enforcedPlatform(project(":spring-boot-project:spring-boot-dependencies"))) + implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-configuration-metadata")) + + testImplementation(enforcedPlatform(project(":spring-boot-project:spring-boot-dependencies"))) + testImplementation("org.assertj:assertj-core") + testImplementation("org.junit.jupiter:junit-jupiter") +} + +if (project.hasProperty("oldVersion") && project.hasProperty("newVersion")) { + dependencies { + ["spring-boot", + "spring-boot-actuator", + "spring-boot-actuator-autoconfigure", + "spring-boot-autoconfigure", + "spring-boot-devtools", + "spring-boot-test-autoconfigure"].each { + oldMetadata("org.springframework.boot:$it:$oldVersion") + newMetadata("org.springframework.boot:$it:$newVersion") + } + } + + def prepareOldMetadata = tasks.register("prepareOldMetadata", Sync) { + from(configurations.oldMetadata) + if (project.hasProperty("oldVersion")) { + destinationDir = project.file("build/configuration-metadata-diff/$oldVersion") + } + } + + def prepareNewMetadata = tasks.register("prepareNewMetadata", Sync) { + from(configurations.newMetadata) + if (project.hasProperty("newVersion")) { + destinationDir = project.file("build/configuration-metadata-diff/$newVersion") + } + } + + tasks.register("generate", JavaExec) { + inputs.files(prepareOldMetadata, prepareNewMetadata) + outputs.file(project.file("build/configuration-metadata-changelog.adoc")) + classpath = sourceSets.main.runtimeClasspath + mainClass = 'org.springframework.boot.configurationmetadata.changelog.ConfigurationMetadataChangelogGenerator' + if (project.hasProperty("oldVersion") && project.hasProperty("newVersion")) { + args = [project.file("build/configuration-metadata-diff/$oldVersion"), project.file("build/configuration-metadata-diff/$newVersion"), project.file("build/configuration-metadata-changelog.adoc")] + } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataChangelogGenerator.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataChangelogGenerator.java new file mode 100644 index 000000000000..2eb5b1244ec0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataChangelogGenerator.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2023 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.configurationmetadata.changelog; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +/** + * Generates a configuration metadata changelog. Requires three arguments: + * + *

    + *
  1. The path of a directory containing jar files from which the old metadata will be + * extracted + *
  2. The path of a directory containing jar files from which the new metadata will be + * extracted + *
  3. The path of a file to which the changelog will be written + *
+ * + * The name of each directory will be used to name the old and new metadata in the + * generated changelog + * + * @author Andy Wilkinson + */ +final class ConfigurationMetadataChangelogGenerator { + + private ConfigurationMetadataChangelogGenerator() { + + } + + public static void main(String[] args) throws IOException { + ConfigurationMetadataDiff diff = ConfigurationMetadataDiff.of( + NamedConfigurationMetadataRepository.from(new File(args[0])), + NamedConfigurationMetadataRepository.from(new File(args[1]))); + try (ConfigurationMetadataChangelogWriter writer = new ConfigurationMetadataChangelogWriter( + new FileWriter(new File(args[2])))) { + writer.write(diff); + } + System.out.println("\nConfiguration metadata changelog written to '" + args[2] + "'"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataChangelogWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataChangelogWriter.java new file mode 100644 index 000000000000..b08b1667d34e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataChangelogWriter.java @@ -0,0 +1,204 @@ +/* + * Copyright 2012-2023 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.configurationmetadata.changelog; + +import java.io.PrintWriter; +import java.io.Writer; +import java.text.BreakIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; +import org.springframework.boot.configurationmetadata.Deprecation; +import org.springframework.boot.configurationmetadata.changelog.ConfigurationMetadataDiff.Difference; +import org.springframework.boot.configurationmetadata.changelog.ConfigurationMetadataDiff.Difference.Type; + +/** + * Writes a configuration metadata changelog from a {@link ConfigurationMetadataDiff}. + * + * @author Stephane Nicoll + * @author Andy Wilkinson + */ +class ConfigurationMetadataChangelogWriter implements AutoCloseable { + + private final PrintWriter out; + + ConfigurationMetadataChangelogWriter(Writer out) { + this.out = new PrintWriter(out); + } + + void write(ConfigurationMetadataDiff diff) { + this.out.append(String.format("Configuration property changes between `%s` and " + "`%s`%n", diff.leftName(), + diff.rightName())); + this.out.append(System.lineSeparator()); + this.out.append(String.format("== Deprecated in `%s`%n", diff.rightName())); + Map> differencesByType = differencesByType(diff); + writeDeprecatedProperties(differencesByType.get(Type.DEPRECATED)); + this.out.append(System.lineSeparator()); + this.out.append(String.format("== New in `%s`%n", diff.rightName())); + writeAddedProperties(differencesByType.get(Type.ADDED)); + this.out.append(System.lineSeparator()); + this.out.append(String.format("== Removed in `%s`%n", diff.rightName())); + writeRemovedProperties(differencesByType.get(Type.DELETED), differencesByType.get(Type.DEPRECATED)); + } + + private Map> differencesByType(ConfigurationMetadataDiff diff) { + Map> differencesByType = new HashMap<>(); + for (Type type : Type.values()) { + differencesByType.put(type, new ArrayList<>()); + } + for (Difference difference : diff.differences()) { + differencesByType.get(difference.type()).add(difference); + } + return differencesByType; + } + + private void writeDeprecatedProperties(List differences) { + if (differences.isEmpty()) { + this.out.append(String.format("None.%n")); + } + else { + List properties = sortProperties(differences, Difference::right).stream() + .filter(this::isDeprecatedInRelease) + .collect(Collectors.toList()); + this.out.append(String.format("|======================%n")); + this.out.append(String.format("|Key |Replacement |Reason%n")); + properties.forEach((diff) -> { + ConfigurationMetadataProperty property = diff.right(); + writeDeprecatedProperty(property); + }); + this.out.append(String.format("|======================%n")); + } + this.out.append(String.format("%n%n")); + } + + private boolean isDeprecatedInRelease(Difference difference) { + return difference.right().getDeprecation() != null + && Deprecation.Level.ERROR != difference.right().getDeprecation().getLevel(); + } + + private void writeAddedProperties(List differences) { + if (differences.isEmpty()) { + this.out.append(String.format("None.%n")); + } + else { + List properties = sortProperties(differences, Difference::right); + this.out.append(String.format("|======================%n")); + this.out.append(String.format("|Key |Default value |Description%n")); + properties.forEach((diff) -> writeRegularProperty(diff.right())); + this.out.append(String.format("|======================%n")); + } + this.out.append(String.format("%n%n")); + } + + private void writeRemovedProperties(List deleted, List deprecated) { + List removed = getRemovedProperties(deleted, deprecated); + if (removed.isEmpty()) { + this.out.append(String.format("None.%n")); + } + else { + this.out.append(String.format("|======================%n")); + this.out.append(String.format("|Key |Replacement |Reason%n")); + removed.forEach((property) -> writeDeprecatedProperty( + (property.right() != null) ? property.right() : property.left())); + this.out.append(String.format("|======================%n")); + } + } + + private List getRemovedProperties(List deleted, List deprecated) { + List properties = new ArrayList<>(deleted); + properties.addAll(deprecated.stream().filter((p) -> !isDeprecatedInRelease(p)).collect(Collectors.toList())); + return sortProperties(properties, + (difference) -> (difference.left() != null) ? difference.left() : difference.right()); + } + + private void writeRegularProperty(ConfigurationMetadataProperty property) { + this.out.append("|`").append(property.getId()).append("` |"); + if (property.getDefaultValue() != null) { + this.out.append("`").append(defaultValueToString(property.getDefaultValue())).append("`"); + } + this.out.append(" |"); + if (property.getDescription() != null) { + this.out.append(property.getShortDescription()); + } + this.out.append(System.lineSeparator()); + } + + private void writeDeprecatedProperty(ConfigurationMetadataProperty property) { + Deprecation deprecation = (property.getDeprecation() != null) ? property.getDeprecation() : new Deprecation(); + this.out.append("|`").append(property.getId()).append("` |"); + if (deprecation.getReplacement() != null) { + this.out.append("`").append(deprecation.getReplacement()).append("`"); + } + this.out.append(" |"); + if (deprecation.getReason() != null) { + this.out.append(getFirstSentence(deprecation.getReason())); + } + this.out.append(System.lineSeparator()); + } + + private String getFirstSentence(String text) { + int dot = text.indexOf('.'); + if (dot != -1) { + BreakIterator breakIterator = BreakIterator.getSentenceInstance(Locale.US); + breakIterator.setText(text); + String sentence = text.substring(breakIterator.first(), breakIterator.next()).trim(); + return removeSpaceBetweenLine(sentence); + } + else { + String[] lines = text.split(System.lineSeparator()); + return lines[0].trim(); + } + } + + private static String removeSpaceBetweenLine(String text) { + String[] lines = text.split(System.lineSeparator()); + StringBuilder sb = new StringBuilder(); + for (String line : lines) { + sb.append(line.trim()).append(" "); + } + return sb.toString().trim(); + } + + private List sortProperties(List properties, + Function property) { + List sorted = new ArrayList<>(properties); + sorted.sort((o1, o2) -> property.apply(o1).getId().compareTo(property.apply(o2).getId())); + return sorted; + } + + private static String defaultValueToString(Object defaultValue) { + if (defaultValue instanceof Object[]) { + return Stream.of((Object[]) defaultValue).map(Object::toString).collect(Collectors.joining(", ")); + } + else { + return defaultValue.toString(); + } + } + + @Override + public void close() { + this.out.close(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataDiff.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataDiff.java new file mode 100644 index 000000000000..260c7f95ea4b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataDiff.java @@ -0,0 +1,109 @@ +/* + * Copyright 2012-2023 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.configurationmetadata.changelog; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; +import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepository; +import org.springframework.boot.configurationmetadata.Deprecation.Level; +import org.springframework.boot.configurationmetadata.changelog.ConfigurationMetadataDiff.Difference.Type; + +/** + * A diff of two repositories of configuration metadata. + * + * @param leftName the name of the left-hand side of the diff + * @param rightName the name of the right-hand side of the diff + * @param differences the differences + * @author Stephane Nicoll + * @author Andy Wilkinson + */ +record ConfigurationMetadataDiff(String leftName, String rightName, List differences) { + + static ConfigurationMetadataDiff of(NamedConfigurationMetadataRepository left, + NamedConfigurationMetadataRepository right) { + return new ConfigurationMetadataDiff(left.getName(), right.getName(), differences(left, right)); + } + + private static List differences(ConfigurationMetadataRepository left, + ConfigurationMetadataRepository right) { + List differences = new ArrayList<>(); + List matches = new ArrayList<>(); + Map leftProperties = left.getAllProperties(); + Map rightProperties = right.getAllProperties(); + for (ConfigurationMetadataProperty leftProperty : leftProperties.values()) { + String id = leftProperty.getId(); + matches.add(id); + ConfigurationMetadataProperty rightProperty = rightProperties.get(id); + if (rightProperty == null) { + if (!(leftProperty.isDeprecated() && leftProperty.getDeprecation().getLevel() == Level.ERROR)) { + differences.add(new Difference(Type.DELETED, leftProperty, null)); + } + } + else if (rightProperty.isDeprecated() && !leftProperty.isDeprecated()) { + differences.add(new Difference(Type.DEPRECATED, leftProperty, rightProperty)); + } + else if (leftProperty.isDeprecated() && leftProperty.getDeprecation().getLevel() == Level.WARNING + && rightProperty.isDeprecated() && rightProperty.getDeprecation().getLevel() == Level.ERROR) { + differences.add(new Difference(Type.DELETED, leftProperty, rightProperty)); + } + } + for (ConfigurationMetadataProperty rightProperty : rightProperties.values()) { + if ((!matches.contains(rightProperty.getId())) && (!rightProperty.isDeprecated())) { + differences.add(new Difference(Type.ADDED, null, rightProperty)); + } + } + return differences; + } + + /** + * A difference in the metadata. + * + * @param type the type of the difference + * @param left the left-hand side of the difference + * @param right the right-hand side of the difference + */ + static record Difference(Type type, ConfigurationMetadataProperty left, ConfigurationMetadataProperty right) { + + /** + * The type of a difference in the metadata. + */ + enum Type { + + /** + * The entry has been added. + */ + ADDED, + + /** + * The entry has been made deprecated. It may or may not still exist in the + * previous version. + */ + DEPRECATED, + + /** + * The entry has been deleted. + */ + DELETED + + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/NamedConfigurationMetadataRepository.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/NamedConfigurationMetadataRepository.java new file mode 100644 index 000000000000..51ec5535e33e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/NamedConfigurationMetadataRepository.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2023 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.configurationmetadata.changelog; + +import java.io.File; +import java.io.IOException; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import org.springframework.boot.configurationmetadata.ConfigurationMetadataGroup; +import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; +import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepository; +import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepositoryJsonBuilder; + +/** + * A {@link ConfigurationMetadataRepository} with a name. + * + * @author Andy Wilkinson + */ +class NamedConfigurationMetadataRepository implements ConfigurationMetadataRepository { + + private final String name; + + private final ConfigurationMetadataRepository delegate; + + NamedConfigurationMetadataRepository(String name, ConfigurationMetadataRepository delegate) { + this.name = name; + this.delegate = delegate; + } + + /** + * The name of the metadata held in the repository. + * @return the name of the metadata + */ + String getName() { + return this.name; + } + + @Override + public Map getAllGroups() { + return this.delegate.getAllGroups(); + } + + @Override + public Map getAllProperties() { + return this.delegate.getAllProperties(); + } + + static NamedConfigurationMetadataRepository from(File metadataDir) { + ConfigurationMetadataRepositoryJsonBuilder builder = ConfigurationMetadataRepositoryJsonBuilder.create(); + for (File jar : metadataDir.listFiles()) { + try (JarFile jarFile = new JarFile(jar)) { + JarEntry jsonMetadata = jarFile.getJarEntry("META-INF/spring-configuration-metadata.json"); + if (jsonMetadata != null) { + builder.withJsonResource(jarFile.getInputStream(jsonMetadata)); + } + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + return new NamedConfigurationMetadataRepository(metadataDir.getName(), builder.build()); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/package-info.java new file mode 100644 index 000000000000..96eca3173f88 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Spring Boot configuration metadata changelog generator. + */ +package org.springframework.boot.configurationmetadata.changelog; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataDiffTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataDiffTests.java new file mode 100644 index 000000000000..787184a93f43 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataDiffTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2023 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.configurationmetadata.changelog; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; +import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepository; +import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepositoryJsonBuilder; +import org.springframework.boot.configurationmetadata.changelog.ConfigurationMetadataDiff.Difference; +import org.springframework.boot.configurationmetadata.changelog.ConfigurationMetadataDiff.Difference.Type; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConfigurationMetadataDiff}. + * + * @author Stephane Nicoll + * @author Andy Wilkinson + */ +class ConfigurationMetadataDiffTests { + + @Test + void diffContainsDifferencesBetweenLeftAndRightInputs() { + NamedConfigurationMetadataRepository left = new NamedConfigurationMetadataRepository("1.0", + load("sample-1.0.json")); + NamedConfigurationMetadataRepository right = new NamedConfigurationMetadataRepository("2.0", + load("sample-2.0.json")); + ConfigurationMetadataDiff diff = ConfigurationMetadataDiff.of(left, right); + assertThat(diff).isNotNull(); + assertThat(diff.leftName()).isEqualTo("1.0"); + assertThat(diff.rightName()).isEqualTo("2.0"); + assertThat(diff.differences()).hasSize(4); + List added = diff.differences() + .stream() + .filter((difference) -> difference.type() == Type.ADDED) + .collect(Collectors.toList()); + assertThat(added).hasSize(1); + assertProperty(added.get(0).right(), "test.add", String.class, "new"); + List deleted = diff.differences() + .stream() + .filter((difference) -> difference.type() == Type.DELETED) + .collect(Collectors.toList()); + assertThat(deleted).hasSize(2) + .anySatisfy((entry) -> assertProperty(entry.left(), "test.delete", String.class, "delete")) + .anySatisfy((entry) -> assertProperty(entry.right(), "test.delete.deprecated", String.class, "delete")); + List deprecated = diff.differences() + .stream() + .filter((difference) -> difference.type() == Type.DEPRECATED) + .collect(Collectors.toList()); + assertThat(deprecated).hasSize(1); + assertProperty(deprecated.get(0).left(), "test.deprecate", String.class, "wrong"); + assertProperty(deprecated.get(0).right(), "test.deprecate", String.class, "wrong"); + } + + private void assertProperty(ConfigurationMetadataProperty property, String id, Class type, Object defaultValue) { + assertThat(property).isNotNull(); + assertThat(property.getId()).isEqualTo(id); + assertThat(property.getType()).isEqualTo(type.getName()); + assertThat(property.getDefaultValue()).isEqualTo(defaultValue); + } + + private ConfigurationMetadataRepository load(String filename) { + try (InputStream inputStream = new FileInputStream("src/test/resources/" + filename)) { + return ConfigurationMetadataRepositoryJsonBuilder.create(inputStream).build(); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-1.0.json b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-1.0.json new file mode 100644 index 000000000000..a0584bc5695b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-1.0.json @@ -0,0 +1,31 @@ +{ + "properties": [ + { + "name": "test.equal", + "type": "java.lang.String", + "description": "Test equality.", + "defaultValue": "test" + }, + { + "name": "test.deprecate", + "type": "java.lang.String", + "description": "Test deprecate.", + "defaultValue": "wrong" + }, + { + "name": "test.delete", + "type": "java.lang.String", + "description": "Test delete.", + "defaultValue": "delete" + }, + { + "name": "test.delete.deprecated", + "type": "java.lang.String", + "description": "Test delete deprecated.", + "defaultValue": "delete", + "deprecation": { + "level": "warning" + } + } + ] +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-2.0.json b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-2.0.json new file mode 100644 index 000000000000..2de71ca99e57 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-2.0.json @@ -0,0 +1,34 @@ +{ + "properties": [ + { + "name": "test.add", + "type": "java.lang.String", + "description": "Test add.", + "defaultValue": "new" + }, + { + "name": "test.equal", + "type": "java.lang.String", + "description": "Test equality.", + "defaultValue": "test" + }, + { + "name": "test.deprecate", + "type": "java.lang.String", + "description": "Test deprecate.", + "defaultValue": "wrong", + "deprecation": { + "level": "error" + } + }, + { + "name": "test.delete.deprecated", + "type": "java.lang.String", + "description": "Test delete deprecated.", + "defaultValue": "delete", + "deprecation": { + "level": "error" + } + } + ] +} \ No newline at end of file diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 9062ad5e2b4f..3f7d8b5e0655 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -70,6 +70,7 @@ + From 849f65a0def0a8b5c28d4101690f0cf81ce74e4c Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 30 Jun 2023 13:47:04 +0200 Subject: [PATCH 0073/1215] Revert "Apply filter order to ServerHttpObservationFilter" This reverts commit efcc65bc5bb2870868cb43e540252cd87fa42a4a. --- .../servlet/WebMvcObservationAutoConfiguration.java | 4 ++-- .../WebMvcObservationAutoConfigurationTests.java | 10 ---------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java index e6f1c487def1..2b4aa96c3933 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java @@ -40,6 +40,7 @@ import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.server.observation.DefaultServerRequestObservationConvention; import org.springframework.http.server.observation.ServerRequestObservationConvention; @@ -53,7 +54,6 @@ * @author Brian Clozel * @author Jon Schneider * @author Dmytro Nosan - * @author Moritz Halbritter * @since 3.0.0 */ @AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, @@ -74,7 +74,7 @@ public FilterRegistrationBean webMvcObservationFilt .getIfAvailable(() -> new DefaultServerRequestObservationConvention(name)); ServerHttpObservationFilter filter = new ServerHttpObservationFilter(registry, convention); FilterRegistrationBean registration = new FilterRegistrationBean<>(filter); - registration.setOrder(observationProperties.getHttp().getServer().getFilter().getOrder()); + registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1); registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC); return registration; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java index b996ba206430..3fd1a2b61bef 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java @@ -57,7 +57,6 @@ * @author Tadaya Tsuyukubo * @author Madhura Bhave * @author Chanhyeong LEE - * @author Moritz Halbritter */ @ExtendWith(OutputCaptureExtension.class) class WebMvcObservationAutoConfigurationTests { @@ -101,15 +100,6 @@ void filterRegistrationHasExpectedDispatcherTypesAndOrder() { }); } - @Test - void filterRegistrationOrderCanBeOverridden() { - this.contextRunner.withPropertyValues("management.observations.http.server.filter.order=1000") - .run((context) -> { - FilterRegistrationBean registration = context.getBean(FilterRegistrationBean.class); - assertThat(registration.getOrder()).isEqualTo(1000); - }); - } - @Test void filterRegistrationBacksOffWithAnotherServerHttpObservationFilterRegistration() { this.contextRunner.withUserConfiguration(TestServerHttpObservationFilterRegistrationConfiguration.class) From b4bc7cebbc54783ab99f9fbcc7444eda6ce7c297 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 30 Jun 2023 14:00:38 +0200 Subject: [PATCH 0074/1215] Revert "Add property to specify the order of ServerHttpObservationFilter" This reverts commit 7b90fbb0b2ef73871ca776d9f999e9cdc1cb7e6d. --- .../observation/ObservationProperties.java | 24 ---------- .../OrderedServerHttpObservationFilter.java | 47 ------------------- .../WebFluxObservationAutoConfiguration.java | 8 ++-- ...FluxObservationAutoConfigurationTests.java | 10 ---- 4 files changed, 4 insertions(+), 85 deletions(-) delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/OrderedServerHttpObservationFilter.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java index 24d81daa4b1a..08de1a01c104 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java @@ -20,7 +20,6 @@ import java.util.Map; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.core.Ordered; /** * {@link ConfigurationProperties @ConfigurationProperties} for configuring Micrometer @@ -111,16 +110,10 @@ public static class Server { private final ServerRequests requests = new ServerRequests(); - private final Filter filter = new Filter(); - public ServerRequests getRequests() { return this.requests; } - public Filter getFilter() { - return this.filter; - } - public static class ServerRequests { /** @@ -138,23 +131,6 @@ public void setName(String name) { } - public static class Filter { - - /** - * Order of the filter that creates the observations. - */ - private int order = Ordered.HIGHEST_PRECEDENCE + 1; - - public int getOrder() { - return this.order; - } - - public void setOrder(int order) { - this.order = order; - } - - } - } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/OrderedServerHttpObservationFilter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/OrderedServerHttpObservationFilter.java deleted file mode 100644 index 4541f6e16521..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/OrderedServerHttpObservationFilter.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2012-2023 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.actuate.autoconfigure.observation.web.reactive; - -import io.micrometer.observation.ObservationRegistry; - -import org.springframework.boot.web.reactive.filter.OrderedWebFilter; -import org.springframework.core.Ordered; -import org.springframework.http.server.reactive.observation.ServerRequestObservationConvention; -import org.springframework.web.filter.reactive.ServerHttpObservationFilter; - -/** - * {@link ServerHttpObservationFilter} that also implements {@link Ordered}. - * - * @author Moritz Halbritter - */ -@SuppressWarnings({ "deprecation", "removal" }) -class OrderedServerHttpObservationFilter extends ServerHttpObservationFilter implements OrderedWebFilter { - - private final int order; - - OrderedServerHttpObservationFilter(ObservationRegistry observationRegistry, - ServerRequestObservationConvention observationConvention, int order) { - super(observationRegistry, observationConvention); - this.order = order; - } - - @Override - public int getOrder() { - return this.order; - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java index 457d2cdf3937..91b1d6fa95aa 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java @@ -38,6 +38,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention; import org.springframework.http.server.reactive.observation.ServerRequestObservationConvention; @@ -50,7 +51,6 @@ * @author Brian Clozel * @author Jon Schneider * @author Dmytro Nosan - * @author Moritz Halbritter * @since 3.0.0 */ @AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, @@ -70,13 +70,13 @@ public WebFluxObservationAutoConfiguration(ObservationProperties observationProp @Bean @ConditionalOnMissingBean(ServerHttpObservationFilter.class) - public OrderedServerHttpObservationFilter webfluxObservationFilter(ObservationRegistry registry, + @Order(Ordered.HIGHEST_PRECEDENCE + 1) + public ServerHttpObservationFilter webfluxObservationFilter(ObservationRegistry registry, ObjectProvider customConvention) { String name = this.observationProperties.getHttp().getServer().getRequests().getName(); ServerRequestObservationConvention convention = customConvention .getIfAvailable(() -> new DefaultServerRequestObservationConvention(name)); - int order = this.observationProperties.getHttp().getServer().getFilter().getOrder(); - return new OrderedServerHttpObservationFilter(registry, convention, order); + return new ServerHttpObservationFilter(registry, convention); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java index 939b754424a8..48c065660fa0 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java @@ -52,7 +52,6 @@ * @author Brian Clozel * @author Dmytro Nosan * @author Madhura Bhave - * @author Moritz Halbritter */ @ExtendWith(OutputCaptureExtension.class) @SuppressWarnings("removal") @@ -128,15 +127,6 @@ void shouldNotDenyNorLogIfMaxUrisIsNotReached(CapturedOutput output) { }); } - @Test - void shouldUsePropertyForServerHttpObservationFilterOrder() { - this.contextRunner.withPropertyValues("management.observations.http.server.filter.order=1000") - .run((context) -> { - OrderedServerHttpObservationFilter bean = context.getBean(OrderedServerHttpObservationFilter.class); - assertThat(bean.getOrder()).isEqualTo(1000); - }); - } - private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicationContext context) throws Exception { return getInitializedMeterRegistry(context, "/test0", "/test1", "/test2"); From 298bfd96c2b9c14d7443570e73e7d5c4273ca6a8 Mon Sep 17 00:00:00 2001 From: Ahmed Ashour Date: Wed, 28 Jun 2023 12:39:54 +0000 Subject: [PATCH 0075/1215] Change WebServer log messages to use port or ports, not port(s) See gh-36103 --- .../boot/build/docs/ApplicationRunner.java | 4 ++-- .../endpoint/web/documentation/sample.log | 4 ++-- .../boot/rsocket/netty/NettyRSocketServer.java | 2 +- .../web/embedded/jetty/JettyWebServer.java | 14 +++++++++++--- .../web/embedded/tomcat/TomcatWebServer.java | 18 +++++++++++------- .../embedded/undertow/UndertowWebServer.java | 16 +++++++++++++--- 6 files changed, 40 insertions(+), 18 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/docs/ApplicationRunner.java b/buildSrc/src/main/java/org/springframework/boot/build/docs/ApplicationRunner.java index 0f95d55d3d67..785be772544b 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/docs/ApplicationRunner.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/docs/ApplicationRunner.java @@ -105,8 +105,8 @@ public Property getApplicationJar() { } public void normalizeTomcatPort() { - this.normalizations.put("(Tomcat started on port\\(s\\): )[\\d]+( \\(http\\))", "$18080$2"); - this.normalizations.put("(Tomcat initialized with port\\(s\\): )[\\d]+( \\(http\\))", "$18080$2"); + this.normalizations.put("(Tomcat started on port )[\\d]+( \\(http\\))", "$18080$2"); + this.normalizations.put("(Tomcat initialized with port )[\\d]+( \\(http\\))", "$18080$2"); } public void normalizeLiveReloadPort() { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/sample.log b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/sample.log index 9b8f1ab7eced..b1f92c2d2c35 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/sample.log +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/resources/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/sample.log @@ -9,7 +9,7 @@ 2017-08-08 17:12:30.910 INFO 19866 --- [ main] s.f.SampleWebFreeMarkerApplication : Starting SampleWebFreeMarkerApplication with PID 19866 2017-08-08 17:12:30.913 INFO 19866 --- [ main] s.f.SampleWebFreeMarkerApplication : No active profile set, falling back to default profiles: default 2017-08-08 17:12:30.952 INFO 19866 --- [ main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@76b10754: startup date [Tue Aug 08 17:12:30 BST 2017]; root of context hierarchy -2017-08-08 17:12:31.878 INFO 19866 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) +2017-08-08 17:12:31.878 INFO 19866 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http) 2017-08-08 17:12:31.889 INFO 19866 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2017-08-08 17:12:31.890 INFO 19866 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.16 2017-08-08 17:12:31.978 INFO 19866 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext @@ -27,5 +27,5 @@ 2017-08-08 17:12:32.471 INFO 19866 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 2017-08-08 17:12:32.600 INFO 19866 --- [ main] o.s.w.s.v.f.FreeMarkerConfigurer : ClassTemplateLoader for Spring macros added to FreeMarker configuration 2017-08-08 17:12:32.681 INFO 19866 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup -2017-08-08 17:12:32.744 INFO 19866 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) +2017-08-08 17:12:32.744 INFO 19866 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) 2017-08-08 17:12:32.750 INFO 19866 --- [ main] s.f.SampleWebFreeMarkerApplication : Started SampleWebFreeMarkerApplication in 2.172 seconds (JVM running for 2.479) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServer.java index cb2df6779f62..f50f847346dd 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServer.java @@ -62,7 +62,7 @@ public InetSocketAddress address() { @Override public void start() throws RSocketServerException { this.channel = block(this.starter, this.lifecycleTimeout); - logger.info("Netty RSocket started on port(s): " + address().getPort()); + logger.info("Netty RSocket started on port " + address().getPort()); startDaemonAwaitThread(this.channel); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java index 21f1052e6ed2..cd0951ce994a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java @@ -168,7 +168,7 @@ public void start() throws WebServerException { } } this.started = true; - logger.info("Jetty started on port(s) " + getActualPortsDescription() + " with context path '" + logger.info("Jetty started on " + getActualPortsDescription() + " with context path '" + getContextPath() + "'"); } catch (WebServerException ex) { @@ -184,10 +184,18 @@ public void start() throws WebServerException { private String getActualPortsDescription() { StringBuilder ports = new StringBuilder(); - for (Connector connector : this.server.getConnectors()) { - if (ports.length() != 0) { + Connector[] connectors = this.server.getConnectors(); + ports.append("port"); + if (connectors.length != 1) { + ports.append("s"); + } + ports.append(" "); + + for (int i = 0; i < connectors.length; i++) { + if (i != 0) { ports.append(", "); } + Connector connector = connectors[i]; ports.append(getLocalPort(connector)).append(getProtocols(connector)); } return ports.toString(); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java index c2dfde03e9e7..7319f620365d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java @@ -105,7 +105,7 @@ public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) { } private void initialize() throws WebServerException { - logger.info("Tomcat initialized with port(s): " + getPortsDescription(false)); + logger.info("Tomcat initialized with " + getPortsDescription(false)); synchronized (this.monitor) { try { addInstanceIdToEngineName(); @@ -218,8 +218,8 @@ public void start() throws WebServerException { } checkThatConnectorsHaveStarted(); this.started = true; - logger.info("Tomcat started on port(s): " + getPortsDescription(true) + " with context path '" - + getContextPath() + "'"); + logger.info("Tomcat started on " + getPortsDescription(true) + " with context path '" + getContextPath() + + "'"); } catch (ConnectorStartFailedException ex) { stopSilently(); @@ -356,10 +356,14 @@ public void destroy() throws WebServerException { private String getPortsDescription(boolean localPort) { StringBuilder ports = new StringBuilder(); - for (Connector connector : this.tomcat.getService().findConnectors()) { - if (ports.length() != 0) { - ports.append(' '); - } + Connector[] connectors = this.tomcat.getService().findConnectors(); + ports.append("port"); + if (connectors.length != 1) { + ports.append("s"); + } + + for (Connector connector : connectors) { + ports.append(' '); int port = localPort ? connector.getLocalPort() : connector.getPort(); ports.append(port).append(" (").append(connector.getScheme()).append(')'); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java index 563b2fc4b197..4ac2c5b897ee 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java @@ -182,11 +182,21 @@ protected HttpHandler createHttpHandler() { } private String getPortsDescription() { + StringBuilder portsDescription = new StringBuilder(); List ports = getActualPorts(); + portsDescription.append("port"); + if (ports.size() != 1) { + portsDescription.append("s"); + } + portsDescription.append(" "); + if (!ports.isEmpty()) { - return StringUtils.collectionToDelimitedString(ports, " "); + portsDescription.append(StringUtils.collectionToDelimitedString(ports, " ")); + } + else { + portsDescription.append("unknown"); } - return "unknown"; + return portsDescription.toString(); } private List getActualPorts() { @@ -315,7 +325,7 @@ private void notifyGracefulCallback(boolean success) { } protected String getStartLogMessage() { - return "Undertow started on port(s) " + getPortsDescription(); + return "Undertow started on " + getPortsDescription(); } /** From 318198ae5d7a1a22846ad3b00028c393b10fff26 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 30 Jun 2023 19:41:14 +0100 Subject: [PATCH 0076/1215] Polish "Change WebServer log messages to use port or ports, not port(s)" See gh-36103 --- .../web/embedded/jetty/JettyWebServer.java | 21 +++++++------ .../web/embedded/netty/NettyWebServer.java | 10 ++++-- .../web/embedded/tomcat/TomcatWebServer.java | 26 ++++++++++------ .../undertow/UndertowServletWebServer.java | 13 +++++--- .../embedded/undertow/UndertowWebServer.java | 15 +++++---- .../JettyReactiveWebServerFactoryTests.java | 15 +++++++++ .../JettyServletWebServerFactoryTests.java | 5 +++ .../NettyReactiveWebServerFactoryTests.java | 17 ++++++++++ .../TomcatReactiveWebServerFactoryTests.java | 12 +++++++ .../TomcatServletWebServerFactoryTests.java | 5 +++ ...UndertowReactiveWebServerFactoryTests.java | 12 +++++++ .../UndertowServletWebServerFactoryTests.java | 5 +++ ...AbstractReactiveWebServerFactoryTests.java | 23 ++++++++++++++ .../AbstractServletWebServerFactoryTests.java | 31 +++++++++++++++++++ 14 files changed, 174 insertions(+), 36 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java index cd0951ce994a..f85210163754 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java @@ -168,8 +168,7 @@ public void start() throws WebServerException { } } this.started = true; - logger.info("Jetty started on " + getActualPortsDescription() + " with context path '" - + getContextPath() + "'"); + logger.info(getStartedLogMessage()); } catch (WebServerException ex) { stopSilently(); @@ -182,23 +181,25 @@ public void start() throws WebServerException { } } + String getStartedLogMessage() { + return "Jetty started on " + getActualPortsDescription() + " with context path '" + getContextPath() + "'"; + } + private String getActualPortsDescription() { - StringBuilder ports = new StringBuilder(); + StringBuilder description = new StringBuilder("port"); Connector[] connectors = this.server.getConnectors(); - ports.append("port"); if (connectors.length != 1) { - ports.append("s"); + description.append("s"); } - ports.append(" "); - + description.append(" "); for (int i = 0; i < connectors.length; i++) { if (i != 0) { - ports.append(", "); + description.append(", "); } Connector connector = connectors[i]; - ports.append(getLocalPort(connector)).append(getProtocols(connector)); + description.append(getLocalPort(connector)).append(getProtocols(connector)); } - return ports.toString(); + return description.toString(); } private String getProtocols(Connector connector) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java index 8b555f350f75..f21f47a70081 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -108,7 +108,7 @@ public void start() throws WebServerException { throw new WebServerException("Unable to start Netty", ex); } if (this.disposableServer != null) { - logger.info("Netty started" + getStartedOnMessage(this.disposableServer)); + logger.info(getStartedOnMessage(this.disposableServer)); } startDaemonAwaitThread(this.disposableServer); } @@ -118,7 +118,11 @@ private String getStartedOnMessage(DisposableServer server) { StringBuilder message = new StringBuilder(); tryAppend(message, "port %s", server::port); tryAppend(message, "path %s", server::path); - return (message.length() > 0) ? " on " + message : ""; + return (message.length() > 0) ? "Netty started on " + message : "Netty started"; + } + + protected String getStartedLogMessage() { + return getStartedOnMessage(this.disposableServer); } private void tryAppend(StringBuilder message, String format, Supplier supplier) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java index 7319f620365d..86f3a0ff2c46 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java @@ -218,8 +218,7 @@ public void start() throws WebServerException { } checkThatConnectorsHaveStarted(); this.started = true; - logger.info("Tomcat started on " + getPortsDescription(true) + " with context path '" + getContextPath() - + "'"); + logger.info(getStartedLogMessage()); } catch (ConnectorStartFailedException ex) { stopSilently(); @@ -236,6 +235,10 @@ public void start() throws WebServerException { } } + String getStartedLogMessage() { + return "Tomcat started on " + getPortsDescription(true) + " with context path '" + getContextPath() + "'"; + } + private void checkThatConnectorsHaveStarted() { checkConnectorHasStarted(this.tomcat.getConnector()); for (Connector connector : this.tomcat.getService().findConnectors()) { @@ -355,19 +358,22 @@ public void destroy() throws WebServerException { } private String getPortsDescription(boolean localPort) { - StringBuilder ports = new StringBuilder(); + StringBuilder description = new StringBuilder(); Connector[] connectors = this.tomcat.getService().findConnectors(); - ports.append("port"); + description.append("port"); if (connectors.length != 1) { - ports.append("s"); + description.append("s"); } - - for (Connector connector : connectors) { - ports.append(' '); + description.append(" "); + for (int i = 0; i < connectors.length; i++) { + if (i != 0) { + description.append(", "); + } + Connector connector = connectors[i]; int port = localPort ? connector.getLocalPort() : connector.getPort(); - ports.append(port).append(" (").append(connector.getScheme()).append(')'); + description.append(port).append(" (").append(connector.getScheme()).append(')'); } - return ports.toString(); + return description.toString(); } @Override diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServer.java index 11ce72755bae..3a0a644d2459 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -78,11 +78,14 @@ protected HttpHandler createHttpHandler() { @Override protected String getStartLogMessage() { - String message = super.getStartLogMessage(); - if (StringUtils.hasText(this.contextPath)) { - message += " with context path '" + this.contextPath + "'"; + if (!StringUtils.hasText(this.contextPath)) { + return super.getStartLogMessage(); } - return message; + StringBuilder message = new StringBuilder(super.getStartLogMessage()); + message.append(" with context path '"); + message.append(this.contextPath); + message.append("'"); + return message.toString(); } public DeploymentManager getDeploymentManager() { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java index 4ac2c5b897ee..bce6152a7e68 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java @@ -182,21 +182,20 @@ protected HttpHandler createHttpHandler() { } private String getPortsDescription() { - StringBuilder portsDescription = new StringBuilder(); + StringBuilder description = new StringBuilder(); List ports = getActualPorts(); - portsDescription.append("port"); + description.append("port"); if (ports.size() != 1) { - portsDescription.append("s"); + description.append("s"); } - portsDescription.append(" "); - + description.append(" "); if (!ports.isEmpty()) { - portsDescription.append(StringUtils.collectionToDelimitedString(ports, " ")); + description.append(StringUtils.collectionToDelimitedString(ports, ", ")); } else { - portsDescription.append("unknown"); + description.append("unknown"); } - return portsDescription.toString(); + return description.toString(); } private List getActualPorts() { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java index cf42ba29d7a4..f8baaa8db5b6 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java @@ -31,6 +31,7 @@ import org.mockito.InOrder; import org.springframework.boot.testsupport.web.servlet.Servlet5ClassPathOverrides; +import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactoryTests; import org.springframework.boot.web.server.Shutdown; import org.springframework.http.client.reactive.JettyResourceFactory; @@ -161,4 +162,18 @@ void shouldApplyMaxConnections() { assertThat(connectionLimit.getMaxConnections()).isOne(); } + @Override + protected String startedLogMessage() { + return ((JettyWebServer) this.webServer).getStartedLogMessage(); + } + + @Override + protected void addConnector(int port, AbstractReactiveWebServerFactory factory) { + ((JettyReactiveWebServerFactory) factory).addServerCustomizers((server) -> { + ServerConnector connector = new ServerConnector(server); + connector.setPort(port); + server.addConnector(connector); + }); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java index 9dc3ec21c541..560269579c62 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java @@ -531,6 +531,11 @@ void shouldApplyMaxConnections() { assertThat(connectionLimit.getMaxConnections()).isOne(); } + @Override + protected String startedLogMessage() { + return ((JettyWebServer) this.webServer).getStartedLogMessage(); + } + private WebAppContext findWebAppContext(JettyWebServer webServer) { return findWebAppContext(webServer.getServer().getHandler()); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java index 61f123b74bc3..b1e8a318aa1b 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java @@ -23,6 +23,7 @@ import io.netty.channel.Channel; import org.awaitility.Awaitility; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.InOrder; import reactor.core.CoreSubscriber; @@ -135,6 +136,12 @@ void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() { this.webServer.stop(); } + @Override + @Test + @Disabled("Reactor Netty does not support mutiple ports") + protected void startedLogMessageWithMultiplePorts() { + } + protected Mono testSslWithAlias(String alias) { String keyStore = "classpath:test.jks"; String keyPassword = "password"; @@ -164,6 +171,16 @@ protected NettyReactiveWebServerFactory getFactory() { return new NettyReactiveWebServerFactory(0); } + @Override + protected String startedLogMessage() { + return ((NettyWebServer) this.webServer).getStartedLogMessage(); + } + + @Override + protected void addConnector(int port, AbstractReactiveWebServerFactory factory) { + throw new UnsupportedOperationException("Reactor Netty does not support multiple ports"); + } + static class NoPortNettyReactiveWebServerFactory extends NettyReactiveWebServerFactory { NoPortNettyReactiveWebServerFactory(int port) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactoryTests.java index 4bbea8b0d41b..aaa1170c62ff 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactoryTests.java @@ -275,4 +275,16 @@ private void handleExceptionCausedByBlockedPortOnPrimaryConnector(RuntimeExcepti assertThat(((PortInUseException) ex).getPort()).isEqualTo(blockedPort); } + @Override + protected String startedLogMessage() { + return ((TomcatWebServer) this.webServer).getStartedLogMessage(); + } + + @Override + protected void addConnector(int port, AbstractReactiveWebServerFactory factory) { + Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol"); + connector.setPort(port); + ((TomcatReactiveWebServerFactory) factory).addAdditionalTomcatConnectors(connector); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java index ba8c07b6c0e9..e619c9120ccd 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java @@ -689,4 +689,9 @@ protected void handleExceptionCausedByBlockedPortOnSecondaryConnector(RuntimeExc assertThat(((ConnectorStartFailedException) ex).getPort()).isEqualTo(blockedPort); } + @Override + protected String startedLogMessage() { + return ((TomcatWebServer) this.webServer).getStartedLogMessage(); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactoryTests.java index 836532109fff..8b607e228a97 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactoryTests.java @@ -27,6 +27,7 @@ import org.mockito.InOrder; import reactor.core.publisher.Mono; +import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactoryTests; import org.springframework.boot.web.server.Shutdown; import org.springframework.http.MediaType; @@ -155,4 +156,15 @@ private void awaitFile(File file) { Awaitility.waitAtMost(Duration.ofSeconds(10)).until(file::exists, is(true)); } + @Override + protected String startedLogMessage() { + return ((UndertowWebServer) this.webServer).getStartLogMessage(); + } + + @Override + protected void addConnector(int port, AbstractReactiveWebServerFactory factory) { + ((UndertowReactiveWebServerFactory) factory) + .addBuilderCustomizers((builder) -> builder.addHttpListener(port, "0.0.0.0")); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactoryTests.java index f0c5f7d4bd55..48d9aafda094 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactoryTests.java @@ -333,4 +333,9 @@ protected void handleExceptionCausedByBlockedPortOnSecondaryConnector(RuntimeExc handleExceptionCausedByBlockedPortOnPrimaryConnector(ex, blockedPort); } + @Override + protected String startedLogMessage() { + return ((UndertowServletWebServer) this.webServer).getStartLogMessage(); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java index 2fb586580812..509483854ee0 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java @@ -602,6 +602,25 @@ protected void whenHttp2IsEnabledAndSslIsDisabledThenHttp11CanStillBeUsed() { assertThat(result.block(Duration.ofSeconds(30))).isEqualTo("Hello World"); } + @Test + void startedLogMessageWithSinglePort() { + AbstractReactiveWebServerFactory factory = getFactory(); + this.webServer = factory.getWebServer(new EchoHandler()); + this.webServer.start(); + assertThat(startedLogMessage()).matches("(Jetty|Netty|Tomcat|Undertow) started on port " + + this.webServer.getPort() + "( \\(http(/1.1)?\\))?( with context path '(/)?')?"); + } + + @Test + protected void startedLogMessageWithMultiplePorts() { + AbstractReactiveWebServerFactory factory = getFactory(); + addConnector(0, factory); + this.webServer = factory.getWebServer(new EchoHandler()); + this.webServer.start(); + assertThat(startedLogMessage()).matches("(Jetty|Tomcat|Undertow) started on ports " + this.webServer.getPort() + + "( \\(http(/1.1)?\\))?, [0-9]+( \\(http(/1.1)?\\))?( with context path '(/)?')?"); + } + protected WebClient prepareCompressionTest() { Compression compression = new Compression(); compression.setEnabled(true); @@ -673,6 +692,10 @@ protected final void doWithBlockedPort(BlockedPortAction action) throws Exceptio } } + protected abstract String startedLogMessage(); + + protected abstract void addConnector(int port, AbstractReactiveWebServerFactory factory); + public interface BlockedPortAction { void run(int port); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java index c6c53781daf6..0953d225aefe 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java @@ -1336,6 +1336,35 @@ void whenARequestIsActiveAfterGracefulShutdownEndsThenStopWillComplete() throws } } + @Test + void startedLogMessageWithSinglePort() { + AbstractServletWebServerFactory factory = getFactory(); + this.webServer = factory.getWebServer(); + this.webServer.start(); + assertThat(startedLogMessage()).matches("(Jetty|Tomcat|Undertow) started on port " + this.webServer.getPort() + + " \\(http(/1.1)?\\)( with context path '(/)?')?"); + } + + @Test + void startedLogMessageWithSinglePortAndContextPath() { + AbstractServletWebServerFactory factory = getFactory(); + factory.setContextPath("/test"); + this.webServer = factory.getWebServer(); + this.webServer.start(); + assertThat(startedLogMessage()).matches("(Jetty|Tomcat|Undertow) started on port " + this.webServer.getPort() + + " \\(http(/1.1)?\\) with context path '/test'"); + } + + @Test + void startedLogMessageWithMultiplePorts() { + AbstractServletWebServerFactory factory = getFactory(); + addConnector(0, factory); + this.webServer = factory.getWebServer(); + this.webServer.start(); + assertThat(startedLogMessage()).matches("(Jetty|Tomcat|Undertow) started on ports " + this.webServer.getPort() + + " \\(http(/1.1)?\\), [0-9]+ \\(http(/1.1)?\\)( with context path '(/)?')?"); + } + protected Future initiateGetRequest(int port, String path) { return initiateGetRequest(HttpClients.createMinimal(), port, path); } @@ -1584,6 +1613,8 @@ private void loadStore(KeyStore keyStore, Resource resource) } } + protected abstract String startedLogMessage(); + private class TestGzipInputStreamFactory implements InputStreamFactory { private final AtomicBoolean requested = new AtomicBoolean(); From 1bf334ae0fcc33e59d39598ae87e597548fad19c Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 30 Jun 2023 22:15:19 +0100 Subject: [PATCH 0077/1215] Polish config metadata changelog generator See gh-21486 --- .../build.gradle | 2 +- .../changelog/Changelog.java | 64 +++++ .../changelog/ChangelogGenerator.java | 79 ++++++ .../changelog/ChangelogWriter.java | 224 ++++++++++++++++++ ...nfigurationMetadataChangelogGenerator.java | 56 ----- .../ConfigurationMetadataChangelogWriter.java | 204 ---------------- .../changelog/ConfigurationMetadataDiff.java | 109 --------- .../changelog/Difference.java | 52 ++++ .../changelog/DifferenceType.java | 42 ++++ .../NamedConfigurationMetadataRepository.java | 80 ------- .../changelog/ChangelogGeneratorTests.java | 68 ++++++ .../changelog/ChangelogTests.java | 73 ++++++ .../changelog/ChangelogWriterTests.java | 45 ++++ .../ConfigurationMetadataDiffTests.java | 92 ------- .../changelog/TestChangelog.java | 52 ++++ .../src/test/resources/sample-2.0.json | 4 +- .../src/test/resources/sample.adoc | 32 +++ 17 files changed, 735 insertions(+), 543 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Changelog.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogGenerator.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataChangelogGenerator.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataChangelogWriter.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataDiff.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Difference.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/DifferenceType.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/NamedConfigurationMetadataRepository.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogGeneratorTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriterTests.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataDiffTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/TestChangelog.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample.adoc diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/build.gradle index 4d5c517e9040..186a2cff85a1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/build.gradle @@ -50,7 +50,7 @@ if (project.hasProperty("oldVersion") && project.hasProperty("newVersion")) { inputs.files(prepareOldMetadata, prepareNewMetadata) outputs.file(project.file("build/configuration-metadata-changelog.adoc")) classpath = sourceSets.main.runtimeClasspath - mainClass = 'org.springframework.boot.configurationmetadata.changelog.ConfigurationMetadataChangelogGenerator' + mainClass = 'org.springframework.boot.configurationmetadata.changelog.ChangelogGenerator' if (project.hasProperty("oldVersion") && project.hasProperty("newVersion")) { args = [project.file("build/configuration-metadata-diff/$oldVersion"), project.file("build/configuration-metadata-diff/$newVersion"), project.file("build/configuration-metadata-changelog.adoc")] } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Changelog.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Changelog.java new file mode 100644 index 000000000000..964298fe567c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Changelog.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2023 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.configurationmetadata.changelog; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; +import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepository; + +/** + * A changelog containing differences computed from two repositories of configuration + * metadata. + * + * @param oldVersionNumber the name of the old version + * @param newVersionNumber the name of the new version + * @param differences the differences + * @author Stephane Nicoll + * @author Andy Wilkinson + * @author Phillip Webb + */ +record Changelog(String oldVersionNumber, String newVersionNumber, List differences) { + + static Changelog of(String oldVersionNumber, ConfigurationMetadataRepository oldMetadata, String newVersionNumber, + ConfigurationMetadataRepository newMetadata) { + return new Changelog(oldVersionNumber, newVersionNumber, computeDifferences(oldMetadata, newMetadata)); + } + + static List computeDifferences(ConfigurationMetadataRepository oldMetadata, + ConfigurationMetadataRepository newMetadata) { + List seenIds = new ArrayList<>(); + List differences = new ArrayList<>(); + for (ConfigurationMetadataProperty oldProperty : oldMetadata.getAllProperties().values()) { + String id = oldProperty.getId(); + seenIds.add(id); + ConfigurationMetadataProperty newProperty = newMetadata.getAllProperties().get(id); + Difference difference = Difference.compute(oldProperty, newProperty); + if (difference != null) { + differences.add(difference); + } + } + for (ConfigurationMetadataProperty newProperty : newMetadata.getAllProperties().values()) { + if ((!seenIds.contains(newProperty.getId())) && (!newProperty.isDeprecated())) { + differences.add(new Difference(DifferenceType.ADDED, null, newProperty)); + } + } + return List.copyOf(differences); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogGenerator.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogGenerator.java new file mode 100644 index 000000000000..9d1ee1d62282 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogGenerator.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2023 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.configurationmetadata.changelog; + +import java.io.File; +import java.io.IOException; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepository; +import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepositoryJsonBuilder; + +/** + * Generates a configuration metadata changelog. Requires three arguments: + * + *
    + *
  1. The path of a directory containing jar files of the old version + *
  2. The path of a directory containing jar files of the new version + *
  3. The path of a file to which the asciidoc changelog will be written + *
+ * + * The name of each directory will be used as version numbers in generated changelog. + * + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.2.0 + */ +public final class ChangelogGenerator { + + private ChangelogGenerator() { + } + + public static void main(String[] args) throws IOException { + generate(new File(args[0]), new File(args[1]), new File(args[2])); + } + + private static void generate(File oldDir, File newDir, File out) throws IOException { + String oldVersionNumber = oldDir.getName(); + ConfigurationMetadataRepository oldMetadata = buildRepository(oldDir); + String newVersionNumber = newDir.getName(); + ConfigurationMetadataRepository newMetadata = buildRepository(newDir); + Changelog changelog = Changelog.of(oldVersionNumber, oldMetadata, newVersionNumber, newMetadata); + try (ChangelogWriter writer = new ChangelogWriter(out)) { + writer.write(changelog); + } + System.out.println("%nConfiguration metadata changelog written to '%s'".formatted(out)); + } + + static ConfigurationMetadataRepository buildRepository(File directory) { + ConfigurationMetadataRepositoryJsonBuilder builder = ConfigurationMetadataRepositoryJsonBuilder.create(); + for (File file : directory.listFiles()) { + try (JarFile jarFile = new JarFile(file)) { + JarEntry metadataEntry = jarFile.getJarEntry("META-INF/spring-configuration-metadata.json"); + if (metadataEntry != null) { + builder.withJsonResource(jarFile.getInputStream(metadataEntry)); + } + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + return builder.build(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java new file mode 100644 index 000000000000..fd79845d0ecf --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java @@ -0,0 +1,224 @@ +/* + * Copyright 2012-2023 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.configurationmetadata.changelog; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Writer; +import java.text.BreakIterator; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; +import org.springframework.boot.configurationmetadata.Deprecation; + +/** + * Writes a {@link Changelog} using asciidoc markup. + * + * @author Stephane Nicoll + * @author Andy Wilkinson + * @author Phillip Webb + */ +class ChangelogWriter implements AutoCloseable { + + private static final Comparator COMPARING_ID = Comparator + .comparing(ConfigurationMetadataProperty::getId); + + private final PrintWriter out; + + ChangelogWriter(File out) throws IOException { + this(new FileWriter(out)); + } + + ChangelogWriter(Writer out) { + this.out = new PrintWriter(out); + } + + void write(Changelog changelog) { + String oldVersionNumber = changelog.oldVersionNumber(); + String newVersionNumber = changelog.newVersionNumber(); + Map> differencesByType = collateByType(changelog); + write("Configuration property changes between `%s` and `%s`%n", oldVersionNumber, newVersionNumber); + write("%n%n%n== Deprecated in %s%n", newVersionNumber); + writeDeprecated(differencesByType.get(DifferenceType.DEPRECATED)); + write("%n%n%n== Added in %s%n", newVersionNumber); + writeAdded(differencesByType.get(DifferenceType.ADDED)); + write("%n%n%n== Removed in %s%n", newVersionNumber); + writeRemoved(differencesByType.get(DifferenceType.DELETED), differencesByType.get(DifferenceType.DEPRECATED)); + } + + private Map> collateByType(Changelog differences) { + Map> byType = new HashMap<>(); + for (DifferenceType type : DifferenceType.values()) { + byType.put(type, new ArrayList<>()); + } + for (Difference difference : differences.differences()) { + byType.get(difference.type()).add(difference); + } + return byType; + } + + private void writeDeprecated(List differences) { + List rows = sortProperties(differences, Difference::newProperty).stream() + .filter(this::isDeprecatedInRelease) + .collect(Collectors.toList()); + writeTable("| Key | Replacement | Reason", rows, this::writeDeprecated); + } + + private void writeDeprecated(Difference difference) { + writeDeprecatedPropertyRow(difference.newProperty()); + } + + private void writeAdded(List differences) { + List rows = sortProperties(differences, Difference::newProperty); + writeTable("| Key | Default value | Description", rows, this::writeAdded); + } + + private void writeAdded(Difference difference) { + writeRegularPropertyRow(difference.newProperty()); + } + + private void writeRemoved(List deleted, List deprecated) { + List rows = getRemoved(deleted, deprecated); + writeTable("| Key | Replacement | Reason", rows, this::writeRemoved); + } + + private List getRemoved(List deleted, List deprecated) { + List result = new ArrayList<>(deleted); + deprecated.stream().filter(Predicate.not(this::isDeprecatedInRelease)).forEach(result::remove); + return sortProperties(result, + (difference) -> getFirstNonNull(difference, Difference::oldProperty, Difference::newProperty)); + } + + private void writeRemoved(Difference difference) { + writeDeprecatedPropertyRow(getFirstNonNull(difference, Difference::newProperty, Difference::oldProperty)); + } + + private List sortProperties(List differences, + Function extractor) { + return differences.stream().sorted(Comparator.comparing(extractor, COMPARING_ID)).toList(); + } + + @SafeVarargs + @SuppressWarnings("varargs") + private P getFirstNonNull(T t, Function... extractors) { + return Stream.of(extractors) + .map((extractor) -> extractor.apply(t)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + private void writeTable(String header, List rows, Consumer action) { + if (rows.isEmpty()) { + write("_None_.%n"); + } + else { + writeTableBreak(); + write(header + "%n%n"); + for (Iterator iterator = rows.iterator(); iterator.hasNext();) { + action.accept(iterator.next()); + write((!iterator.hasNext()) ? null : "%n"); + } + writeTableBreak(); + } + } + + private void writeTableBreak() { + write("|======================%n"); + } + + private void writeRegularPropertyRow(ConfigurationMetadataProperty property) { + writeCell(monospace(property.getId())); + writeCell(monospace(asString(property.getDefaultValue()))); + writeCell(property.getShortDescription()); + } + + private void writeDeprecatedPropertyRow(ConfigurationMetadataProperty property) { + Deprecation deprecation = (property.getDeprecation() != null) ? property.getDeprecation() : new Deprecation(); + writeCell(monospace(property.getId())); + writeCell(monospace(deprecation.getReplacement())); + writeCell(getFirstSentence(deprecation.getReason())); + } + + private String getFirstSentence(String text) { + if (text == null) { + return null; + } + int dot = text.indexOf('.'); + if (dot != -1) { + BreakIterator breakIterator = BreakIterator.getSentenceInstance(Locale.US); + breakIterator.setText(text); + String sentence = text.substring(breakIterator.first(), breakIterator.next()).trim(); + return removeSpaceBetweenLine(sentence); + } + String[] lines = text.split(System.lineSeparator()); + return lines[0].trim(); + } + + private String removeSpaceBetweenLine(String text) { + String[] lines = text.split(System.lineSeparator()); + return Arrays.stream(lines).map(String::trim).collect(Collectors.joining(" ")); + } + + private boolean isDeprecatedInRelease(Difference difference) { + Deprecation deprecation = difference.newProperty().getDeprecation(); + return (deprecation != null) && (deprecation.getLevel() != Deprecation.Level.ERROR); + } + + private String monospace(String value) { + return (value != null) ? "`%s`".formatted(value) : null; + } + + private void writeCell(String format, Object... args) { + write((format != null) ? "| %s%n".formatted(format) : "|%n", args); + } + + private void write(String format, Object... args) { + if (format != null) { + Object[] strings = Arrays.stream(args).map(this::asString).toArray(); + this.out.append(format.formatted(strings)); + } + } + + private String asString(Object value) { + if (value instanceof Object[] array) { + return Stream.of(array).map(this::asString).collect(Collectors.joining(", ")); + } + return (value != null) ? value.toString() : null; + } + + @Override + public void close() { + this.out.close(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataChangelogGenerator.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataChangelogGenerator.java deleted file mode 100644 index 2eb5b1244ec0..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataChangelogGenerator.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2012-2023 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.configurationmetadata.changelog; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; - -/** - * Generates a configuration metadata changelog. Requires three arguments: - * - *
    - *
  1. The path of a directory containing jar files from which the old metadata will be - * extracted - *
  2. The path of a directory containing jar files from which the new metadata will be - * extracted - *
  3. The path of a file to which the changelog will be written - *
- * - * The name of each directory will be used to name the old and new metadata in the - * generated changelog - * - * @author Andy Wilkinson - */ -final class ConfigurationMetadataChangelogGenerator { - - private ConfigurationMetadataChangelogGenerator() { - - } - - public static void main(String[] args) throws IOException { - ConfigurationMetadataDiff diff = ConfigurationMetadataDiff.of( - NamedConfigurationMetadataRepository.from(new File(args[0])), - NamedConfigurationMetadataRepository.from(new File(args[1]))); - try (ConfigurationMetadataChangelogWriter writer = new ConfigurationMetadataChangelogWriter( - new FileWriter(new File(args[2])))) { - writer.write(diff); - } - System.out.println("\nConfiguration metadata changelog written to '" + args[2] + "'"); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataChangelogWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataChangelogWriter.java deleted file mode 100644 index b08b1667d34e..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataChangelogWriter.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright 2012-2023 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.configurationmetadata.changelog; - -import java.io.PrintWriter; -import java.io.Writer; -import java.text.BreakIterator; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; -import org.springframework.boot.configurationmetadata.Deprecation; -import org.springframework.boot.configurationmetadata.changelog.ConfigurationMetadataDiff.Difference; -import org.springframework.boot.configurationmetadata.changelog.ConfigurationMetadataDiff.Difference.Type; - -/** - * Writes a configuration metadata changelog from a {@link ConfigurationMetadataDiff}. - * - * @author Stephane Nicoll - * @author Andy Wilkinson - */ -class ConfigurationMetadataChangelogWriter implements AutoCloseable { - - private final PrintWriter out; - - ConfigurationMetadataChangelogWriter(Writer out) { - this.out = new PrintWriter(out); - } - - void write(ConfigurationMetadataDiff diff) { - this.out.append(String.format("Configuration property changes between `%s` and " + "`%s`%n", diff.leftName(), - diff.rightName())); - this.out.append(System.lineSeparator()); - this.out.append(String.format("== Deprecated in `%s`%n", diff.rightName())); - Map> differencesByType = differencesByType(diff); - writeDeprecatedProperties(differencesByType.get(Type.DEPRECATED)); - this.out.append(System.lineSeparator()); - this.out.append(String.format("== New in `%s`%n", diff.rightName())); - writeAddedProperties(differencesByType.get(Type.ADDED)); - this.out.append(System.lineSeparator()); - this.out.append(String.format("== Removed in `%s`%n", diff.rightName())); - writeRemovedProperties(differencesByType.get(Type.DELETED), differencesByType.get(Type.DEPRECATED)); - } - - private Map> differencesByType(ConfigurationMetadataDiff diff) { - Map> differencesByType = new HashMap<>(); - for (Type type : Type.values()) { - differencesByType.put(type, new ArrayList<>()); - } - for (Difference difference : diff.differences()) { - differencesByType.get(difference.type()).add(difference); - } - return differencesByType; - } - - private void writeDeprecatedProperties(List differences) { - if (differences.isEmpty()) { - this.out.append(String.format("None.%n")); - } - else { - List properties = sortProperties(differences, Difference::right).stream() - .filter(this::isDeprecatedInRelease) - .collect(Collectors.toList()); - this.out.append(String.format("|======================%n")); - this.out.append(String.format("|Key |Replacement |Reason%n")); - properties.forEach((diff) -> { - ConfigurationMetadataProperty property = diff.right(); - writeDeprecatedProperty(property); - }); - this.out.append(String.format("|======================%n")); - } - this.out.append(String.format("%n%n")); - } - - private boolean isDeprecatedInRelease(Difference difference) { - return difference.right().getDeprecation() != null - && Deprecation.Level.ERROR != difference.right().getDeprecation().getLevel(); - } - - private void writeAddedProperties(List differences) { - if (differences.isEmpty()) { - this.out.append(String.format("None.%n")); - } - else { - List properties = sortProperties(differences, Difference::right); - this.out.append(String.format("|======================%n")); - this.out.append(String.format("|Key |Default value |Description%n")); - properties.forEach((diff) -> writeRegularProperty(diff.right())); - this.out.append(String.format("|======================%n")); - } - this.out.append(String.format("%n%n")); - } - - private void writeRemovedProperties(List deleted, List deprecated) { - List removed = getRemovedProperties(deleted, deprecated); - if (removed.isEmpty()) { - this.out.append(String.format("None.%n")); - } - else { - this.out.append(String.format("|======================%n")); - this.out.append(String.format("|Key |Replacement |Reason%n")); - removed.forEach((property) -> writeDeprecatedProperty( - (property.right() != null) ? property.right() : property.left())); - this.out.append(String.format("|======================%n")); - } - } - - private List getRemovedProperties(List deleted, List deprecated) { - List properties = new ArrayList<>(deleted); - properties.addAll(deprecated.stream().filter((p) -> !isDeprecatedInRelease(p)).collect(Collectors.toList())); - return sortProperties(properties, - (difference) -> (difference.left() != null) ? difference.left() : difference.right()); - } - - private void writeRegularProperty(ConfigurationMetadataProperty property) { - this.out.append("|`").append(property.getId()).append("` |"); - if (property.getDefaultValue() != null) { - this.out.append("`").append(defaultValueToString(property.getDefaultValue())).append("`"); - } - this.out.append(" |"); - if (property.getDescription() != null) { - this.out.append(property.getShortDescription()); - } - this.out.append(System.lineSeparator()); - } - - private void writeDeprecatedProperty(ConfigurationMetadataProperty property) { - Deprecation deprecation = (property.getDeprecation() != null) ? property.getDeprecation() : new Deprecation(); - this.out.append("|`").append(property.getId()).append("` |"); - if (deprecation.getReplacement() != null) { - this.out.append("`").append(deprecation.getReplacement()).append("`"); - } - this.out.append(" |"); - if (deprecation.getReason() != null) { - this.out.append(getFirstSentence(deprecation.getReason())); - } - this.out.append(System.lineSeparator()); - } - - private String getFirstSentence(String text) { - int dot = text.indexOf('.'); - if (dot != -1) { - BreakIterator breakIterator = BreakIterator.getSentenceInstance(Locale.US); - breakIterator.setText(text); - String sentence = text.substring(breakIterator.first(), breakIterator.next()).trim(); - return removeSpaceBetweenLine(sentence); - } - else { - String[] lines = text.split(System.lineSeparator()); - return lines[0].trim(); - } - } - - private static String removeSpaceBetweenLine(String text) { - String[] lines = text.split(System.lineSeparator()); - StringBuilder sb = new StringBuilder(); - for (String line : lines) { - sb.append(line.trim()).append(" "); - } - return sb.toString().trim(); - } - - private List sortProperties(List properties, - Function property) { - List sorted = new ArrayList<>(properties); - sorted.sort((o1, o2) -> property.apply(o1).getId().compareTo(property.apply(o2).getId())); - return sorted; - } - - private static String defaultValueToString(Object defaultValue) { - if (defaultValue instanceof Object[]) { - return Stream.of((Object[]) defaultValue).map(Object::toString).collect(Collectors.joining(", ")); - } - else { - return defaultValue.toString(); - } - } - - @Override - public void close() { - this.out.close(); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataDiff.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataDiff.java deleted file mode 100644 index 260c7f95ea4b..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataDiff.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2012-2023 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.configurationmetadata.changelog; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; -import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepository; -import org.springframework.boot.configurationmetadata.Deprecation.Level; -import org.springframework.boot.configurationmetadata.changelog.ConfigurationMetadataDiff.Difference.Type; - -/** - * A diff of two repositories of configuration metadata. - * - * @param leftName the name of the left-hand side of the diff - * @param rightName the name of the right-hand side of the diff - * @param differences the differences - * @author Stephane Nicoll - * @author Andy Wilkinson - */ -record ConfigurationMetadataDiff(String leftName, String rightName, List differences) { - - static ConfigurationMetadataDiff of(NamedConfigurationMetadataRepository left, - NamedConfigurationMetadataRepository right) { - return new ConfigurationMetadataDiff(left.getName(), right.getName(), differences(left, right)); - } - - private static List differences(ConfigurationMetadataRepository left, - ConfigurationMetadataRepository right) { - List differences = new ArrayList<>(); - List matches = new ArrayList<>(); - Map leftProperties = left.getAllProperties(); - Map rightProperties = right.getAllProperties(); - for (ConfigurationMetadataProperty leftProperty : leftProperties.values()) { - String id = leftProperty.getId(); - matches.add(id); - ConfigurationMetadataProperty rightProperty = rightProperties.get(id); - if (rightProperty == null) { - if (!(leftProperty.isDeprecated() && leftProperty.getDeprecation().getLevel() == Level.ERROR)) { - differences.add(new Difference(Type.DELETED, leftProperty, null)); - } - } - else if (rightProperty.isDeprecated() && !leftProperty.isDeprecated()) { - differences.add(new Difference(Type.DEPRECATED, leftProperty, rightProperty)); - } - else if (leftProperty.isDeprecated() && leftProperty.getDeprecation().getLevel() == Level.WARNING - && rightProperty.isDeprecated() && rightProperty.getDeprecation().getLevel() == Level.ERROR) { - differences.add(new Difference(Type.DELETED, leftProperty, rightProperty)); - } - } - for (ConfigurationMetadataProperty rightProperty : rightProperties.values()) { - if ((!matches.contains(rightProperty.getId())) && (!rightProperty.isDeprecated())) { - differences.add(new Difference(Type.ADDED, null, rightProperty)); - } - } - return differences; - } - - /** - * A difference in the metadata. - * - * @param type the type of the difference - * @param left the left-hand side of the difference - * @param right the right-hand side of the difference - */ - static record Difference(Type type, ConfigurationMetadataProperty left, ConfigurationMetadataProperty right) { - - /** - * The type of a difference in the metadata. - */ - enum Type { - - /** - * The entry has been added. - */ - ADDED, - - /** - * The entry has been made deprecated. It may or may not still exist in the - * previous version. - */ - DEPRECATED, - - /** - * The entry has been deleted. - */ - DELETED - - } - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Difference.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Difference.java new file mode 100644 index 000000000000..8d0fb66cfa7e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Difference.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2023 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.configurationmetadata.changelog; + +import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; +import org.springframework.boot.configurationmetadata.Deprecation.Level; + +/** + * A difference the metadata. + * + * @param type the type of the difference + * @param oldProperty the old property + * @param newProperty the new property + * @author Stephane Nicoll + * @author Andy Wilkinson + * @author Phillip Webb + */ +record Difference(DifferenceType type, ConfigurationMetadataProperty oldProperty, + ConfigurationMetadataProperty newProperty) { + + static Difference compute(ConfigurationMetadataProperty oldProperty, ConfigurationMetadataProperty newProperty) { + if (newProperty == null) { + if (!(oldProperty.isDeprecated() && oldProperty.getDeprecation().getLevel() == Level.ERROR)) { + return new Difference(DifferenceType.DELETED, oldProperty, null); + } + return null; + } + if (newProperty.isDeprecated() && !oldProperty.isDeprecated()) { + return new Difference(DifferenceType.DEPRECATED, oldProperty, newProperty); + } + if (oldProperty.isDeprecated() && oldProperty.getDeprecation().getLevel() == Level.WARNING + && newProperty.isDeprecated() && newProperty.getDeprecation().getLevel() == Level.ERROR) { + return new Difference(DifferenceType.DELETED, oldProperty, newProperty); + } + return null; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/DifferenceType.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/DifferenceType.java new file mode 100644 index 000000000000..b673310b4072 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/DifferenceType.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2023 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.configurationmetadata.changelog; + +/** + * The type of a difference in the metadata. + * + * @author Andy Wilkinson + */ +enum DifferenceType { + + /** + * The entry has been added. + */ + ADDED, + + /** + * The entry has been made deprecated. It may or may not still exist in the previous + * version. + */ + DEPRECATED, + + /** + * The entry has been deleted. + */ + DELETED + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/NamedConfigurationMetadataRepository.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/NamedConfigurationMetadataRepository.java deleted file mode 100644 index 51ec5535e33e..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/NamedConfigurationMetadataRepository.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2012-2023 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.configurationmetadata.changelog; - -import java.io.File; -import java.io.IOException; -import java.util.Map; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; - -import org.springframework.boot.configurationmetadata.ConfigurationMetadataGroup; -import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; -import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepository; -import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepositoryJsonBuilder; - -/** - * A {@link ConfigurationMetadataRepository} with a name. - * - * @author Andy Wilkinson - */ -class NamedConfigurationMetadataRepository implements ConfigurationMetadataRepository { - - private final String name; - - private final ConfigurationMetadataRepository delegate; - - NamedConfigurationMetadataRepository(String name, ConfigurationMetadataRepository delegate) { - this.name = name; - this.delegate = delegate; - } - - /** - * The name of the metadata held in the repository. - * @return the name of the metadata - */ - String getName() { - return this.name; - } - - @Override - public Map getAllGroups() { - return this.delegate.getAllGroups(); - } - - @Override - public Map getAllProperties() { - return this.delegate.getAllProperties(); - } - - static NamedConfigurationMetadataRepository from(File metadataDir) { - ConfigurationMetadataRepositoryJsonBuilder builder = ConfigurationMetadataRepositoryJsonBuilder.create(); - for (File jar : metadataDir.listFiles()) { - try (JarFile jarFile = new JarFile(jar)) { - JarEntry jsonMetadata = jarFile.getJarEntry("META-INF/spring-configuration-metadata.json"); - if (jsonMetadata != null) { - builder.withJsonResource(jarFile.getInputStream(jsonMetadata)); - } - } - catch (IOException ex) { - throw new RuntimeException(ex); - } - } - return new NamedConfigurationMetadataRepository(metadataDir.getName(), builder.build()); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogGeneratorTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogGeneratorTests.java new file mode 100644 index 000000000000..efa1760c2736 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogGeneratorTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2023 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.configurationmetadata.changelog; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipEntry; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ChangelogGenerator}. + * + * @author Phillip Webb + */ +class ChangelogGeneratorTests { + + @TempDir + File temp; + + @Test + void generateChangeLog() throws IOException { + File oldJars = new File(this.temp, "1.0"); + addJar(oldJars, "sample-1.0.json"); + File newJars = new File(this.temp, "2.0"); + addJar(newJars, "sample-2.0.json"); + File out = new File(this.temp, "changes.adoc"); + String[] args = new String[] { oldJars.getAbsolutePath(), newJars.getAbsolutePath(), out.getAbsolutePath() }; + ChangelogGenerator.main(args); + assertThat(out).usingCharset(StandardCharsets.UTF_8) + .hasSameTextualContentAs(new File("src/test/resources/sample.adoc")); + } + + private void addJar(File directory, String filename) throws IOException { + directory.mkdirs(); + try (JarOutputStream out = new JarOutputStream(new FileOutputStream(new File(directory, "sample.jar")))) { + out.putNextEntry(new ZipEntry("META-INF/spring-configuration-metadata.json")); + try (InputStream in = new FileInputStream("src/test/resources/" + filename)) { + in.transferTo(out); + out.closeEntry(); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogTests.java new file mode 100644 index 000000000000..8e8516e5220c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2023 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.configurationmetadata.changelog; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Changelog}. + * + * @author Stephane Nicoll + * @author Andy Wilkinson + */ +class ChangelogTests { + + @Test + void diffContainsDifferencesBetweenLeftAndRightInputs() { + Changelog differences = TestChangelog.load(); + assertThat(differences).isNotNull(); + assertThat(differences.oldVersionNumber()).isEqualTo("1.0"); + assertThat(differences.newVersionNumber()).isEqualTo("2.0"); + assertThat(differences.differences()).hasSize(4); + List added = differences.differences() + .stream() + .filter((difference) -> difference.type() == DifferenceType.ADDED) + .collect(Collectors.toList()); + assertThat(added).hasSize(1); + assertProperty(added.get(0).newProperty(), "test.add", String.class, "new"); + List deleted = differences.differences() + .stream() + .filter((difference) -> difference.type() == DifferenceType.DELETED) + .collect(Collectors.toList()); + assertThat(deleted).hasSize(2) + .anySatisfy((entry) -> assertProperty(entry.oldProperty(), "test.delete", String.class, "delete")) + .anySatisfy( + (entry) -> assertProperty(entry.newProperty(), "test.delete.deprecated", String.class, "delete")); + List deprecated = differences.differences() + .stream() + .filter((difference) -> difference.type() == DifferenceType.DEPRECATED) + .collect(Collectors.toList()); + assertThat(deprecated).hasSize(1); + assertProperty(deprecated.get(0).oldProperty(), "test.deprecate", String.class, "wrong"); + assertProperty(deprecated.get(0).newProperty(), "test.deprecate", String.class, "wrong"); + } + + private void assertProperty(ConfigurationMetadataProperty property, String id, Class type, Object defaultValue) { + assertThat(property).isNotNull(); + assertThat(property.getId()).isEqualTo(id); + assertThat(property.getType()).isEqualTo(type.getName()); + assertThat(property.getDefaultValue()).isEqualTo(defaultValue); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriterTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriterTests.java new file mode 100644 index 000000000000..5e72e3b567d0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriterTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2023 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.configurationmetadata.changelog; + +import java.io.File; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; + +import org.assertj.core.util.Files; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ChangelogWriter}. + * + * @author Phillip Webb + */ +class ChangelogWriterTests { + + @Test + void writeChangelog() { + StringWriter out = new StringWriter(); + try (ChangelogWriter writer = new ChangelogWriter(out)) { + writer.write(TestChangelog.load()); + } + String expected = Files.contentOf(new File("src/test/resources/sample.adoc"), StandardCharsets.UTF_8); + assertThat(out).hasToString(expected); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataDiffTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataDiffTests.java deleted file mode 100644 index 787184a93f43..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ConfigurationMetadataDiffTests.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2012-2023 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.configurationmetadata.changelog; - -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; -import java.util.stream.Collectors; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; -import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepository; -import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepositoryJsonBuilder; -import org.springframework.boot.configurationmetadata.changelog.ConfigurationMetadataDiff.Difference; -import org.springframework.boot.configurationmetadata.changelog.ConfigurationMetadataDiff.Difference.Type; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link ConfigurationMetadataDiff}. - * - * @author Stephane Nicoll - * @author Andy Wilkinson - */ -class ConfigurationMetadataDiffTests { - - @Test - void diffContainsDifferencesBetweenLeftAndRightInputs() { - NamedConfigurationMetadataRepository left = new NamedConfigurationMetadataRepository("1.0", - load("sample-1.0.json")); - NamedConfigurationMetadataRepository right = new NamedConfigurationMetadataRepository("2.0", - load("sample-2.0.json")); - ConfigurationMetadataDiff diff = ConfigurationMetadataDiff.of(left, right); - assertThat(diff).isNotNull(); - assertThat(diff.leftName()).isEqualTo("1.0"); - assertThat(diff.rightName()).isEqualTo("2.0"); - assertThat(diff.differences()).hasSize(4); - List added = diff.differences() - .stream() - .filter((difference) -> difference.type() == Type.ADDED) - .collect(Collectors.toList()); - assertThat(added).hasSize(1); - assertProperty(added.get(0).right(), "test.add", String.class, "new"); - List deleted = diff.differences() - .stream() - .filter((difference) -> difference.type() == Type.DELETED) - .collect(Collectors.toList()); - assertThat(deleted).hasSize(2) - .anySatisfy((entry) -> assertProperty(entry.left(), "test.delete", String.class, "delete")) - .anySatisfy((entry) -> assertProperty(entry.right(), "test.delete.deprecated", String.class, "delete")); - List deprecated = diff.differences() - .stream() - .filter((difference) -> difference.type() == Type.DEPRECATED) - .collect(Collectors.toList()); - assertThat(deprecated).hasSize(1); - assertProperty(deprecated.get(0).left(), "test.deprecate", String.class, "wrong"); - assertProperty(deprecated.get(0).right(), "test.deprecate", String.class, "wrong"); - } - - private void assertProperty(ConfigurationMetadataProperty property, String id, Class type, Object defaultValue) { - assertThat(property).isNotNull(); - assertThat(property.getId()).isEqualTo(id); - assertThat(property.getType()).isEqualTo(type.getName()); - assertThat(property.getDefaultValue()).isEqualTo(defaultValue); - } - - private ConfigurationMetadataRepository load(String filename) { - try (InputStream inputStream = new FileInputStream("src/test/resources/" + filename)) { - return ConfigurationMetadataRepositoryJsonBuilder.create(inputStream).build(); - } - catch (IOException ex) { - throw new RuntimeException(ex); - } - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/TestChangelog.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/TestChangelog.java new file mode 100644 index 000000000000..58a1c34642b7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/TestChangelog.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2023 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.configurationmetadata.changelog; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepository; +import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepositoryJsonBuilder; + +/** + * Factory to create test {@link Changelog} instance. + * + * @author Andy Wilkinson + * @author Phillip Webb + */ +final class TestChangelog { + + private TestChangelog() { + } + + static Changelog load() { + ConfigurationMetadataRepository previousRepository = load("sample-1.0.json"); + ConfigurationMetadataRepository repository = load("sample-2.0.json"); + return Changelog.of("1.0", previousRepository, "2.0", repository); + } + + private static ConfigurationMetadataRepository load(String filename) { + try (InputStream inputStream = new FileInputStream("src/test/resources/" + filename)) { + return ConfigurationMetadataRepositoryJsonBuilder.create(inputStream).build(); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-2.0.json b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-2.0.json index 2de71ca99e57..ef959d39c9eb 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-2.0.json +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-2.0.json @@ -27,7 +27,9 @@ "description": "Test delete deprecated.", "defaultValue": "delete", "deprecation": { - "level": "error" + "level": "error", + "replacement": "test.add", + "reason": "it was just bad" } } ] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample.adoc b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample.adoc new file mode 100644 index 000000000000..ac5cc843e16f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample.adoc @@ -0,0 +1,32 @@ +Configuration property changes between `1.0` and `2.0` + + + +== Deprecated in 2.0 +_None_. + + + +== Added in 2.0 +|====================== +| Key | Default value | Description + +| `test.add` +| `new` +| Test add. +|====================== + + + +== Removed in 2.0 +|====================== +| Key | Replacement | Reason + +| `test.delete` +| +| + +| `test.delete.deprecated` +| `test.add` +| it was just bad +|====================== From 7c77e1bb85b94d15b6e33477711a4f2e1ba14cf1 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Sun, 2 Jul 2023 19:07:40 +0900 Subject: [PATCH 0078/1215] Polish 'Log correlation IDs when Micrometer tracing is being used' See gh-36158 --- ...ogCorrelationEnvironmentPostProcessor.java | 2 +- ...relationEnvironmentPostProcessorTests.java | 4 ++-- .../boot/logging/CorrelationIdFormatter.java | 21 ++++++++++--------- .../logging/log4j2/Log4J2LoggingSystem.java | 3 +-- .../logging/CorrelationIdFormatterTests.java | 2 +- .../logback/LogbackLoggingSystemTests.java | 2 +- .../build.gradle | 2 +- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessor.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessor.java index d7e86849d05e..50a92939f4f5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessor.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessor.java @@ -26,7 +26,7 @@ /** * {@link EnvironmentPostProcessor} to add a {@link PropertySource} to support log - * correlation IDs when Micrometer is present. Adds support for the + * correlation IDs when Micrometer Tracing is present. Adds support for the * {@value LoggingSystem#EXPECT_CORRELATION_ID_PROPERTY} property by delegating to * {@code management.tracing.enabled}. * diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessorTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessorTests.java index a75a91a28bd5..4bbfa4a0e8d9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessorTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessorTests.java @@ -42,7 +42,7 @@ class LogCorrelationEnvironmentPostProcessorTests { private final LogCorrelationEnvironmentPostProcessor postProcessor = new LogCorrelationEnvironmentPostProcessor(); @Test - void getExpectCorrelationIdPropertyWhenMicrometerPresentReturnsTrue() { + void getExpectCorrelationIdPropertyWhenMicrometerTracingPresentReturnsTrue() { this.postProcessor.postProcessEnvironment(this.environment, this.application); assertThat(this.environment.getProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, Boolean.class, false)) .isTrue(); @@ -50,7 +50,7 @@ void getExpectCorrelationIdPropertyWhenMicrometerPresentReturnsTrue() { @Test @ClassPathExclusions("micrometer-tracing-*.jar") - void getExpectCorrelationIdPropertyWhenMicrometerMissingReturnsFalse() { + void getExpectCorrelationIdPropertyWhenMicrometerTracingMissingReturnsFalse() { this.postProcessor.postProcessEnvironment(this.environment, this.application); assertThat(this.environment.getProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, Boolean.class, false)) .isFalse(); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/CorrelationIdFormatter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/CorrelationIdFormatter.java index 5dcab9f4d503..1388aecca0b6 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/CorrelationIdFormatter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/CorrelationIdFormatter.java @@ -18,7 +18,6 @@ import java.io.IOException; import java.io.UncheckedIOException; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; @@ -36,7 +35,7 @@ /** * Utility class that can be used to format a correlation identifier for logging based on * w3c + * "https://www.w3.org/TR/trace-context/#examples-of-http-traceparent-headers">W3C * recommendations. *

* The formatter can be configured with a comma-separated list of names and the expected @@ -46,8 +45,8 @@ * {@code 16} respectively. *

* Correlation IDs are formatted as dash separated strings surrounded in square brackets. - * Formatted output is always of a fixed width and with trailing whitespace. Dashes are - * omitted if none of the named items can be resolved. + * Formatted output is always of a fixed width and with trailing space. Dashes are omitted + * if none of the named items can be resolved. *

* The following example would return a formatted result of * {@code "[01234567890123456789012345678901-0123456789012345] "}:

@@ -101,10 +100,12 @@ public void formatTo(Function resolver, Appendable appendable) {
 		Predicate canResolve = (part) -> StringUtils.hasLength(resolver.apply(part.name()));
 		try {
 			if (this.parts.stream().anyMatch(canResolve)) {
-				appendable.append("[");
+				appendable.append('[');
 				for (Iterator iterator = this.parts.iterator(); iterator.hasNext();) {
 					appendable.append(iterator.next().resolve(resolver));
-					appendable.append((!iterator.hasNext()) ? "" : "-");
+					if (iterator.hasNext()) {
+						appendable.append('-');
+					}
 				}
 				appendable.append("] ");
 			}
@@ -124,7 +125,7 @@ public String toString() {
 
 	/**
 	 * Create a new {@link CorrelationIdFormatter} instance from the given specification.
-	 * @param spec a comma separated specification
+	 * @param spec a comma-separated specification
 	 * @return a new {@link CorrelationIdFormatter} instance
 	 */
 	public static CorrelationIdFormatter of(String spec) {
@@ -142,7 +143,7 @@ public static CorrelationIdFormatter of(String spec) {
 	 * @return a new {@link CorrelationIdFormatter} instance
 	 */
 	public static CorrelationIdFormatter of(String[] spec) {
-		return of((spec != null) ? Arrays.asList(spec) : Collections.emptyList());
+		return of((spec != null) ? List.of(spec) : Collections.emptyList());
 	}
 
 	/**
@@ -166,7 +167,7 @@ public static CorrelationIdFormatter of(Collection spec) {
 	 */
 	record Part(String name, int length) {
 
-		private static final Pattern pattern = Pattern.compile("^(.+?)\\((\\d+)\\)?$");
+		private static final Pattern pattern = Pattern.compile("^(.+?)\\((\\d+)\\)$");
 
 		String resolve(Function resolver) {
 			String resolved = resolver.apply(name());
@@ -174,7 +175,7 @@ String resolve(Function resolver) {
 				return blank();
 			}
 			int padding = length() - resolved.length();
-			return resolved + " ".repeat((padding > 0) ? padding : 0);
+			return (padding <= 0) ? resolved : resolved + " ".repeat(padding);
 		}
 
 		String blank() {
diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java
index da996bfd5c97..10152f88c398 100644
--- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java
+++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java
@@ -249,8 +249,7 @@ public void initialize(LoggingInitializationContext initializationContext, Strin
 
 	@Override
 	protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) {
-		String location = (logFile != null) ? getPackagedConfigFile("log4j2-file.xml")
-				: getPackagedConfigFile("log4j2.xml");
+		String location = getPackagedConfigFile((logFile != null) ? "log4j2-file.xml" : "log4j2.xml");
 		load(initializationContext, location, logFile);
 	}
 
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/CorrelationIdFormatterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/CorrelationIdFormatterTests.java
index b34771352a47..2e36d0f00fc0 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/CorrelationIdFormatterTests.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/CorrelationIdFormatterTests.java
@@ -96,7 +96,7 @@ void formatToWithDefaultSpec() {
 		context.put("traceId", "01234567890123456789012345678901");
 		context.put("spanId", "0123456789012345");
 		StringBuilder formatted = new StringBuilder();
-		CorrelationIdFormatter.of("").formatTo(context::get, formatted);
+		CorrelationIdFormatter.DEFAULT.formatTo(context::get, formatted);
 		assertThat(formatted).hasToString("[01234567890123456789012345678901-0123456789012345] ");
 	}
 
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java
index 685923ead60c..1baa88001219 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java
@@ -743,7 +743,7 @@ void correlationLoggingToConsoleWhenUsingXmlConfiguration(CapturedOutput output)
 	}
 
 	@Test
-	void correlationLoggingToConsoleWhenUsingFileConfiguration() {
+	void correlationLoggingToFileWhenUsingFileConfiguration() {
 		this.environment.setProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, "true");
 		File file = new File(tmpDir(), "logback-test.log");
 		LogFile logFile = getLogFile(file.getPath(), null);
diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/build.gradle
index 656ad05a752d..c5157df03e02 100644
--- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/build.gradle
+++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-actuator/build.gradle
@@ -11,7 +11,7 @@ dependencies {
 	implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-security"))
 	implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web"))
 	implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-validation"))
-	implementation 'io.micrometer:micrometer-tracing-bridge-brave'
+	implementation("io.micrometer:micrometer-tracing-bridge-brave")
 
 	runtimeOnly("com.h2database:h2")
 

From df107890c7eb6ddbb1ddb24e5b88c8ee95949c7b Mon Sep 17 00:00:00 2001
From: Johnny Lim 
Date: Sun, 2 Jul 2023 19:06:03 +0900
Subject: [PATCH 0079/1215] Fix metadata for logging.include-application-name

See gh-36157
---
 .../META-INF/additional-spring-configuration-metadata.json      | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json
index a22819a934c5..d4bd08721b86 100644
--- a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json
+++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -105,7 +105,7 @@
     },
     {
       "name": "logging.include-application-name",
-      "type": "java.lang.String>",
+      "type": "java.lang.Boolean",
       "description": "Whether to include the application name in the logs.",
       "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener",
       "defaultValue": true

From dc1d458e64aa9ff0fdb8036b2c6561fe280dc994 Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Mon, 3 Jul 2023 19:26:40 +0100
Subject: [PATCH 0080/1215] Start building against Micrometer 1.12.0 snapshots

See gh-36188
---
 spring-boot-project/spring-boot-dependencies/build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle
index dac95aae50e5..8f2f7f57e1f1 100644
--- a/spring-boot-project/spring-boot-dependencies/build.gradle
+++ b/spring-boot-project/spring-boot-dependencies/build.gradle
@@ -972,7 +972,7 @@ bom {
 			]
 		}
 	}
-	library("Micrometer", "1.11.1") {
+	library("Micrometer", "1.12.0-SNAPSHOT") {
 		group("io.micrometer") {
 			modules = [
 				"micrometer-registry-stackdriver" {

From e847e662c2c215df479d2d3998212fd4f7463e24 Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Mon, 3 Jul 2023 19:26:45 +0100
Subject: [PATCH 0081/1215] Start building against Spring Batch 5.1.0 snapshots

See gh-36189
---
 spring-boot-project/spring-boot-dependencies/build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle
index 8f2f7f57e1f1..d061f7816e9a 100644
--- a/spring-boot-project/spring-boot-dependencies/build.gradle
+++ b/spring-boot-project/spring-boot-dependencies/build.gradle
@@ -1386,7 +1386,7 @@ bom {
 			]
 		}
 	}
-	library("Spring Batch", "5.0.2") {
+	library("Spring Batch", "5.1.0-SNAPSHOT") {
 		group("org.springframework.batch") {
 			imports = [
 				"spring-batch-bom"

From ec8e1e2c950632e65d38a50f0e81e4500b301450 Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Mon, 3 Jul 2023 19:26:50 +0100
Subject: [PATCH 0082/1215] Start building against Spring Data Bom 2023.1.0
 snapshots

See gh-36190
---
 .../cache/CacheAutoConfigurationTests.java    | 21 ++++++++++++---
 .../MongoDataAutoConfigurationTests.java      | 27 ++++++++++++++-----
 .../spring-boot-dependencies/build.gradle     |  2 +-
 .../connectingusingspringdata/MyBean.kt       |  5 ++--
 4 files changed, 41 insertions(+), 14 deletions(-)

diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfigurationTests.java
index 2c887c890bd2..9a84e99eb3d3 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfigurationTests.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfigurationTests.java
@@ -69,6 +69,7 @@
 import org.springframework.data.couchbase.cache.CouchbaseCache;
 import org.springframework.data.couchbase.cache.CouchbaseCacheConfiguration;
 import org.springframework.data.couchbase.cache.CouchbaseCacheManager;
+import org.springframework.data.redis.cache.FixedDurationTtlFunction;
 import org.springframework.data.redis.cache.RedisCacheConfiguration;
 import org.springframework.data.redis.cache.RedisCacheManager;
 import org.springframework.data.redis.connection.RedisConnectionFactory;
@@ -273,7 +274,10 @@ void redisCacheExplicit() {
 				RedisCacheManager cacheManager = getCacheManager(context, RedisCacheManager.class);
 				assertThat(cacheManager.getCacheNames()).isEmpty();
 				RedisCacheConfiguration redisCacheConfiguration = getDefaultRedisCacheConfiguration(cacheManager);
-				assertThat(redisCacheConfiguration.getTtl()).isEqualTo(java.time.Duration.ofSeconds(15));
+				assertThat(redisCacheConfiguration).extracting(RedisCacheConfiguration::getTtlFunction)
+					.isInstanceOf(FixedDurationTtlFunction.class)
+					.extracting("duration")
+					.isEqualTo(java.time.Duration.ofSeconds(15));
 				assertThat(redisCacheConfiguration.getAllowCacheNullValues()).isFalse();
 				assertThat(redisCacheConfiguration.getKeyPrefixFor("MyCache")).isEqualTo("prefixMyCache::");
 				assertThat(redisCacheConfiguration.usePrefix()).isTrue();
@@ -289,7 +293,10 @@ void redisCacheWithRedisCacheConfiguration() {
 				RedisCacheManager cacheManager = getCacheManager(context, RedisCacheManager.class);
 				assertThat(cacheManager.getCacheNames()).isEmpty();
 				RedisCacheConfiguration redisCacheConfiguration = getDefaultRedisCacheConfiguration(cacheManager);
-				assertThat(redisCacheConfiguration.getTtl()).isEqualTo(java.time.Duration.ofSeconds(30));
+				assertThat(redisCacheConfiguration).extracting(RedisCacheConfiguration::getTtlFunction)
+					.isInstanceOf(FixedDurationTtlFunction.class)
+					.extracting("duration")
+					.isEqualTo(java.time.Duration.ofSeconds(30));
 				assertThat(redisCacheConfiguration.getKeyPrefixFor("")).isEqualTo("bar::");
 			});
 	}
@@ -301,7 +308,10 @@ void redisCacheWithRedisCacheManagerBuilderCustomizer() {
 			.run((context) -> {
 				RedisCacheManager cacheManager = getCacheManager(context, RedisCacheManager.class);
 				RedisCacheConfiguration redisCacheConfiguration = getDefaultRedisCacheConfiguration(cacheManager);
-				assertThat(redisCacheConfiguration.getTtl()).isEqualTo(java.time.Duration.ofSeconds(10));
+				assertThat(redisCacheConfiguration).extracting(RedisCacheConfiguration::getTtlFunction)
+					.isInstanceOf(FixedDurationTtlFunction.class)
+					.extracting("duration")
+					.isEqualTo(java.time.Duration.ofSeconds(10));
 			});
 	}
 
@@ -321,7 +331,10 @@ void redisCacheExplicitWithCaches() {
 				RedisCacheManager cacheManager = getCacheManager(context, RedisCacheManager.class);
 				assertThat(cacheManager.getCacheNames()).containsOnly("foo", "bar");
 				RedisCacheConfiguration redisCacheConfiguration = getDefaultRedisCacheConfiguration(cacheManager);
-				assertThat(redisCacheConfiguration.getTtl()).isEqualTo(java.time.Duration.ofMinutes(0));
+				assertThat(redisCacheConfiguration).extracting(RedisCacheConfiguration::getTtlFunction)
+					.isInstanceOf(FixedDurationTtlFunction.class)
+					.extracting("duration")
+					.isEqualTo(java.time.Duration.ofSeconds(0));
 				assertThat(redisCacheConfiguration.getAllowCacheNullValues()).isTrue();
 				assertThat(redisCacheConfiguration.getKeyPrefixFor("test")).isEqualTo("test::");
 				assertThat(redisCacheConfiguration.usePrefix()).isTrue();
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoDataAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoDataAutoConfigurationTests.java
index 590fb36bdf1f..caff745e56f1 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoDataAutoConfigurationTests.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoDataAutoConfigurationTests.java
@@ -18,10 +18,14 @@
 
 import java.time.LocalDateTime;
 import java.util.Arrays;
+import java.util.function.Supplier;
 
 import com.mongodb.ConnectionString;
 import com.mongodb.client.MongoClient;
 import com.mongodb.client.MongoClients;
+import com.mongodb.client.MongoCollection;
+import com.mongodb.client.gridfs.GridFSBucket;
+import org.assertj.core.api.InstanceOfAssertFactories;
 import org.junit.jupiter.api.Test;
 
 import org.springframework.beans.factory.BeanCreationException;
@@ -78,32 +82,43 @@ void templateExists() {
 	}
 
 	@Test
+	@SuppressWarnings("unchecked")
 	void whenGridFsDatabaseIsConfiguredThenGridFsTemplateIsAutoConfiguredAndUsesIt() {
 		this.contextRunner.withPropertyValues("spring.data.mongodb.gridfs.database:grid").run((context) -> {
 			assertThat(context).hasSingleBean(GridFsTemplate.class);
 			GridFsTemplate template = context.getBean(GridFsTemplate.class);
-			MongoDatabaseFactory factory = (MongoDatabaseFactory) ReflectionTestUtils.getField(template, "dbFactory");
-			assertThat(factory.getMongoDatabase().getName()).isEqualTo("grid");
+			GridFSBucket bucket = ((Supplier) ReflectionTestUtils.getField(template, "bucketSupplier"))
+				.get();
+			assertThat(bucket).extracting("filesCollection", InstanceOfAssertFactories.type(MongoCollection.class))
+				.extracting((collection) -> collection.getNamespace().getDatabaseName())
+				.isEqualTo("grid");
 		});
 	}
 
 	@Test
+	@SuppressWarnings("unchecked")
 	void usesMongoConnectionDetailsIfAvailable() {
 		this.contextRunner.withUserConfiguration(ConnectionDetailsConfiguration.class).run((context) -> {
 			assertThat(context).hasSingleBean(GridFsTemplate.class);
 			GridFsTemplate template = context.getBean(GridFsTemplate.class);
-			assertThat(template).hasFieldOrPropertyWithValue("bucket", "connection-details-bucket");
-			MongoDatabaseFactory factory = (MongoDatabaseFactory) ReflectionTestUtils.getField(template, "dbFactory");
-			assertThat(factory.getMongoDatabase().getName()).isEqualTo("grid-database-1");
+			GridFSBucket bucket = ((Supplier) ReflectionTestUtils.getField(template, "bucketSupplier"))
+				.get();
+			assertThat(bucket.getBucketName()).isEqualTo("connection-details-bucket");
+			assertThat(bucket).extracting("filesCollection", InstanceOfAssertFactories.type(MongoCollection.class))
+				.extracting((collection) -> collection.getNamespace().getDatabaseName())
+				.isEqualTo("grid-database-1");
 		});
 	}
 
 	@Test
+	@SuppressWarnings("unchecked")
 	void whenGridFsBucketIsConfiguredThenGridFsTemplateIsAutoConfiguredAndUsesIt() {
 		this.contextRunner.withPropertyValues("spring.data.mongodb.gridfs.bucket:test-bucket").run((context) -> {
 			assertThat(context).hasSingleBean(GridFsTemplate.class);
 			GridFsTemplate template = context.getBean(GridFsTemplate.class);
-			assertThat(template).hasFieldOrPropertyWithValue("bucket", "test-bucket");
+			GridFSBucket bucket = ((Supplier) ReflectionTestUtils.getField(template, "bucketSupplier"))
+				.get();
+			assertThat(bucket.getBucketName()).isEqualTo("test-bucket");
 		});
 	}
 
diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle
index d061f7816e9a..154b064b6d8a 100644
--- a/spring-boot-project/spring-boot-dependencies/build.gradle
+++ b/spring-boot-project/spring-boot-dependencies/build.gradle
@@ -1393,7 +1393,7 @@ bom {
 			]
 		}
 	}
-	library("Spring Data Bom", "2023.0.1") {
+	library("Spring Data Bom", "2023.1.0-SNAPSHOT") {
 		group("org.springframework.data") {
 			imports = [
 				"spring-data-bom"
diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/data/nosql/elasticsearch/connectingusingspringdata/MyBean.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/data/nosql/elasticsearch/connectingusingspringdata/MyBean.kt
index 95442da426a2..001ee654ce00 100644
--- a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/data/nosql/elasticsearch/connectingusingspringdata/MyBean.kt
+++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/data/nosql/elasticsearch/connectingusingspringdata/MyBean.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2022 the original author or authors.
+ * Copyright 2012-2023 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.
@@ -19,8 +19,7 @@ package org.springframework.boot.docs.data.nosql.elasticsearch.connectingusingsp
 import org.springframework.stereotype.Component
 
 @Component
-@Suppress("DEPRECATION")
-class MyBean(private val template: org.springframework.data.elasticsearch.client.erhlc.ElasticsearchRestTemplate ) {
+class MyBean(private val template: org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate ) {
 
 	// @fold:on // ...
 	fun someMethod(id: String): Boolean {

From f85ba2a37e260c8c67df3d380207bd612abb6371 Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Mon, 3 Jul 2023 19:26:55 +0100
Subject: [PATCH 0083/1215] Start building against Spring GraphQL 1.2.2
 snapshots

See gh-36191
---
 spring-boot-project/spring-boot-dependencies/build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle
index 154b064b6d8a..1cff8077aa05 100644
--- a/spring-boot-project/spring-boot-dependencies/build.gradle
+++ b/spring-boot-project/spring-boot-dependencies/build.gradle
@@ -1407,7 +1407,7 @@ bom {
 			]
 		}
 	}
-	library("Spring GraphQL", "1.2.1") {
+	library("Spring GraphQL", "1.2.2-SNAPSHOT") {
 		group("org.springframework.graphql") {
 			modules = [
 					"spring-graphql",

From c794f52085230fbb1312aad664f743fce317530c Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Mon, 3 Jul 2023 19:27:00 +0100
Subject: [PATCH 0084/1215] Start building against Spring HATEOAS 2.2.0
 snapshots

See gh-36192
---
 spring-boot-project/spring-boot-dependencies/build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle
index 1cff8077aa05..a43dd7f2a3e9 100644
--- a/spring-boot-project/spring-boot-dependencies/build.gradle
+++ b/spring-boot-project/spring-boot-dependencies/build.gradle
@@ -1415,7 +1415,7 @@ bom {
 			]
 		}
 	}
-	library("Spring HATEOAS", "2.1.0") {
+	library("Spring HATEOAS", "2.2.0-SNAPSHOT") {
 		group("org.springframework.hateoas") {
 			modules = [
 				"spring-hateoas"

From 1e0a572dfa2a33c878a26d600a94215fba82758a Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Mon, 3 Jul 2023 19:27:05 +0100
Subject: [PATCH 0085/1215] Start building against Spring Integration 6.2.0
 snapshots

See gh-36193
---
 spring-boot-project/spring-boot-dependencies/build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle
index a43dd7f2a3e9..8d2a70c7a69e 100644
--- a/spring-boot-project/spring-boot-dependencies/build.gradle
+++ b/spring-boot-project/spring-boot-dependencies/build.gradle
@@ -1422,7 +1422,7 @@ bom {
 			]
 		}
 	}
-	library("Spring Integration", "6.1.1") {
+	library("Spring Integration", "6.2.0-SNAPSHOT") {
 		group("org.springframework.integration") {
 			imports = [
 				"spring-integration-bom"

From 32d83551919a13a4aa104147461d5ab22fb52258 Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Mon, 3 Jul 2023 19:27:10 +0100
Subject: [PATCH 0086/1215] Start building against Spring Kafka 3.0.9 snapshots

See gh-36194
---
 spring-boot-project/spring-boot-dependencies/build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle
index 8d2a70c7a69e..64813b60ab2d 100644
--- a/spring-boot-project/spring-boot-dependencies/build.gradle
+++ b/spring-boot-project/spring-boot-dependencies/build.gradle
@@ -1429,7 +1429,7 @@ bom {
 			]
 		}
 	}
-	library("Spring Kafka", "3.0.8") {
+	library("Spring Kafka", "3.0.9-SNAPSHOT") {
 		group("org.springframework.kafka") {
 			modules = [
 				"spring-kafka",

From e1b5eb5040e987f5780b000632ff89696e836bd8 Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Mon, 3 Jul 2023 19:27:14 +0100
Subject: [PATCH 0087/1215] Start building against Spring Security 6.2.0
 snapshots

See gh-36195
---
 spring-boot-project/spring-boot-dependencies/build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle
index 64813b60ab2d..e1842e44ee51 100644
--- a/spring-boot-project/spring-boot-dependencies/build.gradle
+++ b/spring-boot-project/spring-boot-dependencies/build.gradle
@@ -1461,7 +1461,7 @@ bom {
 			]
 		}
 	}
-	library("Spring Security", "6.1.1") {
+	library("Spring Security", "6.2.0-SNAPSHOT") {
 		group("org.springframework.security") {
 			imports = [
 				"spring-security-bom"

From afdc133d6a1465abca35f4d3c868b05bf528d45d Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Mon, 3 Jul 2023 19:27:19 +0100
Subject: [PATCH 0088/1215] Start building against Spring Session 3.2.0
 snapshots

See gh-36196
---
 spring-boot-project/spring-boot-dependencies/build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle
index e1842e44ee51..4af52df9df3d 100644
--- a/spring-boot-project/spring-boot-dependencies/build.gradle
+++ b/spring-boot-project/spring-boot-dependencies/build.gradle
@@ -1468,7 +1468,7 @@ bom {
 			]
 		}
 	}
-	library("Spring Session", "3.1.1") {
+	library("Spring Session", "3.2.0-SNAPSHOT") {
 		prohibit {
 			startsWith(["Apple-", "Bean-", "Corn-", "Dragonfruit-"])
 			because "Spring Session switched to numeric version numbers"

From 1fa079d9b5bfdb8a7aeadbf0a35342e365676d3b Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Mon, 3 Jul 2023 19:35:06 +0100
Subject: [PATCH 0089/1215] Start building against Micrometer Tracing 1.2.0
 snapshots

See gh-36199
---
 spring-boot-project/spring-boot-dependencies/build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle
index 4af52df9df3d..e77b1d30271d 100644
--- a/spring-boot-project/spring-boot-dependencies/build.gradle
+++ b/spring-boot-project/spring-boot-dependencies/build.gradle
@@ -984,7 +984,7 @@ bom {
 			]
 		}
 	}
-	library("Micrometer Tracing", "1.1.2") {
+	library("Micrometer Tracing", "1.2.0-SNAPSHOT") {
 		group("io.micrometer") {
 			imports = [
 				"micrometer-tracing-bom"

From 5a9ca67fbaba2c92c5a9ef0d13f5a7a5025eedba Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Mon, 3 Jul 2023 20:37:25 +0100
Subject: [PATCH 0090/1215] Start building against Spring Framework 6.2.0-M2
 snapshots

See gh-36198
---
 gradle.properties                             |  2 +-
 .../DispatcherServletAutoConfiguration.java   |  8 ++++++-
 .../web/servlet/WebMvcProperties.java         |  9 +++++++-
 ...spatcherServletAutoConfigurationTests.java | 23 ++++++++++++++++---
 .../web/servlet/MockMvcAutoConfiguration.java |  5 ++++
 .../boot/maven/JarIntegrationTests.java       |  3 ++-
 ...tpRequestFactoriesHttpComponentsTests.java |  2 +-
 7 files changed, 44 insertions(+), 8 deletions(-)

diff --git a/gradle.properties b/gradle.properties
index eb9bbe14aff8..d4321ef602e8 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -6,7 +6,7 @@ org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8
 
 kotlinVersion=1.8.22
 nativeBuildToolsVersion=0.9.23
-springFrameworkVersion=6.1.0-M1
+springFrameworkVersion=6.1.0-SNAPSHOT
 tomcatVersion=10.1.10
 
 kotlin.stdlib.default.dependency=false
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration.java
index 5575bde20a9c..5aafbfb0091e 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfiguration.java
@@ -89,12 +89,18 @@ public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
 			DispatcherServlet dispatcherServlet = new DispatcherServlet();
 			dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
 			dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
-			dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
+			configureThrowExceptionIfNoHandlerFound(webMvcProperties, dispatcherServlet);
 			dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
 			dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
 			return dispatcherServlet;
 		}
 
+		@SuppressWarnings({ "deprecation", "removal" })
+		private void configureThrowExceptionIfNoHandlerFound(WebMvcProperties webMvcProperties,
+				DispatcherServlet dispatcherServlet) {
+			dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
+		}
+
 		@Bean
 		@ConditionalOnBean(MultipartResolver.class)
 		@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java
index 0c73524c8f94..2ca9ae03743f 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java
@@ -21,6 +21,7 @@
 import java.util.Map;
 
 import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
 import org.springframework.http.MediaType;
 import org.springframework.util.Assert;
 import org.springframework.validation.DefaultMessageCodesResolver;
@@ -64,8 +65,10 @@ public class WebMvcProperties {
 	/**
 	 * Whether a "NoHandlerFoundException" should be thrown if no Handler was found to
 	 * process a request.
+	 * @deprecated since 3.2.0 for removal in 3.4.0
 	 */
-	private boolean throwExceptionIfNoHandlerFound = false;
+	@Deprecated(since = "3.2.0", forRemoval = true)
+	private boolean throwExceptionIfNoHandlerFound = true;
 
 	/**
 	 * Whether logging of (potentially sensitive) request details at DEBUG and TRACE level
@@ -121,10 +124,14 @@ public void setPublishRequestHandledEvents(boolean publishRequestHandledEvents)
 		this.publishRequestHandledEvents = publishRequestHandledEvents;
 	}
 
+	@Deprecated(since = "3.2.0", forRemoval = true)
+	@DeprecatedConfigurationProperty(
+			reason = "DispatcherServlet property is deprecated for removal and should no longer need to be configured")
 	public boolean isThrowExceptionIfNoHandlerFound() {
 		return this.throwExceptionIfNoHandlerFound;
 	}
 
+	@Deprecated(since = "3.2.0", forRemoval = true)
 	public void setThrowExceptionIfNoHandlerFound(boolean throwExceptionIfNoHandlerFound) {
 		this.throwExceptionIfNoHandlerFound = throwExceptionIfNoHandlerFound;
 	}
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfigurationTests.java
index 0dbf0eaf0ad6..100d36cdd917 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfigurationTests.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/DispatcherServletAutoConfigurationTests.java
@@ -141,7 +141,6 @@ void renamesMultipartResolver() {
 	void dispatcherServletDefaultConfig() {
 		this.contextRunner.run((context) -> {
 			DispatcherServlet dispatcherServlet = context.getBean(DispatcherServlet.class);
-			assertThat(dispatcherServlet).extracting("throwExceptionIfNoHandlerFound").isEqualTo(false);
 			assertThat(dispatcherServlet).extracting("dispatchOptionsRequest").isEqualTo(true);
 			assertThat(dispatcherServlet).extracting("dispatchTraceRequest").isEqualTo(false);
 			assertThat(dispatcherServlet).extracting("enableLoggingRequestDetails").isEqualTo(false);
@@ -151,15 +150,24 @@ void dispatcherServletDefaultConfig() {
 		});
 	}
 
+	@Test
+	@Deprecated(since = "3.2.0", forRemoval = true)
+	void dispatcherServletThrowExceptionIfNoHandlerFoundDefaultConfig() {
+		this.contextRunner.run((context) -> {
+			DispatcherServlet dispatcherServlet = context.getBean(DispatcherServlet.class);
+			assertThat(dispatcherServlet).extracting("throwExceptionIfNoHandlerFound").isEqualTo(true);
+		});
+	}
+
 	@Test
 	void dispatcherServletCustomConfig() {
 		this.contextRunner
-			.withPropertyValues("spring.mvc.throw-exception-if-no-handler-found:true",
+			.withPropertyValues("spring.mvc.throw-exception-if-no-handler-found:false",
 					"spring.mvc.dispatch-options-request:false", "spring.mvc.dispatch-trace-request:true",
 					"spring.mvc.publish-request-handled-events:false", "spring.mvc.servlet.load-on-startup=5")
 			.run((context) -> {
 				DispatcherServlet dispatcherServlet = context.getBean(DispatcherServlet.class);
-				assertThat(dispatcherServlet).extracting("throwExceptionIfNoHandlerFound").isEqualTo(true);
+				assertThat(dispatcherServlet).extracting("throwExceptionIfNoHandlerFound").isEqualTo(false);
 				assertThat(dispatcherServlet).extracting("dispatchOptionsRequest").isEqualTo(false);
 				assertThat(dispatcherServlet).extracting("dispatchTraceRequest").isEqualTo(true);
 				assertThat(dispatcherServlet).extracting("publishEvents").isEqualTo(false);
@@ -168,6 +176,15 @@ void dispatcherServletCustomConfig() {
 			});
 	}
 
+	@Test
+	@Deprecated(since = "3.2.0", forRemoval = true)
+	void dispatcherServletThrowExceptionIfNoHandlerFoundCustomConfig() {
+		this.contextRunner.withPropertyValues("spring.mvc.throw-exception-if-no-handler-found:false").run((context) -> {
+			DispatcherServlet dispatcherServlet = context.getBean(DispatcherServlet.class);
+			assertThat(dispatcherServlet).extracting("throwExceptionIfNoHandlerFound").isEqualTo(false);
+		});
+	}
+
 	@Configuration(proxyBeanMethods = false)
 	static class MultipartConfiguration {
 
diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfiguration.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfiguration.java
index 6d429eb4fae3..9e944e4fad3f 100644
--- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfiguration.java
+++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfiguration.java
@@ -131,6 +131,11 @@ private static class MockMvcDispatcherServletCustomizer implements DispatcherSer
 		public void customize(DispatcherServlet dispatcherServlet) {
 			dispatcherServlet.setDispatchOptionsRequest(this.webMvcProperties.isDispatchOptionsRequest());
 			dispatcherServlet.setDispatchTraceRequest(this.webMvcProperties.isDispatchTraceRequest());
+			configureThrowExceptionIfNoHandlerFound(dispatcherServlet);
+		}
+
+		@SuppressWarnings({ "deprecation", "removal" })
+		private void configureThrowExceptionIfNoHandlerFound(DispatcherServlet dispatcherServlet) {
 			dispatcherServlet
 				.setThrowExceptionIfNoHandlerFound(this.webMvcProperties.isThrowExceptionIfNoHandlerFound());
 		}
diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java
index 46ad4f7f3a83..07eeaaa70bc7 100644
--- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java
+++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java
@@ -428,7 +428,8 @@ private String buildJarWithOutputTimestamp(MavenBuild mavenBuild) {
 	void whenJarIsRepackagedWithOutputTimestampConfiguredThenLibrariesAreSorted(MavenBuild mavenBuild) {
 		mavenBuild.project("jar-output-timestamp").execute((project) -> {
 			File repackaged = new File(project, "target/jar-output-timestamp-0.0.1.BUILD-SNAPSHOT.jar");
-			List sortedLibs = Arrays.asList("BOOT-INF/lib/jakarta.servlet-api", "BOOT-INF/lib/spring-aop",
+			List sortedLibs = Arrays.asList("BOOT-INF/lib/jakarta.servlet-api",
+					"BOOT-INF/lib/micrometer-commons", "BOOT-INF/lib/micrometer-observation", "BOOT-INF/lib/spring-aop",
 					"BOOT-INF/lib/spring-beans", "BOOT-INF/lib/spring-boot-jarmode-layertools",
 					"BOOT-INF/lib/spring-context", "BOOT-INF/lib/spring-core", "BOOT-INF/lib/spring-expression",
 					"BOOT-INF/lib/spring-jcl");
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesHttpComponentsTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesHttpComponentsTests.java
index 98b9095afec7..b8df330e7790 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesHttpComponentsTests.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesHttpComponentsTests.java
@@ -39,7 +39,7 @@ class ClientHttpRequestFactoriesHttpComponentsTests
 
 	@Override
 	protected long connectTimeout(HttpComponentsClientHttpRequestFactory requestFactory) {
-		return (int) ReflectionTestUtils.getField(requestFactory, "connectTimeout");
+		return (long) ReflectionTestUtils.getField(requestFactory, "connectTimeout");
 	}
 
 	@Override

From 9985c845f29d40b5ab335167319acab0d0bc554b Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Tue, 4 Jul 2023 11:59:31 +0100
Subject: [PATCH 0091/1215] Adapt to Framework changes missed due to predictive
 test selection

See gh-36198
---
 .../org/springframework/boot/maven/WarIntegrationTests.java  | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java
index 77fd8842ec71..0a8894cb6bfc 100644
--- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java
+++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java
@@ -122,8 +122,9 @@ void whenWarIsRepackagedWithOutputTimestampConfiguredThenLibrariesAreSorted(Mave
 			List sortedLibs = Arrays.asList(
 					// these libraries are copied from the original war, sorted when
 					// packaged by Maven
-					"WEB-INF/lib/spring-aop", "WEB-INF/lib/spring-beans", "WEB-INF/lib/spring-context",
-					"WEB-INF/lib/spring-core", "WEB-INF/lib/spring-expression", "WEB-INF/lib/spring-jcl",
+					"WEB-INF/lib/micrometer-commons", "WEB-INF/lib/micrometer-observation", "WEB-INF/lib/spring-aop",
+					"WEB-INF/lib/spring-beans", "WEB-INF/lib/spring-context", "WEB-INF/lib/spring-core",
+					"WEB-INF/lib/spring-expression", "WEB-INF/lib/spring-jcl",
 					// these libraries are contributed by Spring Boot repackaging, and
 					// sorted separately
 					"WEB-INF/lib/spring-boot-jarmode-layertools");

From 2350d9c870461da229377e7f4426e68d7ba540cb Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Tue, 4 Jul 2023 12:08:27 +0100
Subject: [PATCH 0092/1215] Adapt to Data changes missed due to predictive test
 selection

See gh-36190
---
 ...ngoReactiveDataAutoConfigurationTests.java | 23 +++++++++++++++----
 1 file changed, 18 insertions(+), 5 deletions(-)

diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveDataAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveDataAutoConfigurationTests.java
index df9a5f264ee2..a64fe5b7031d 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveDataAutoConfigurationTests.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/mongo/MongoReactiveDataAutoConfigurationTests.java
@@ -16,8 +16,13 @@
 
 package org.springframework.boot.autoconfigure.data.mongo;
 
+import java.time.Duration;
+
 import com.mongodb.ConnectionString;
+import com.mongodb.reactivestreams.client.MongoCollection;
+import com.mongodb.reactivestreams.client.gridfs.GridFSBucket;
 import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Mono;
 
 import org.springframework.boot.autoconfigure.AutoConfigurations;
 import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
@@ -68,20 +73,26 @@ void whenGridFsDatabaseIsConfiguredThenGridFsTemplateUsesIt() {
 	}
 
 	@Test
+	@SuppressWarnings("unchecked")
 	void usesMongoConnectionDetailsIfAvailable() {
 		this.contextRunner.withUserConfiguration(ConnectionDetailsConfiguration.class).run((context) -> {
 			assertThat(grisFsTemplateDatabaseName(context)).isEqualTo("grid-database-1");
 			ReactiveGridFsTemplate template = context.getBean(ReactiveGridFsTemplate.class);
-			assertThat(template).hasFieldOrPropertyWithValue("bucket", "connection-details-bucket");
+			GridFSBucket bucket = ((Mono) ReflectionTestUtils.getField(template, "bucketSupplier"))
+				.block(Duration.ofSeconds(30));
+			assertThat(bucket.getBucketName()).isEqualTo("connection-details-bucket");
 		});
 	}
 
 	@Test
+	@SuppressWarnings("unchecked")
 	void whenGridFsBucketIsConfiguredThenGridFsTemplateUsesIt() {
 		this.contextRunner.withPropertyValues("spring.data.mongodb.gridfs.bucket:test-bucket").run((context) -> {
 			assertThat(context).hasSingleBean(ReactiveGridFsTemplate.class);
 			ReactiveGridFsTemplate template = context.getBean(ReactiveGridFsTemplate.class);
-			assertThat(template).hasFieldOrPropertyWithValue("bucket", "test-bucket");
+			GridFSBucket bucket = ((Mono) ReflectionTestUtils.getField(template, "bucketSupplier"))
+				.block(Duration.ofSeconds(30));
+			assertThat(bucket.getBucketName()).isEqualTo("test-bucket");
 		});
 	}
 
@@ -150,12 +161,14 @@ void contextFailsWhenDatabaseNotSet() {
 			.run((context) -> assertThat(context).getFailure().hasMessageContaining("Database name must not be empty"));
 	}
 
+	@SuppressWarnings("unchecked")
 	private String grisFsTemplateDatabaseName(AssertableApplicationContext context) {
 		assertThat(context).hasSingleBean(ReactiveGridFsTemplate.class);
 		ReactiveGridFsTemplate template = context.getBean(ReactiveGridFsTemplate.class);
-		ReactiveMongoDatabaseFactory factory = (ReactiveMongoDatabaseFactory) ReflectionTestUtils.getField(template,
-				"dbFactory");
-		return factory.getMongoDatabase().block().getName();
+		GridFSBucket bucket = ((Mono) ReflectionTestUtils.getField(template, "bucketSupplier"))
+			.block(Duration.ofSeconds(30));
+		MongoCollection collection = (MongoCollection) ReflectionTestUtils.getField(bucket, "filesCollection");
+		return collection.getNamespace().getDatabaseName();
 	}
 
 	@Configuration(proxyBeanMethods = false)

From 4562189125d2894e27aaac122591aac44f2ef1bb Mon Sep 17 00:00:00 2001
From: Laurent Martelli 
Date: Tue, 4 Jul 2023 12:22:57 +0100
Subject: [PATCH 0093/1215] Switch ImportsContextCustomizer to use
 MergedAnnotations.search #36211

Use `MergedAnnotations.search` in `ImportsContextCustomizer` rather than
needing dedicated search logic.

See gh-36211
---
 .../context/ImportsContextCustomizer.java     | 165 ++++--------------
 1 file changed, 35 insertions(+), 130 deletions(-)

diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java
index fc4a8babbf6a..4b8a6a753132 100644
--- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java
+++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java
@@ -16,13 +16,12 @@
 
 package org.springframework.boot.test.context;
 
-import java.lang.annotation.Annotation;
-import java.lang.reflect.AnnotatedElement;
 import java.lang.reflect.Constructor;
 import java.util.Collections;
 import java.util.HashSet;
-import java.util.LinkedHashSet;
 import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import org.springframework.beans.BeansException;
 import org.springframework.beans.factory.BeanFactory;
@@ -42,10 +41,10 @@
 import org.springframework.context.annotation.ImportSelector;
 import org.springframework.context.support.AbstractApplicationContext;
 import org.springframework.core.Ordered;
-import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.core.annotation.AnnotationFilter;
 import org.springframework.core.annotation.MergedAnnotation;
 import org.springframework.core.annotation.MergedAnnotations;
-import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
 import org.springframework.core.annotation.Order;
 import org.springframework.core.style.ToStringCreator;
 import org.springframework.core.type.AnnotationMetadata;
@@ -53,6 +52,8 @@
 import org.springframework.test.context.MergedContextConfiguration;
 import org.springframework.util.ReflectionUtils;
 
+import static org.springframework.core.annotation.AnnotationFilter.packages;
+
 /**
  * {@link ContextCustomizer} to allow {@code @Import} annotations to be used directly on
  * test classes.
@@ -217,82 +218,41 @@ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) t
 	 */
 	static class ContextCustomizerKey {
 
-		private static final Class[] NO_IMPORTS = {};
-
-		private static final Set ANNOTATION_FILTERS;
-
-		static {
-			Set filters = new HashSet<>();
-			filters.add(new JavaLangAnnotationFilter());
-			filters.add(new KotlinAnnotationFilter());
-			filters.add(new SpockAnnotationFilter());
-			filters.add(new JUnitAnnotationFilter());
-			ANNOTATION_FILTERS = Collections.unmodifiableSet(filters);
-		}
+		private static final AnnotationFilter ANNOTATION_FILTERS = or(packages("java.lang.annotation"),
+				packages("org.spockframework", "spock"),
+				or(isEqualTo("kotlin.Metadata"), packages("kotlin.annotation")), packages(("org.junit")));
 
-		private final Set key;
+		private final Object key;
 
 		ContextCustomizerKey(Class testClass) {
-			Set annotations = new HashSet<>();
-			Set> seen = new HashSet<>();
-			collectClassAnnotations(testClass, annotations, seen);
-			Set determinedImports = determineImports(annotations, testClass);
-			this.key = Collections.unmodifiableSet((determinedImports != null) ? determinedImports : annotations);
-		}
-
-		private void collectClassAnnotations(Class classType, Set annotations, Set> seen) {
-			if (seen.add(classType)) {
-				collectElementAnnotations(classType, annotations, seen);
-				for (Class interfaceType : classType.getInterfaces()) {
-					collectClassAnnotations(interfaceType, annotations, seen);
-				}
-				if (classType.getSuperclass() != null) {
-					collectClassAnnotations(classType.getSuperclass(), annotations, seen);
-				}
+			MergedAnnotations mergedAnnotations = MergedAnnotations
+				.search(MergedAnnotations.SearchStrategy.TYPE_HIERARCHY)
+				.withAnnotationFilter(ANNOTATION_FILTERS)
+				.from(testClass);
+			Set determinedImports = determineImports(mergedAnnotations, testClass);
+			if (determinedImports != null) {
+				this.key = determinedImports;
 			}
-		}
-
-		private void collectElementAnnotations(AnnotatedElement element, Set annotations,
-				Set> seen) {
-			for (MergedAnnotation mergedAnnotation : MergedAnnotations.from(element,
-					SearchStrategy.DIRECT)) {
-				Annotation annotation = mergedAnnotation.synthesize();
-				if (!isIgnoredAnnotation(annotation)) {
-					annotations.add(annotation);
-					collectClassAnnotations(annotation.annotationType(), annotations, seen);
-				}
+			else {
+				this.key = AnnotatedElementUtils.findAllMergedAnnotations(testClass,
+						mergedAnnotations.stream().map(MergedAnnotation::getType).collect(Collectors.toSet()));
 			}
 		}
 
-		private boolean isIgnoredAnnotation(Annotation annotation) {
-			for (AnnotationFilter annotationFilter : ANNOTATION_FILTERS) {
-				if (annotationFilter.isIgnored(annotation)) {
-					return true;
-				}
-			}
-			return false;
-		}
-
-		private Set determineImports(Set annotations, Class testClass) {
-			Set determinedImports = new LinkedHashSet<>();
+		private Set determineImports(MergedAnnotations mergedAnnotations, Class testClass) {
 			AnnotationMetadata testClassMetadata = AnnotationMetadata.introspect(testClass);
-			for (Annotation annotation : annotations) {
-				for (Class source : getImports(annotation)) {
-					Set determinedSourceImports = determineImports(source, testClassMetadata);
-					if (determinedSourceImports == null) {
+			return mergedAnnotations.stream(Import.class)
+				.flatMap((ma) -> Stream.of(ma.getClassArray("value")))
+				.map((source) -> determineImports(source, testClassMetadata))
+				.reduce(new HashSet<>(), (a, b) -> {
+					if (a == null || b == null) {
 						return null;
 					}
-					determinedImports.addAll(determinedSourceImports);
-				}
-			}
-			return determinedImports;
-		}
-
-		private Class[] getImports(Annotation annotation) {
-			if (annotation instanceof Import importAnnotation) {
-				return importAnnotation.value();
-			}
-			return NO_IMPORTS;
+					else {
+						a.add(b);
+						return a;
+					}
+				});
 		}
 
 		private Set determineImports(Class source, AnnotationMetadata metadata) {
@@ -340,67 +300,12 @@ public String toString() {
 
 	}
 
-	/**
-	 * Filter used to limit considered annotations.
-	 */
-	private interface AnnotationFilter {
-
-		boolean isIgnored(Annotation annotation);
-
-	}
-
-	/**
-	 * {@link AnnotationFilter} for {@literal java.lang} annotations.
-	 */
-	private static final class JavaLangAnnotationFilter implements AnnotationFilter {
-
-		@Override
-		public boolean isIgnored(Annotation annotation) {
-			return AnnotationUtils.isInJavaLangAnnotationPackage(annotation);
-		}
-
-	}
-
-	/**
-	 * {@link AnnotationFilter} for Kotlin annotations.
-	 */
-	private static final class KotlinAnnotationFilter implements AnnotationFilter {
-
-		@Override
-		public boolean isIgnored(Annotation annotation) {
-			return "kotlin.Metadata".equals(annotation.annotationType().getName())
-					|| isInKotlinAnnotationPackage(annotation);
-		}
-
-		private boolean isInKotlinAnnotationPackage(Annotation annotation) {
-			return annotation.annotationType().getName().startsWith("kotlin.annotation.");
-		}
-
+	static AnnotationFilter or(AnnotationFilter... filters) {
+		return typeName -> Stream.of(filters).anyMatch(filter -> filter.matches(typeName));
 	}
 
-	/**
-	 * {@link AnnotationFilter} for Spock annotations.
-	 */
-	private static final class SpockAnnotationFilter implements AnnotationFilter {
-
-		@Override
-		public boolean isIgnored(Annotation annotation) {
-			return annotation.annotationType().getName().startsWith("org.spockframework.")
-					|| annotation.annotationType().getName().startsWith("spock.");
-		}
-
-	}
-
-	/**
-	 * {@link AnnotationFilter} for JUnit annotations.
-	 */
-	private static final class JUnitAnnotationFilter implements AnnotationFilter {
-
-		@Override
-		public boolean isIgnored(Annotation annotation) {
-			return annotation.annotationType().getName().startsWith("org.junit.");
-		}
-
+	static AnnotationFilter isEqualTo(String expectedTypeName) {
+		return typeName -> typeName.equals(expectedTypeName);
 	}
 
 }

From 7c942679ad85f254acaa0318cf07753c65a21716 Mon Sep 17 00:00:00 2001
From: Phillip Webb 
Date: Tue, 4 Jul 2023 12:49:52 +0100
Subject: [PATCH 0094/1215] Polish 'Switch ImportsContextCustomizer to use
 MergedAnnotations.search'

See gh-36211
---
 .../context/ImportsContextCustomizer.java     | 77 +++++++++----------
 1 file changed, 36 insertions(+), 41 deletions(-)

diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java
index 4b8a6a753132..ca5786240bbe 100644
--- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java
+++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java
@@ -18,10 +18,9 @@
 
 import java.lang.reflect.Constructor;
 import java.util.Collections;
-import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.Set;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
 import org.springframework.beans.BeansException;
 import org.springframework.beans.factory.BeanFactory;
@@ -41,7 +40,6 @@
 import org.springframework.context.annotation.ImportSelector;
 import org.springframework.context.support.AbstractApplicationContext;
 import org.springframework.core.Ordered;
-import org.springframework.core.annotation.AnnotatedElementUtils;
 import org.springframework.core.annotation.AnnotationFilter;
 import org.springframework.core.annotation.MergedAnnotation;
 import org.springframework.core.annotation.MergedAnnotations;
@@ -52,14 +50,13 @@
 import org.springframework.test.context.MergedContextConfiguration;
 import org.springframework.util.ReflectionUtils;
 
-import static org.springframework.core.annotation.AnnotationFilter.packages;
-
 /**
  * {@link ContextCustomizer} to allow {@code @Import} annotations to be used directly on
  * test classes.
  *
  * @author Phillip Webb
  * @author Andy Wilkinson
+ * @author Laurent Martelli
  * @see ImportsContextCustomizerFactory
  */
 class ImportsContextCustomizer implements ContextCustomizer {
@@ -218,41 +215,43 @@ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) t
 	 */
 	static class ContextCustomizerKey {
 
-		private static final AnnotationFilter ANNOTATION_FILTERS = or(packages("java.lang.annotation"),
-				packages("org.spockframework", "spock"),
-				or(isEqualTo("kotlin.Metadata"), packages("kotlin.annotation")), packages(("org.junit")));
-
-		private final Object key;
+		private static final Set ANNOTATION_FILTERS;
+		static {
+			Set annotationFilters = new LinkedHashSet<>();
+			annotationFilters.add(AnnotationFilter.PLAIN);
+			annotationFilters.add("kotlin.Metadata"::equals);
+			annotationFilters.add(AnnotationFilter.packages("kotlin.annotation"));
+			annotationFilters.add(AnnotationFilter.packages("org.spockframework", "spock"));
+			annotationFilters.add(AnnotationFilter.packages("org.junit"));
+			ANNOTATION_FILTERS = Collections.unmodifiableSet(annotationFilters);
+		}
+		private final Set key;
 
 		ContextCustomizerKey(Class testClass) {
-			MergedAnnotations mergedAnnotations = MergedAnnotations
-				.search(MergedAnnotations.SearchStrategy.TYPE_HIERARCHY)
-				.withAnnotationFilter(ANNOTATION_FILTERS)
+			MergedAnnotations annotations = MergedAnnotations.search(MergedAnnotations.SearchStrategy.TYPE_HIERARCHY)
+				.withAnnotationFilter(this::isFilteredAnnotation)
 				.from(testClass);
-			Set determinedImports = determineImports(mergedAnnotations, testClass);
-			if (determinedImports != null) {
-				this.key = determinedImports;
-			}
-			else {
-				this.key = AnnotatedElementUtils.findAllMergedAnnotations(testClass,
-						mergedAnnotations.stream().map(MergedAnnotation::getType).collect(Collectors.toSet()));
-			}
+			Set determinedImports = determineImports(annotations, testClass);
+			this.key = (determinedImports != null) ? determinedImports : synthesize(annotations);
+		}
+
+		private boolean isFilteredAnnotation(String typeName) {
+			return ANNOTATION_FILTERS.stream().anyMatch((filter) -> filter.matches(typeName));
 		}
 
-		private Set determineImports(MergedAnnotations mergedAnnotations, Class testClass) {
-			AnnotationMetadata testClassMetadata = AnnotationMetadata.introspect(testClass);
-			return mergedAnnotations.stream(Import.class)
-				.flatMap((ma) -> Stream.of(ma.getClassArray("value")))
-				.map((source) -> determineImports(source, testClassMetadata))
-				.reduce(new HashSet<>(), (a, b) -> {
-					if (a == null || b == null) {
+		private Set determineImports(MergedAnnotations annotations, Class testClass) {
+			Set determinedImports = new LinkedHashSet<>();
+			AnnotationMetadata metadata = AnnotationMetadata.introspect(testClass);
+			for (MergedAnnotation annotation : annotations.stream(Import.class).toList()) {
+				for (Class source : annotation.getClassArray(MergedAnnotation.VALUE)) {
+					Set determinedSourceImports = determineImports(source, metadata);
+					if (determinedSourceImports == null) {
 						return null;
 					}
-					else {
-						a.add(b);
-						return a;
-					}
-				});
+					determinedImports.addAll(determinedSourceImports);
+				}
+			}
+			return determinedImports;
 		}
 
 		private Set determineImports(Class source, AnnotationMetadata metadata) {
@@ -270,6 +269,10 @@ private Set determineImports(Class source, AnnotationMetadata metadat
 			return Collections.singleton(source.getName());
 		}
 
+		private Set synthesize(MergedAnnotations annotations) {
+			return annotations.stream().map(MergedAnnotation::synthesize).collect(Collectors.toSet());
+		}
+
 		@SuppressWarnings("unchecked")
 		private  T instantiate(Class source) {
 			try {
@@ -300,12 +303,4 @@ public String toString() {
 
 	}
 
-	static AnnotationFilter or(AnnotationFilter... filters) {
-		return typeName -> Stream.of(filters).anyMatch(filter -> filter.matches(typeName));
-	}
-
-	static AnnotationFilter isEqualTo(String expectedTypeName) {
-		return typeName -> typeName.equals(expectedTypeName);
-	}
-
 }

From 7ceece3d3d1a6a77672d00b32e3f166d81f06b66 Mon Sep 17 00:00:00 2001
From: Arjen Poutsma 
Date: Thu, 29 Jun 2023 13:24:43 +0200
Subject: [PATCH 0095/1215] Support Jetty in ClientHttpRequestFactories

This commit introduces support for the JettyClientHttpRequestFactory
in ClientHttpRequestFactories.

See gh-36116
---
 spring-boot-project/spring-boot/build.gradle  |  1 +
 .../client/ClientHttpRequestFactories.java    | 46 +++++++++++++++++
 .../ClientHttpRequestFactoriesJettyTests.java | 51 +++++++++++++++++++
 ...ClientHttpRequestFactoriesSimpleTests.java |  2 +-
 4 files changed, 99 insertions(+), 1 deletion(-)
 create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesJettyTests.java

diff --git a/spring-boot-project/spring-boot/build.gradle b/spring-boot-project/spring-boot/build.gradle
index 3a862fe98b67..6b86fd02adcf 100644
--- a/spring-boot-project/spring-boot/build.gradle
+++ b/spring-boot-project/spring-boot/build.gradle
@@ -67,6 +67,7 @@ dependencies {
 	optional("org.eclipse.jetty.http2:http2-server") {
 		exclude(group: "org.eclipse.jetty.toolchain", module: "jetty-jakarta-servlet-api")
 	}
+	optional("org.eclipse.jetty:jetty-client")
 	optional("org.flywaydb:flyway-core")
 	optional("org.hamcrest:hamcrest-library")
 	optional("org.hibernate.orm:hibernate-core")
diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java
index 5b85a914c039..384e2f2b8914 100644
--- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java
+++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java
@@ -26,6 +26,7 @@
 import java.util.function.Supplier;
 
 import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLSocketFactory;
 import javax.net.ssl.TrustManager;
 import javax.net.ssl.X509TrustManager;
@@ -38,6 +39,9 @@
 import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier;
 import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
 import org.apache.hc.core5.http.io.SocketConfig;
+import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic;
+import org.eclipse.jetty.io.ClientConnector;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
 
 import org.springframework.boot.context.properties.PropertyMapper;
 import org.springframework.boot.ssl.SslBundle;
@@ -45,6 +49,7 @@
 import org.springframework.http.client.AbstractClientHttpRequestFactoryWrapper;
 import org.springframework.http.client.ClientHttpRequestFactory;
 import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
+import org.springframework.http.client.JettyClientHttpRequestFactory;
 import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
 import org.springframework.http.client.SimpleClientHttpRequestFactory;
 import org.springframework.util.Assert;
@@ -70,6 +75,10 @@ public final class ClientHttpRequestFactories {
 
 	private static final boolean OKHTTP_CLIENT_PRESENT = ClassUtils.isPresent(OKHTTP_CLIENT_CLASS, null);
 
+	static final String JETTY_CLIENT_CLASS = "org.eclipse.jetty.client.HttpClient";
+
+	private static final boolean JETTY_CLIENT_PRESENT = ClassUtils.isPresent(JETTY_CLIENT_CLASS, null);
+
 	private ClientHttpRequestFactories() {
 	}
 
@@ -87,6 +96,9 @@ public static ClientHttpRequestFactory get(ClientHttpRequestFactorySettings sett
 		if (OKHTTP_CLIENT_PRESENT) {
 			return OkHttp.get(settings);
 		}
+		if (JETTY_CLIENT_PRESENT) {
+			return Jetty.get(settings);
+		}
 		return Simple.get(settings);
 	}
 
@@ -111,6 +123,9 @@ public static  T get(Class requestFactory
 		if (requestFactoryType == OkHttp3ClientHttpRequestFactory.class) {
 			return (T) OkHttp.get(settings);
 		}
+		if (requestFactoryType == JettyClientHttpRequestFactory.class) {
+			return (T) Jetty.get(settings);
+		}
 		if (requestFactoryType == SimpleClientHttpRequestFactory.class) {
 			return (T) Simple.get(settings);
 		}
@@ -210,6 +225,37 @@ private static OkHttp3ClientHttpRequestFactory createRequestFactory(SslBundle ss
 
 	}
 
+	/**
+	 * Support for {@link JettyClientHttpRequestFactory}.
+	 */
+	static class Jetty {
+
+		static JettyClientHttpRequestFactory get(ClientHttpRequestFactorySettings settings) {
+			JettyClientHttpRequestFactory requestFactory = createRequestFactory(settings.sslBundle());
+			PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
+			map.from(settings::connectTimeout).asInt(Duration::toMillis).to(requestFactory::setConnectTimeout);
+			map.from(settings::readTimeout).asInt(Duration::toMillis).to(requestFactory::setReadTimeout);
+			return requestFactory;
+		}
+
+		private static JettyClientHttpRequestFactory createRequestFactory(SslBundle sslBundle) {
+			if (sslBundle != null) {
+				SSLContext sslContext = sslBundle.createSslContext();
+
+				SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
+				sslContextFactory.setSslContext(sslContext);
+
+				ClientConnector connector = new ClientConnector();
+				connector.setSslContextFactory(sslContextFactory);
+				org.eclipse.jetty.client.HttpClient httpClient =
+						new org.eclipse.jetty.client.HttpClient(new HttpClientTransportDynamic(connector));
+				return new JettyClientHttpRequestFactory(httpClient);
+			}
+			return new JettyClientHttpRequestFactory();
+		}
+
+	}
+
 	/**
 	 * Support for {@link SimpleClientHttpRequestFactory}.
 	 */
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesJettyTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesJettyTests.java
new file mode 100644
index 000000000000..10cb8a609fcb
--- /dev/null
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesJettyTests.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2012-2023 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.web.client;
+
+import org.eclipse.jetty.client.HttpClient;
+
+import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
+import org.springframework.http.client.JettyClientHttpRequestFactory;
+import org.springframework.test.util.ReflectionTestUtils;
+
+/**
+ * Tests for {@link ClientHttpRequestFactories} when Jetty is the
+ * predominant HTTP client.
+ *
+ * @author Arjen Poutsma
+ */
+@ClassPathExclusions({ "httpclient5-*.jar", "okhttp-*.jar" })
+class ClientHttpRequestFactoriesJettyTests
+		extends AbstractClientHttpRequestFactoriesTests {
+
+	ClientHttpRequestFactoriesJettyTests() {
+		super(JettyClientHttpRequestFactory.class);
+	}
+
+	@Override
+	protected long connectTimeout(JettyClientHttpRequestFactory requestFactory) {
+		HttpClient client = (HttpClient) ReflectionTestUtils.getField(requestFactory, "httpClient");
+		return client.getConnectTimeout();
+	}
+
+	@Override
+	@SuppressWarnings("unchecked")
+	protected long readTimeout(JettyClientHttpRequestFactory requestFactory) {
+		return (int) ReflectionTestUtils.getField(requestFactory, "readTimeout");
+	}
+
+}
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesSimpleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesSimpleTests.java
index a9e75aa6496b..f00882bc7df5 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesSimpleTests.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesSimpleTests.java
@@ -26,7 +26,7 @@
  *
  * @author Andy Wilkinson
  */
-@ClassPathExclusions({ "httpclient5-*.jar", "okhttp-*.jar" })
+@ClassPathExclusions({ "httpclient5-*.jar", "okhttp-*.jar", "jetty-client-*.jar" })
 class ClientHttpRequestFactoriesSimpleTests
 		extends AbstractClientHttpRequestFactoriesTests {
 

From c3e2c9d684e8df0b2b598e883858b934b1cb8048 Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Tue, 4 Jul 2023 13:57:46 +0100
Subject: [PATCH 0096/1215] Polish "Support Jetty in
 ClientHttpRequestFactories"

See gh-36116
---
 .../src/docs/asciidoc/io/rest-client.adoc     |  1 +
 .../client/ClientHttpRequestFactories.java    |  6 ++----
 ...lientHttpRequestFactoriesRuntimeHints.java | 14 +++++++++++--
 .../ClientHttpRequestFactoriesJettyTests.java |  9 +++-----
 ...HttpRequestFactoriesRuntimeHintsTests.java | 12 +++++++++++
 ...geSenderBuilderSimpleIntegrationTests.java |  2 +-
 .../jetty/SampleJettyApplicationTests.java    | 21 +++++++------------
 7 files changed, 38 insertions(+), 27 deletions(-)

diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/rest-client.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/rest-client.adoc
index 0d46c8c21e15..0147af61f5e6 100644
--- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/rest-client.adoc
+++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/rest-client.adoc
@@ -25,6 +25,7 @@ In order of preference, the following clients are supported:
 
 . Apache HttpClient
 . OkHttp
+. Jetty HttpClient
 . Simple JDK client (`HttpURLConnection`)
 
 If multiple clients are available on the classpath, the most preferred client will be used.
diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java
index 384e2f2b8914..347f6c4b0c58 100644
--- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java
+++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java
@@ -241,14 +241,12 @@ static JettyClientHttpRequestFactory get(ClientHttpRequestFactorySettings settin
 		private static JettyClientHttpRequestFactory createRequestFactory(SslBundle sslBundle) {
 			if (sslBundle != null) {
 				SSLContext sslContext = sslBundle.createSslContext();
-
 				SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
 				sslContextFactory.setSslContext(sslContext);
-
 				ClientConnector connector = new ClientConnector();
 				connector.setSslContextFactory(sslContextFactory);
-				org.eclipse.jetty.client.HttpClient httpClient =
-						new org.eclipse.jetty.client.HttpClient(new HttpClientTransportDynamic(connector));
+				org.eclipse.jetty.client.HttpClient httpClient = new org.eclipse.jetty.client.HttpClient(
+						new HttpClientTransportDynamic(connector));
 				return new JettyClientHttpRequestFactory(httpClient);
 			}
 			return new JettyClientHttpRequestFactory();
diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHints.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHints.java
index 457110d6c116..90f3db9af163 100644
--- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHints.java
+++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHints.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2022 the original author or authors.
+ * Copyright 2012-2023 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.
@@ -28,6 +28,7 @@
 import org.springframework.http.client.AbstractClientHttpRequestFactoryWrapper;
 import org.springframework.http.client.ClientHttpRequestFactory;
 import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
+import org.springframework.http.client.JettyClientHttpRequestFactory;
 import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
 import org.springframework.http.client.SimpleClientHttpRequestFactory;
 import org.springframework.util.Assert;
@@ -59,6 +60,10 @@ private void registerHints(ReflectionHints hints, ClassLoader classLoader) {
 			typeHint.onReachableType(TypeReference.of(ClientHttpRequestFactories.OKHTTP_CLIENT_CLASS));
 			registerReflectionHints(hints, OkHttp3ClientHttpRequestFactory.class);
 		});
+		hints.registerTypeIfPresent(classLoader, ClientHttpRequestFactories.JETTY_CLIENT_CLASS, (typeHint) -> {
+			typeHint.onReachableType(TypeReference.of(ClientHttpRequestFactories.JETTY_CLIENT_CLASS));
+			registerReflectionHints(hints, JettyClientHttpRequestFactory.class, long.class);
+		});
 		hints.registerType(SimpleClientHttpRequestFactory.class, (typeHint) -> {
 			typeHint.onReachableType(HttpURLConnection.class);
 			registerReflectionHints(hints, SimpleClientHttpRequestFactory.class);
@@ -67,8 +72,13 @@ private void registerHints(ReflectionHints hints, ClassLoader classLoader) {
 
 	private void registerReflectionHints(ReflectionHints hints,
 			Class requestFactoryType) {
+		registerReflectionHints(hints, requestFactoryType, int.class);
+	}
+
+	private void registerReflectionHints(ReflectionHints hints,
+			Class requestFactoryType, Class readTimeoutType) {
 		registerMethod(hints, requestFactoryType, "setConnectTimeout", int.class);
-		registerMethod(hints, requestFactoryType, "setReadTimeout", int.class);
+		registerMethod(hints, requestFactoryType, "setReadTimeout", readTimeoutType);
 	}
 
 	private void registerMethod(ReflectionHints hints, Class requestFactoryType,
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesJettyTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesJettyTests.java
index 10cb8a609fcb..27bc1800477c 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesJettyTests.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesJettyTests.java
@@ -23,8 +23,7 @@
 import org.springframework.test.util.ReflectionTestUtils;
 
 /**
- * Tests for {@link ClientHttpRequestFactories} when Jetty is the
- * predominant HTTP client.
+ * Tests for {@link ClientHttpRequestFactories} when Jetty is the predominant HTTP client.
  *
  * @author Arjen Poutsma
  */
@@ -38,14 +37,12 @@ class ClientHttpRequestFactoriesJettyTests
 
 	@Override
 	protected long connectTimeout(JettyClientHttpRequestFactory requestFactory) {
-		HttpClient client = (HttpClient) ReflectionTestUtils.getField(requestFactory, "httpClient");
-		return client.getConnectTimeout();
+		return ((HttpClient) ReflectionTestUtils.getField(requestFactory, "httpClient")).getConnectTimeout();
 	}
 
 	@Override
-	@SuppressWarnings("unchecked")
 	protected long readTimeout(JettyClientHttpRequestFactory requestFactory) {
-		return (int) ReflectionTestUtils.getField(requestFactory, "readTimeout");
+		return (long) ReflectionTestUtils.getField(requestFactory, "readTimeout");
 	}
 
 }
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHintsTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHintsTests.java
index bbfdafccf580..7eee323f1130 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHintsTests.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHintsTests.java
@@ -26,6 +26,7 @@
 import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
 import org.springframework.http.client.AbstractClientHttpRequestFactoryWrapper;
 import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
+import org.springframework.http.client.JettyClientHttpRequestFactory;
 import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
 import org.springframework.http.client.SimpleClientHttpRequestFactory;
 import org.springframework.util.ReflectionUtils;
@@ -73,6 +74,17 @@ void shouldRegisterOkHttpHints() {
 		assertThat(hints.reflection().getTypeHint(OkHttp3ClientHttpRequestFactory.class).methods()).hasSize(2);
 	}
 
+	@Test
+	void shouldRegisterJettyClientHints() {
+		RuntimeHints hints = new RuntimeHints();
+		new ClientHttpRequestFactoriesRuntimeHints().registerHints(hints, getClass().getClassLoader());
+		ReflectionHintsPredicates reflection = RuntimeHintsPredicates.reflection();
+		assertThat(reflection.onMethod(method(JettyClientHttpRequestFactory.class, "setConnectTimeout", int.class)))
+			.accepts(hints);
+		assertThat(reflection.onMethod(method(JettyClientHttpRequestFactory.class, "setReadTimeout", long.class)))
+			.accepts(hints);
+	}
+
 	@Test
 	void shouldRegisterSimpleHttpHints() {
 		RuntimeHints hints = new RuntimeHints();
diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderSimpleIntegrationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderSimpleIntegrationTests.java
index 2c3cb5374861..0014d47cd7ef 100644
--- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderSimpleIntegrationTests.java
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderSimpleIntegrationTests.java
@@ -34,7 +34,7 @@
  *
  * @author Stephane Nicoll
  */
-@ClassPathExclusions({ "httpclient5-*.jar", "okhttp*.jar" })
+@ClassPathExclusions(files = { "httpclient5-*.jar", "jetty-client-*.jar", "okhttp*.jar" })
 class HttpWebServiceMessageSenderBuilderSimpleIntegrationTests {
 
 	private final HttpWebServiceMessageSenderBuilder builder = new HttpWebServiceMessageSenderBuilder();
diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/test/java/smoketest/jetty/SampleJettyApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/test/java/smoketest/jetty/SampleJettyApplicationTests.java
index e21fadb0802f..abde5d9033a5 100644
--- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/test/java/smoketest/jetty/SampleJettyApplicationTests.java
+++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/test/java/smoketest/jetty/SampleJettyApplicationTests.java
@@ -16,10 +16,6 @@
 
 package smoketest.jetty;
 
-import java.io.ByteArrayInputStream;
-import java.nio.charset.StandardCharsets;
-import java.util.zip.GZIPInputStream;
-
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import smoketest.jetty.util.RandomStringUtil;
@@ -35,7 +31,6 @@
 import org.springframework.http.HttpMethod;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
-import org.springframework.util.StreamUtils;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -65,16 +60,14 @@ void testHome() {
 	}
 
 	@Test
-	void testCompression() throws Exception {
-		HttpHeaders requestHeaders = new HttpHeaders();
-		requestHeaders.set("Accept-Encoding", "gzip");
-		HttpEntity requestEntity = new HttpEntity<>(requestHeaders);
-		ResponseEntity entity = this.restTemplate.exchange("/", HttpMethod.GET, requestEntity, byte[].class);
+	void testCompression() {
+		// Jetty HttpClient sends Accept-Encoding: gzip by default
+		ResponseEntity entity = this.restTemplate.getForEntity("/", String.class);
 		assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
-		assertThat(entity.getBody()).isNotNull();
-		try (GZIPInputStream inflater = new GZIPInputStream(new ByteArrayInputStream(entity.getBody()))) {
-			assertThat(StreamUtils.copyToString(inflater, StandardCharsets.UTF_8)).isEqualTo("Hello World");
-		}
+		assertThat(entity.getBody()).isEqualTo("Hello World");
+		// Jetty HttpClient decodes gzip reponses automatically
+		// Check that we received a gzip-encoded response
+		assertThat(entity.getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING)).isEqualTo("gzip");
 	}
 
 	@Test

From 7500dab321f0532dd770732c96e4d23763c387ab Mon Sep 17 00:00:00 2001
From: Roman Golovin 
Date: Tue, 13 Jun 2023 20:35:46 +0300
Subject: [PATCH 0097/1215] Support custom token validators for OAuth2

See gh-35874
---
 ...eOAuth2ResourceServerJwkConfiguration.java | 22 ++++--
 .../OAuth2ResourceServerJwtConfiguration.java | 22 ++++--
 ...2ResourceServerAutoConfigurationTests.java | 70 ++++++++++++++++---
 ...2ResourceServerAutoConfigurationTests.java | 62 ++++++++++++++--
 4 files changed, 146 insertions(+), 30 deletions(-)

diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java
index 223fe260033b..e57b824e932c 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java
@@ -61,6 +61,7 @@
  * @author HaiTao Zhang
  * @author Anastasiia Losieva
  * @author Mushtaq Ahmed
+ * @author Roman Golovin
  */
 @Configuration(proxyBeanMethods = false)
 class ReactiveOAuth2ResourceServerJwkConfiguration {
@@ -71,8 +72,12 @@ static class JwtConfiguration {
 
 		private final OAuth2ResourceServerProperties.Jwt properties;
 
-		JwtConfiguration(OAuth2ResourceServerProperties properties) {
+		private final List> customOAuth2TokenValidators;
+
+		JwtConfiguration(OAuth2ResourceServerProperties properties,
+				List> customOAuth2TokenValidators) {
 			this.properties = properties.getJwt();
+			this.customOAuth2TokenValidators = customOAuth2TokenValidators;
 		}
 
 		@Bean
@@ -97,14 +102,17 @@ private void jwsAlgorithms(Set signatureAlgorithms) {
 		}
 
 		private OAuth2TokenValidator getValidators(OAuth2TokenValidator defaultValidator) {
-			List audiences = this.properties.getAudiences();
-			if (CollectionUtils.isEmpty(audiences)) {
-				return defaultValidator;
-			}
 			List> validators = new ArrayList<>();
 			validators.add(defaultValidator);
-			validators.add(new JwtClaimValidator>(JwtClaimNames.AUD,
-					(aud) -> aud != null && !Collections.disjoint(aud, audiences)));
+			validators.addAll(this.customOAuth2TokenValidators);
+			List audiences = this.properties.getAudiences();
+			if (!CollectionUtils.isEmpty(audiences)) {
+				validators.add(new JwtClaimValidator>(JwtClaimNames.AUD,
+						(aud) -> aud != null && !Collections.disjoint(aud, audiences)));
+			}
+			if (validators.size() == 1) {
+				return validators.get(0);
+			}
 			return new DelegatingOAuth2TokenValidator<>(validators);
 		}
 
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java
index b039ecd6b45f..bb32ea0149d1 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java
@@ -62,6 +62,7 @@
  * @author Artsiom Yudovin
  * @author HaiTao Zhang
  * @author Mushtaq Ahmed
+ * @author Roman Golovin
  */
 @Configuration(proxyBeanMethods = false)
 class OAuth2ResourceServerJwtConfiguration {
@@ -72,8 +73,12 @@ static class JwtDecoderConfiguration {
 
 		private final OAuth2ResourceServerProperties.Jwt properties;
 
-		JwtDecoderConfiguration(OAuth2ResourceServerProperties properties) {
+		private final List> customOAuth2TokenValidators;
+
+		JwtDecoderConfiguration(OAuth2ResourceServerProperties properties,
+				List> customOAuth2TokenValidators) {
 			this.properties = properties.getJwt();
+			this.customOAuth2TokenValidators = customOAuth2TokenValidators;
 		}
 
 		@Bean
@@ -97,14 +102,17 @@ private void jwsAlgorithms(Set signatureAlgorithms) {
 		}
 
 		private OAuth2TokenValidator getValidators(OAuth2TokenValidator defaultValidator) {
-			List audiences = this.properties.getAudiences();
-			if (CollectionUtils.isEmpty(audiences)) {
-				return defaultValidator;
-			}
 			List> validators = new ArrayList<>();
 			validators.add(defaultValidator);
-			validators.add(new JwtClaimValidator>(JwtClaimNames.AUD,
-					(aud) -> aud != null && !Collections.disjoint(aud, audiences)));
+			validators.addAll(this.customOAuth2TokenValidators);
+			List audiences = this.properties.getAudiences();
+			if (!CollectionUtils.isEmpty(audiences)) {
+				validators.add(new JwtClaimValidator>(JwtClaimNames.AUD,
+						(aud) -> aud != null && !Collections.disjoint(aud, audiences)));
+			}
+			if (validators.size() == 1) {
+				return validators.get(0);
+			}
 			return new DelegatingOAuth2TokenValidator<>(validators);
 		}
 
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java
index cc5a652d10c2..65c522417f8c 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java
@@ -24,7 +24,9 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -87,6 +89,7 @@
  * @author HaiTao Zhang
  * @author Anastasiia Losieva
  * @author Mushtaq Ahmed
+ * @author Roman Golovin
  */
 class ReactiveOAuth2ResourceServerAutoConfigurationTests {
 
@@ -502,36 +505,73 @@ void autoConfigurationShouldConfigureIssuerAndAudienceJwtValidatorIfPropertyProv
 			.run((context) -> {
 				assertThat(context).hasSingleBean(ReactiveJwtDecoder.class);
 				ReactiveJwtDecoder reactiveJwtDecoder = context.getBean(ReactiveJwtDecoder.class);
-				validate(issuerUri, reactiveJwtDecoder);
+				validate(issuerUri, reactiveJwtDecoder, null);
 			});
 	}
 
 	@SuppressWarnings("unchecked")
-	private void validate(String issuerUri, ReactiveJwtDecoder jwtDecoder) throws MalformedURLException {
+	@Test
+	void autoConfigurationShouldConfigureAudienceAndCustomValidatorsIfPropertyProvidedAndIssuerUri() throws Exception {
+		this.server = new MockWebServer();
+		this.server.start();
+		String path = "test";
+		String issuer = this.server.url(path).toString();
+		String cleanIssuerPath = cleanIssuerPath(issuer);
+		setupMockResponse(cleanIssuerPath);
+		String issuerUri = "http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path;
+		this.contextRunner.withPropertyValues(
+				"spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
+				"spring.security.oauth2.resourceserver.jwt.issuer-uri=" + issuerUri,
+				"spring.security.oauth2.resourceserver.jwt.audiences=https://test-audience.com,https://test-audience1.com")
+			.withUserConfiguration(CustomTokenValidatorsConfig.class)
+			.run((context) -> {
+				assertThat(context).hasSingleBean(ReactiveJwtDecoder.class);
+				ReactiveJwtDecoder reactiveJwtDecoder = context.getBean(ReactiveJwtDecoder.class);
+				assertThat(context).hasBean("customJwtClaimValidator");
+				OAuth2TokenValidator customValidator = (OAuth2TokenValidator) context
+					.getBean("customJwtClaimValidator");
+				validate(issuerUri, reactiveJwtDecoder, customValidator);
+			});
+	}
+
+	@SuppressWarnings("unchecked")
+	private void validate(String issuerUri, ReactiveJwtDecoder jwtDecoder, OAuth2TokenValidator customValidator)
+			throws MalformedURLException {
 		DelegatingOAuth2TokenValidator jwtValidator = (DelegatingOAuth2TokenValidator) ReflectionTestUtils
 			.getField(jwtDecoder, "jwtValidator");
 		Jwt.Builder builder = jwt().claim("aud", Collections.singletonList("https://test-audience.com"));
 		if (issuerUri != null) {
 			builder.claim("iss", new URL(issuerUri));
 		}
+		if (customValidator != null) {
+			builder.claim("custom_claim", "custom_claim_value");
+		}
 		Jwt jwt = builder.build();
 		assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse();
 		Collection> delegates = (Collection>) ReflectionTestUtils
 			.getField(jwtValidator, "tokenValidators");
-		validateDelegates(issuerUri, delegates);
+		validateDelegates(issuerUri, delegates, customValidator);
 	}
 
-	@SuppressWarnings("unchecked")
-	private void validateDelegates(String issuerUri, Collection> delegates) {
+	private void validateDelegates(String issuerUri, Collection> delegates,
+			OAuth2TokenValidator customValidator) {
 		assertThat(delegates).hasAtLeastOneElementOfType(JwtClaimValidator.class);
 		OAuth2TokenValidator delegatingValidator = delegates.stream()
 			.filter((v) -> v instanceof DelegatingOAuth2TokenValidator)
 			.findFirst()
 			.get();
-		Collection> nestedDelegates = (Collection>) ReflectionTestUtils
-			.getField(delegatingValidator, "tokenValidators");
 		if (issuerUri != null) {
-			assertThat(nestedDelegates).hasAtLeastOneElementOfType(JwtIssuerValidator.class);
+			assertThat(delegatingValidator).extracting("tokenValidators")
+				.asInstanceOf(InstanceOfAssertFactories.collection(OAuth2TokenValidator.class))
+				.hasAtLeastOneElementOfType(JwtIssuerValidator.class);
+		}
+		List> claimValidators = delegates.stream()
+			.filter((d) -> d instanceof JwtClaimValidator)
+			.collect(Collectors.toList());
+		assertThat(claimValidators).anyMatch((v) -> "aud".equals(ReflectionTestUtils.getField(v, "claim")));
+		if (customValidator != null) {
+			assertThat(claimValidators)
+				.anyMatch((v) -> "custom_claim".equals(ReflectionTestUtils.getField(v, "claim")));
 		}
 	}
 
@@ -552,7 +592,7 @@ void autoConfigurationShouldConfigureAudienceValidatorIfPropertyProvidedAndIssue
 				Mono jwtDecoderSupplier = (Mono) ReflectionTestUtils
 					.getField(supplierJwtDecoderBean, "jwtDecoderMono");
 				ReactiveJwtDecoder jwtDecoder = jwtDecoderSupplier.block();
-				validate(issuerUri, jwtDecoder);
+				validate(issuerUri, jwtDecoder, null);
 			});
 	}
 
@@ -570,7 +610,7 @@ void autoConfigurationShouldConfigureAudienceValidatorIfPropertyProvidedAndPubli
 			.run((context) -> {
 				assertThat(context).hasSingleBean(ReactiveJwtDecoder.class);
 				ReactiveJwtDecoder jwtDecoder = context.getBean(ReactiveJwtDecoder.class);
-				validate(null, jwtDecoder);
+				validate(null, jwtDecoder, null);
 			});
 	}
 
@@ -740,4 +780,14 @@ SecurityWebFilterChain testSpringSecurityFilterChain(ServerHttpSecurity http) {
 
 	}
 
+	@Configuration(proxyBeanMethods = false)
+	static class CustomTokenValidatorsConfig {
+
+		@Bean
+		JwtClaimValidator customJwtClaimValidator() {
+			return new JwtClaimValidator<>("custom_claim", "custom_claim_value"::equals);
+		}
+
+	}
+
 }
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java
index 878415ec9f9f..42ec6ca08d5f 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java
@@ -25,6 +25,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.function.Supplier;
+import java.util.stream.Collectors;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -80,6 +81,7 @@
  * @author Artsiom Yudovin
  * @author HaiTao Zhang
  * @author Mushtaq Ahmed
+ * @author Roman Golovin
  */
 class OAuth2ResourceServerAutoConfigurationTests {
 
@@ -515,7 +517,7 @@ void autoConfigurationShouldConfigureAudienceAndIssuerJwtValidatorIfPropertyProv
 			.run((context) -> {
 				assertThat(context).hasSingleBean(JwtDecoder.class);
 				JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class);
-				validate(issuerUri, jwtDecoder);
+				validate(issuerUri, jwtDecoder, null);
 			});
 	}
 
@@ -536,26 +538,56 @@ void autoConfigurationShouldConfigureAudienceValidatorIfPropertyProvidedAndIssue
 				Supplier jwtDecoderSupplier = (Supplier) ReflectionTestUtils
 					.getField(supplierJwtDecoderBean, "delegate");
 				JwtDecoder jwtDecoder = jwtDecoderSupplier.get();
-				validate(issuerUri, jwtDecoder);
+				validate(issuerUri, jwtDecoder, null);
 			});
 	}
 
 	@SuppressWarnings("unchecked")
-	private void validate(String issuerUri, JwtDecoder jwtDecoder) throws MalformedURLException {
+	@Test
+	void autoConfigurationShouldConfigureAudienceAndCustomValidatorsIfPropertyProvidedAndIssuerUri() throws Exception {
+		this.server = new MockWebServer();
+		this.server.start();
+		String path = "test";
+		String issuer = this.server.url(path).toString();
+		String cleanIssuerPath = cleanIssuerPath(issuer);
+		setupMockResponse(cleanIssuerPath);
+		String issuerUri = "http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path;
+		this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=" + issuerUri,
+				"spring.security.oauth2.resourceserver.jwt.audiences=https://test-audience.com,https://test-audience1.com")
+			.withUserConfiguration(CustomTokenValidatorsConfig.class)
+			.run((context) -> {
+				SupplierJwtDecoder supplierJwtDecoderBean = context.getBean(SupplierJwtDecoder.class);
+				Supplier jwtDecoderSupplier = (Supplier) ReflectionTestUtils
+					.getField(supplierJwtDecoderBean, "delegate");
+				JwtDecoder jwtDecoder = jwtDecoderSupplier.get();
+				assertThat(context).hasBean("customJwtClaimValidator");
+				OAuth2TokenValidator customValidator = (OAuth2TokenValidator) context
+					.getBean("customJwtClaimValidator");
+				validate(issuerUri, jwtDecoder, customValidator);
+			});
+	}
+
+	@SuppressWarnings("unchecked")
+	private void validate(String issuerUri, JwtDecoder jwtDecoder, OAuth2TokenValidator customValidator)
+			throws MalformedURLException {
 		DelegatingOAuth2TokenValidator jwtValidator = (DelegatingOAuth2TokenValidator) ReflectionTestUtils
 			.getField(jwtDecoder, "jwtValidator");
 		Jwt.Builder builder = jwt().claim("aud", Collections.singletonList("https://test-audience.com"));
 		if (issuerUri != null) {
 			builder.claim("iss", new URL(issuerUri));
 		}
+		if (customValidator != null) {
+			builder.claim("custom_claim", "custom_claim_value");
+		}
 		Jwt jwt = builder.build();
 		assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse();
 		Collection> delegates = (Collection>) ReflectionTestUtils
 			.getField(jwtValidator, "tokenValidators");
-		validateDelegates(issuerUri, delegates);
+		validateDelegates(issuerUri, delegates, customValidator);
 	}
 
-	private void validateDelegates(String issuerUri, Collection> delegates) {
+	private void validateDelegates(String issuerUri, Collection> delegates,
+			OAuth2TokenValidator customValidator) {
 		assertThat(delegates).hasAtLeastOneElementOfType(JwtClaimValidator.class);
 		OAuth2TokenValidator delegatingValidator = delegates.stream()
 			.filter((v) -> v instanceof DelegatingOAuth2TokenValidator)
@@ -566,6 +598,14 @@ private void validateDelegates(String issuerUri, Collection> claimValidators = delegates.stream()
+			.filter((d) -> d instanceof JwtClaimValidator)
+			.collect(Collectors.toList());
+		assertThat(claimValidators).anyMatch((v) -> "aud".equals(ReflectionTestUtils.getField(v, "claim")));
+		if (customValidator != null) {
+			assertThat(claimValidators)
+				.anyMatch((v) -> "custom_claim".equals(ReflectionTestUtils.getField(v, "claim")));
+		}
 	}
 
 	@Test
@@ -582,7 +622,7 @@ void autoConfigurationShouldConfigureAudienceValidatorIfPropertyProvidedAndPubli
 			.run((context) -> {
 				assertThat(context).hasSingleBean(JwtDecoder.class);
 				JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class);
-				validate(null, jwtDecoder);
+				validate(null, jwtDecoder, null);
 			});
 	}
 
@@ -745,4 +785,14 @@ SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception
 
 	}
 
+	@Configuration(proxyBeanMethods = false)
+	static class CustomTokenValidatorsConfig {
+
+		@Bean
+		JwtClaimValidator customJwtClaimValidator() {
+			return new JwtClaimValidator<>("custom_claim", "custom_claim_value"::equals);
+		}
+
+	}
+
 }

From 4feaa28fd1ce83e251aefb1aac1a42272054b9bc Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Wed, 5 Jul 2023 14:01:08 +0100
Subject: [PATCH 0098/1215] Polish "Support custom token validators for OAuth2"

See gh-35874
---
 ...eOAuth2ResourceServerJwkConfiguration.java |  16 +-
 .../OAuth2ResourceServerJwtConfiguration.java |  16 +-
 ...2ResourceServerAutoConfigurationTests.java | 187 ++++++++++--------
 ...2ResourceServerAutoConfigurationTests.java | 126 ++++++------
 4 files changed, 181 insertions(+), 164 deletions(-)

diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java
index e57b824e932c..5f5cba160eaa 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java
@@ -72,12 +72,12 @@ static class JwtConfiguration {
 
 		private final OAuth2ResourceServerProperties.Jwt properties;
 
-		private final List> customOAuth2TokenValidators;
+		private final List> additionalValidators;
 
 		JwtConfiguration(OAuth2ResourceServerProperties properties,
-				List> customOAuth2TokenValidators) {
+				ObjectProvider> additionalValidators) {
 			this.properties = properties.getJwt();
-			this.customOAuth2TokenValidators = customOAuth2TokenValidators;
+			this.additionalValidators = additionalValidators.orderedStream().toList();
 		}
 
 		@Bean
@@ -102,17 +102,17 @@ private void jwsAlgorithms(Set signatureAlgorithms) {
 		}
 
 		private OAuth2TokenValidator getValidators(OAuth2TokenValidator defaultValidator) {
+			List audiences = this.properties.getAudiences();
+			if (CollectionUtils.isEmpty(audiences) && this.additionalValidators.isEmpty()) {
+				return defaultValidator;
+			}
 			List> validators = new ArrayList<>();
 			validators.add(defaultValidator);
-			validators.addAll(this.customOAuth2TokenValidators);
-			List audiences = this.properties.getAudiences();
 			if (!CollectionUtils.isEmpty(audiences)) {
 				validators.add(new JwtClaimValidator>(JwtClaimNames.AUD,
 						(aud) -> aud != null && !Collections.disjoint(aud, audiences)));
 			}
-			if (validators.size() == 1) {
-				return validators.get(0);
-			}
+			validators.addAll(this.additionalValidators);
 			return new DelegatingOAuth2TokenValidator<>(validators);
 		}
 
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java
index bb32ea0149d1..84bafab99db0 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java
@@ -73,12 +73,12 @@ static class JwtDecoderConfiguration {
 
 		private final OAuth2ResourceServerProperties.Jwt properties;
 
-		private final List> customOAuth2TokenValidators;
+		private final List> additionalValidators;
 
 		JwtDecoderConfiguration(OAuth2ResourceServerProperties properties,
-				List> customOAuth2TokenValidators) {
+				ObjectProvider> additionalValidators) {
 			this.properties = properties.getJwt();
-			this.customOAuth2TokenValidators = customOAuth2TokenValidators;
+			this.additionalValidators = additionalValidators.orderedStream().toList();
 		}
 
 		@Bean
@@ -102,17 +102,17 @@ private void jwsAlgorithms(Set signatureAlgorithms) {
 		}
 
 		private OAuth2TokenValidator getValidators(OAuth2TokenValidator defaultValidator) {
+			List audiences = this.properties.getAudiences();
+			if (CollectionUtils.isEmpty(audiences) && this.additionalValidators.isEmpty()) {
+				return defaultValidator;
+			}
 			List> validators = new ArrayList<>();
 			validators.add(defaultValidator);
-			validators.addAll(this.customOAuth2TokenValidators);
-			List audiences = this.properties.getAudiences();
 			if (!CollectionUtils.isEmpty(audiences)) {
 				validators.add(new JwtClaimValidator>(JwtClaimNames.AUD,
 						(aud) -> aud != null && !Collections.disjoint(aud, audiences)));
 			}
-			if (validators.size() == 1) {
-				return validators.get(0);
-			}
+			validators.addAll(this.additionalValidators);
 			return new DelegatingOAuth2TokenValidator<>(validators);
 		}
 
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java
index 65c522417f8c..e8165ee18916 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java
@@ -17,16 +17,17 @@
 package org.springframework.boot.autoconfigure.security.oauth2.resource.reactive;
 
 import java.io.IOException;
-import java.net.MalformedURLException;
+import java.net.URI;
 import java.net.URL;
 import java.time.Duration;
 import java.time.Instant;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
+import java.util.function.Consumer;
 import java.util.stream.Stream;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -35,6 +36,7 @@
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
 import org.assertj.core.api.InstanceOfAssertFactories;
+import org.assertj.core.api.ThrowingConsumer;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.InOrder;
@@ -441,7 +443,6 @@ void autoConfigurationWhenIntrospectionUriAvailableShouldBeConditionalOnClass()
 			.run((context) -> assertThat(context).doesNotHaveBean(ReactiveOpaqueTokenIntrospector.class));
 	}
 
-	@SuppressWarnings("unchecked")
 	@Test
 	void autoConfigurationShouldConfigureResourceServerUsingJwkSetUriAndIssuerUri() throws Exception {
 		this.server = new MockWebServer();
@@ -457,15 +458,11 @@ void autoConfigurationShouldConfigureResourceServerUsingJwkSetUriAndIssuerUri()
 			.run((context) -> {
 				assertThat(context).hasSingleBean(ReactiveJwtDecoder.class);
 				ReactiveJwtDecoder reactiveJwtDecoder = context.getBean(ReactiveJwtDecoder.class);
-				DelegatingOAuth2TokenValidator jwtValidator = (DelegatingOAuth2TokenValidator) ReflectionTestUtils
-					.getField(reactiveJwtDecoder, "jwtValidator");
-				Collection> tokenValidators = (Collection>) ReflectionTestUtils
-					.getField(jwtValidator, "tokenValidators");
-				assertThat(tokenValidators).hasAtLeastOneElementOfType(JwtIssuerValidator.class);
+				validate(jwt().claim("iss", issuer), reactiveJwtDecoder,
+						(validators) -> assertThat(validators).hasAtLeastOneElementOfType(JwtIssuerValidator.class));
 			});
 	}
 
-	@SuppressWarnings("unchecked")
 	@Test
 	void autoConfigurationShouldNotConfigureIssuerUriAndAudienceJwtValidatorIfPropertyNotConfigured() throws Exception {
 		this.server = new MockWebServer();
@@ -479,13 +476,8 @@ void autoConfigurationShouldNotConfigureIssuerUriAndAudienceJwtValidatorIfProper
 			.run((context) -> {
 				assertThat(context).hasSingleBean(ReactiveJwtDecoder.class);
 				ReactiveJwtDecoder reactiveJwtDecoder = context.getBean(ReactiveJwtDecoder.class);
-				DelegatingOAuth2TokenValidator jwtValidator = (DelegatingOAuth2TokenValidator) ReflectionTestUtils
-					.getField(reactiveJwtDecoder, "jwtValidator");
-				Collection> tokenValidators = (Collection>) ReflectionTestUtils
-					.getField(jwtValidator, "tokenValidators");
-				assertThat(tokenValidators).hasExactlyElementsOfTypes(JwtTimestampValidator.class);
-				assertThat(tokenValidators).doesNotHaveAnyElementsOfTypes(JwtClaimValidator.class);
-				assertThat(tokenValidators).doesNotHaveAnyElementsOfTypes(JwtIssuerValidator.class);
+				validate(jwt(), reactiveJwtDecoder, (validators) -> assertThat(validators).singleElement()
+					.isInstanceOf(JwtTimestampValidator.class));
 			});
 	}
 
@@ -505,13 +497,18 @@ void autoConfigurationShouldConfigureIssuerAndAudienceJwtValidatorIfPropertyProv
 			.run((context) -> {
 				assertThat(context).hasSingleBean(ReactiveJwtDecoder.class);
 				ReactiveJwtDecoder reactiveJwtDecoder = context.getBean(ReactiveJwtDecoder.class);
-				validate(issuerUri, reactiveJwtDecoder, null);
+				validate(
+						jwt().claim("iss", URI.create(issuerUri).toURL())
+							.claim("aud", List.of("https://test-audience.com")),
+						reactiveJwtDecoder,
+						(validators) -> assertThat(validators).hasAtLeastOneElementOfType(JwtIssuerValidator.class)
+							.satisfiesOnlyOnce(audClaimValidator()));
 			});
 	}
 
 	@SuppressWarnings("unchecked")
 	@Test
-	void autoConfigurationShouldConfigureAudienceAndCustomValidatorsIfPropertyProvidedAndIssuerUri() throws Exception {
+	void autoConfigurationShouldConfigureAudienceValidatorIfPropertyProvidedAndIssuerUri() throws Exception {
 		this.server = new MockWebServer();
 		this.server.start();
 		String path = "test";
@@ -519,98 +516,63 @@ void autoConfigurationShouldConfigureAudienceAndCustomValidatorsIfPropertyProvid
 		String cleanIssuerPath = cleanIssuerPath(issuer);
 		setupMockResponse(cleanIssuerPath);
 		String issuerUri = "http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path;
-		this.contextRunner.withPropertyValues(
-				"spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
-				"spring.security.oauth2.resourceserver.jwt.issuer-uri=" + issuerUri,
+		this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=" + issuerUri,
 				"spring.security.oauth2.resourceserver.jwt.audiences=https://test-audience.com,https://test-audience1.com")
-			.withUserConfiguration(CustomTokenValidatorsConfig.class)
 			.run((context) -> {
-				assertThat(context).hasSingleBean(ReactiveJwtDecoder.class);
-				ReactiveJwtDecoder reactiveJwtDecoder = context.getBean(ReactiveJwtDecoder.class);
-				assertThat(context).hasBean("customJwtClaimValidator");
-				OAuth2TokenValidator customValidator = (OAuth2TokenValidator) context
-					.getBean("customJwtClaimValidator");
-				validate(issuerUri, reactiveJwtDecoder, customValidator);
+				SupplierReactiveJwtDecoder supplierJwtDecoderBean = context.getBean(SupplierReactiveJwtDecoder.class);
+				Mono jwtDecoderSupplier = (Mono) ReflectionTestUtils
+					.getField(supplierJwtDecoderBean, "jwtDecoderMono");
+				ReactiveJwtDecoder jwtDecoder = jwtDecoderSupplier.block();
+				validate(
+						jwt().claim("iss", URI.create(issuerUri).toURL())
+							.claim("aud", List.of("https://test-audience.com")),
+						jwtDecoder,
+						(validators) -> assertThat(validators).hasAtLeastOneElementOfType(JwtIssuerValidator.class)
+							.satisfiesOnlyOnce(audClaimValidator()));
 			});
 	}
 
-	@SuppressWarnings("unchecked")
-	private void validate(String issuerUri, ReactiveJwtDecoder jwtDecoder, OAuth2TokenValidator customValidator)
-			throws MalformedURLException {
-		DelegatingOAuth2TokenValidator jwtValidator = (DelegatingOAuth2TokenValidator) ReflectionTestUtils
-			.getField(jwtDecoder, "jwtValidator");
-		Jwt.Builder builder = jwt().claim("aud", Collections.singletonList("https://test-audience.com"));
-		if (issuerUri != null) {
-			builder.claim("iss", new URL(issuerUri));
-		}
-		if (customValidator != null) {
-			builder.claim("custom_claim", "custom_claim_value");
-		}
-		Jwt jwt = builder.build();
-		assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse();
-		Collection> delegates = (Collection>) ReflectionTestUtils
-			.getField(jwtValidator, "tokenValidators");
-		validateDelegates(issuerUri, delegates, customValidator);
-	}
-
-	private void validateDelegates(String issuerUri, Collection> delegates,
-			OAuth2TokenValidator customValidator) {
-		assertThat(delegates).hasAtLeastOneElementOfType(JwtClaimValidator.class);
-		OAuth2TokenValidator delegatingValidator = delegates.stream()
-			.filter((v) -> v instanceof DelegatingOAuth2TokenValidator)
-			.findFirst()
-			.get();
-		if (issuerUri != null) {
-			assertThat(delegatingValidator).extracting("tokenValidators")
-				.asInstanceOf(InstanceOfAssertFactories.collection(OAuth2TokenValidator.class))
-				.hasAtLeastOneElementOfType(JwtIssuerValidator.class);
-		}
-		List> claimValidators = delegates.stream()
-			.filter((d) -> d instanceof JwtClaimValidator)
-			.collect(Collectors.toList());
-		assertThat(claimValidators).anyMatch((v) -> "aud".equals(ReflectionTestUtils.getField(v, "claim")));
-		if (customValidator != null) {
-			assertThat(claimValidators)
-				.anyMatch((v) -> "custom_claim".equals(ReflectionTestUtils.getField(v, "claim")));
-		}
-	}
-
-	@SuppressWarnings("unchecked")
 	@Test
-	void autoConfigurationShouldConfigureAudienceValidatorIfPropertyProvidedAndIssuerUri() throws Exception {
+	void autoConfigurationShouldConfigureAudienceValidatorIfPropertyProvidedAndPublicKey() throws Exception {
 		this.server = new MockWebServer();
 		this.server.start();
 		String path = "test";
 		String issuer = this.server.url(path).toString();
 		String cleanIssuerPath = cleanIssuerPath(issuer);
 		setupMockResponse(cleanIssuerPath);
-		String issuerUri = "http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path;
-		this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=" + issuerUri,
+		this.contextRunner.withPropertyValues(
+				"spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location",
 				"spring.security.oauth2.resourceserver.jwt.audiences=https://test-audience.com,https://test-audience1.com")
 			.run((context) -> {
-				SupplierReactiveJwtDecoder supplierJwtDecoderBean = context.getBean(SupplierReactiveJwtDecoder.class);
-				Mono jwtDecoderSupplier = (Mono) ReflectionTestUtils
-					.getField(supplierJwtDecoderBean, "jwtDecoderMono");
-				ReactiveJwtDecoder jwtDecoder = jwtDecoderSupplier.block();
-				validate(issuerUri, jwtDecoder, null);
+				assertThat(context).hasSingleBean(ReactiveJwtDecoder.class);
+				ReactiveJwtDecoder jwtDecoder = context.getBean(ReactiveJwtDecoder.class);
+				validate(jwt().claim("aud", List.of("https://test-audience.com")), jwtDecoder,
+						(validators) -> assertThat(validators).satisfiesOnlyOnce(audClaimValidator()));
 			});
 	}
 
+	@SuppressWarnings("unchecked")
 	@Test
-	void autoConfigurationShouldConfigureAudienceValidatorIfPropertyProvidedAndPublicKey() throws Exception {
+	void autoConfigurationShouldConfigureCustomValidators() throws Exception {
 		this.server = new MockWebServer();
 		this.server.start();
 		String path = "test";
 		String issuer = this.server.url(path).toString();
 		String cleanIssuerPath = cleanIssuerPath(issuer);
 		setupMockResponse(cleanIssuerPath);
-		this.contextRunner.withPropertyValues(
-				"spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location",
-				"spring.security.oauth2.resourceserver.jwt.audiences=https://test-audience.com,https://test-audience1.com")
+		String issuerUri = "http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path;
+		this.contextRunner
+			.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
+					"spring.security.oauth2.resourceserver.jwt.issuer-uri=" + issuerUri)
+			.withUserConfiguration(CustomJwtClaimValidatorConfig.class)
 			.run((context) -> {
 				assertThat(context).hasSingleBean(ReactiveJwtDecoder.class);
-				ReactiveJwtDecoder jwtDecoder = context.getBean(ReactiveJwtDecoder.class);
-				validate(null, jwtDecoder, null);
+				ReactiveJwtDecoder reactiveJwtDecoder = context.getBean(ReactiveJwtDecoder.class);
+				OAuth2TokenValidator customValidator = (OAuth2TokenValidator) context
+					.getBean("customJwtClaimValidator");
+				validate(jwt().claim("iss", URI.create(issuerUri).toURL()).claim("custom_claim", "custom_claim_value"),
+						reactiveJwtDecoder, (validators) -> assertThat(validators).contains(customValidator)
+							.hasAtLeastOneElementOfType(JwtIssuerValidator.class));
 			});
 	}
 
@@ -640,6 +602,30 @@ void audienceValidatorWhenAudienceInvalid() throws Exception {
 			});
 	}
 
+	@SuppressWarnings("unchecked")
+	@Test
+	void customValidatorWhenInvalid() throws Exception {
+		this.server = new MockWebServer();
+		this.server.start();
+		String path = "test";
+		String issuer = this.server.url(path).toString();
+		String cleanIssuerPath = cleanIssuerPath(issuer);
+		setupMockResponse(cleanIssuerPath);
+		String issuerUri = "http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path;
+		this.contextRunner
+			.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com",
+					"spring.security.oauth2.resourceserver.jwt.issuer-uri=" + issuerUri)
+			.withUserConfiguration(CustomJwtClaimValidatorConfig.class)
+			.run((context) -> {
+				assertThat(context).hasSingleBean(ReactiveJwtDecoder.class);
+				ReactiveJwtDecoder jwtDecoder = context.getBean(ReactiveJwtDecoder.class);
+				DelegatingOAuth2TokenValidator jwtValidator = (DelegatingOAuth2TokenValidator) ReflectionTestUtils
+					.getField(jwtDecoder, "jwtValidator");
+				Jwt jwt = jwt().claim("iss", new URL(issuerUri)).claim("custom_claim", "invalid_value").build();
+				assertThat(jwtValidator.validate(jwt).hasErrors()).isTrue();
+			});
+	}
+
 	private void assertFilterConfiguredWithJwtAuthenticationManager(AssertableReactiveWebApplicationContext context) {
 		MatcherSecurityWebFilterChain filterChain = (MatcherSecurityWebFilterChain) context
 			.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN);
@@ -723,6 +709,37 @@ static Jwt.Builder jwt() {
 			.subject("mock-test-subject");
 	}
 
+	@SuppressWarnings("unchecked")
+	private void validate(Jwt.Builder builder, ReactiveJwtDecoder jwtDecoder,
+			ThrowingConsumer>> validatorsConsumer) {
+		DelegatingOAuth2TokenValidator jwtValidator = (DelegatingOAuth2TokenValidator) ReflectionTestUtils
+			.getField(jwtDecoder, "jwtValidator");
+		assertThat(jwtValidator.validate(builder.build()).hasErrors()).isFalse();
+		validatorsConsumer.accept(extractValidators(jwtValidator));
+	}
+
+	@SuppressWarnings("unchecked")
+	private List> extractValidators(DelegatingOAuth2TokenValidator delegatingValidator) {
+		Collection> delegates = (Collection>) ReflectionTestUtils
+			.getField(delegatingValidator, "tokenValidators");
+		List> extracted = new ArrayList<>();
+		for (OAuth2TokenValidator delegate : delegates) {
+			if (delegate instanceof DelegatingOAuth2TokenValidator delegatingDelegate) {
+				extracted.addAll(extractValidators(delegatingDelegate));
+			}
+			else {
+				extracted.add(delegate);
+			}
+		}
+		return extracted;
+	}
+
+	private Consumer> audClaimValidator() {
+		return (validator) -> assertThat(validator).isInstanceOf(JwtClaimValidator.class)
+			.extracting("claim")
+			.isEqualTo("aud");
+	}
+
 	@EnableWebFluxSecurity
 	static class TestConfig {
 
@@ -781,7 +798,7 @@ SecurityWebFilterChain testSpringSecurityFilterChain(ServerHttpSecurity http) {
 	}
 
 	@Configuration(proxyBeanMethods = false)
-	static class CustomTokenValidatorsConfig {
+	static class CustomJwtClaimValidatorConfig {
 
 		@Bean
 		JwtClaimValidator customJwtClaimValidator() {
diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java
index 42ec6ca08d5f..b7aa1a5ec67b 100644
--- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java
+++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java
@@ -16,16 +16,17 @@
 
 package org.springframework.boot.autoconfigure.security.oauth2.resource.servlet;
 
-import java.net.MalformedURLException;
+import java.net.URI;
 import java.net.URL;
 import java.time.Instant;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Consumer;
 import java.util.function.Supplier;
-import java.util.stream.Collectors;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -34,6 +35,7 @@
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
 import org.assertj.core.api.InstanceOfAssertFactories;
+import org.assertj.core.api.ThrowingConsumer;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.InOrder;
@@ -192,8 +194,8 @@ void autoConfigurationUsingPublicKeyValueWithMultipleJwsAlgorithmsShouldFail() {
 			});
 	}
 
-	@Test
 	@SuppressWarnings("unchecked")
+	@Test
 	void autoConfigurationShouldConfigureResourceServerUsingOidcIssuerUri() throws Exception {
 		this.server = new MockWebServer();
 		this.server.start();
@@ -217,8 +219,8 @@ void autoConfigurationShouldConfigureResourceServerUsingOidcIssuerUri() throws E
 		assertThat(this.server.getRequestCount()).isEqualTo(2);
 	}
 
-	@Test
 	@SuppressWarnings("unchecked")
+	@Test
 	void autoConfigurationShouldConfigureResourceServerUsingOidcRfc8414IssuerUri() throws Exception {
 		this.server = new MockWebServer();
 		this.server.start();
@@ -242,8 +244,8 @@ void autoConfigurationShouldConfigureResourceServerUsingOidcRfc8414IssuerUri() t
 		assertThat(this.server.getRequestCount()).isEqualTo(3);
 	}
 
-	@Test
 	@SuppressWarnings("unchecked")
+	@Test
 	void autoConfigurationShouldConfigureResourceServerUsingOAuthIssuerUri() throws Exception {
 		this.server = new MockWebServer();
 		this.server.start();
@@ -474,9 +476,8 @@ void autoConfigurationShouldConfigureResourceServerUsingJwkSetUriAndIssuerUri()
 			.run((context) -> {
 				assertThat(context).hasSingleBean(JwtDecoder.class);
 				JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class);
-				assertThat(jwtDecoder).extracting("jwtValidator.tokenValidators")
-					.asInstanceOf(InstanceOfAssertFactories.collection(OAuth2TokenValidator.class))
-					.hasAtLeastOneElementOfType(JwtIssuerValidator.class);
+				validate(jwt().claim("iss", issuer), jwtDecoder,
+						(validators) -> assertThat(validators).hasAtLeastOneElementOfType(JwtIssuerValidator.class));
 			});
 	}
 
@@ -493,11 +494,8 @@ void autoConfigurationShouldNotConfigureIssuerUriAndAudienceJwtValidatorIfProper
 			.run((context) -> {
 				assertThat(context).hasSingleBean(JwtDecoder.class);
 				JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class);
-				assertThat(jwtDecoder).extracting("jwtValidator.tokenValidators")
-					.asInstanceOf(InstanceOfAssertFactories.collection(OAuth2TokenValidator.class))
-					.hasExactlyElementsOfTypes(JwtTimestampValidator.class)
-					.doesNotHaveAnyElementsOfTypes(JwtClaimValidator.class)
-					.doesNotHaveAnyElementsOfTypes(JwtIssuerValidator.class);
+				validate(jwt(), jwtDecoder, (validators) -> assertThat(validators).singleElement()
+					.isInstanceOf(JwtTimestampValidator.class));
 			});
 	}
 
@@ -517,7 +515,12 @@ void autoConfigurationShouldConfigureAudienceAndIssuerJwtValidatorIfPropertyProv
 			.run((context) -> {
 				assertThat(context).hasSingleBean(JwtDecoder.class);
 				JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class);
-				validate(issuerUri, jwtDecoder, null);
+				validate(
+						jwt().claim("iss", URI.create(issuerUri).toURL())
+							.claim("aud", List.of("https://test-audience.com")),
+						jwtDecoder,
+						(validators) -> assertThat(validators).hasAtLeastOneElementOfType(JwtIssuerValidator.class)
+							.satisfiesOnlyOnce(audClaimValidator()));
 			});
 	}
 
@@ -538,13 +541,18 @@ void autoConfigurationShouldConfigureAudienceValidatorIfPropertyProvidedAndIssue
 				Supplier jwtDecoderSupplier = (Supplier) ReflectionTestUtils
 					.getField(supplierJwtDecoderBean, "delegate");
 				JwtDecoder jwtDecoder = jwtDecoderSupplier.get();
-				validate(issuerUri, jwtDecoder, null);
+				validate(
+						jwt().claim("iss", URI.create(issuerUri).toURL())
+							.claim("aud", List.of("https://test-audience.com")),
+						jwtDecoder,
+						(validators) -> assertThat(validators).hasAtLeastOneElementOfType(JwtIssuerValidator.class)
+							.satisfiesOnlyOnce(audClaimValidator()));
 			});
 	}
 
 	@SuppressWarnings("unchecked")
 	@Test
-	void autoConfigurationShouldConfigureAudienceAndCustomValidatorsIfPropertyProvidedAndIssuerUri() throws Exception {
+	void autoConfigurationShouldConfigureCustomValidators() throws Exception {
 		this.server = new MockWebServer();
 		this.server.start();
 		String path = "test";
@@ -552,9 +560,8 @@ void autoConfigurationShouldConfigureAudienceAndCustomValidatorsIfPropertyProvid
 		String cleanIssuerPath = cleanIssuerPath(issuer);
 		setupMockResponse(cleanIssuerPath);
 		String issuerUri = "http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path;
-		this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=" + issuerUri,
-				"spring.security.oauth2.resourceserver.jwt.audiences=https://test-audience.com,https://test-audience1.com")
-			.withUserConfiguration(CustomTokenValidatorsConfig.class)
+		this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=" + issuerUri)
+			.withUserConfiguration(CustomJwtClaimValidatorConfig.class)
 			.run((context) -> {
 				SupplierJwtDecoder supplierJwtDecoderBean = context.getBean(SupplierJwtDecoder.class);
 				Supplier jwtDecoderSupplier = (Supplier) ReflectionTestUtils
@@ -563,51 +570,12 @@ void autoConfigurationShouldConfigureAudienceAndCustomValidatorsIfPropertyProvid
 				assertThat(context).hasBean("customJwtClaimValidator");
 				OAuth2TokenValidator customValidator = (OAuth2TokenValidator) context
 					.getBean("customJwtClaimValidator");
-				validate(issuerUri, jwtDecoder, customValidator);
+				validate(jwt().claim("iss", URI.create(issuerUri).toURL()).claim("custom_claim", "custom_claim_value"),
+						jwtDecoder, (validators) -> assertThat(validators).contains(customValidator)
+							.hasAtLeastOneElementOfType(JwtIssuerValidator.class));
 			});
 	}
 
-	@SuppressWarnings("unchecked")
-	private void validate(String issuerUri, JwtDecoder jwtDecoder, OAuth2TokenValidator customValidator)
-			throws MalformedURLException {
-		DelegatingOAuth2TokenValidator jwtValidator = (DelegatingOAuth2TokenValidator) ReflectionTestUtils
-			.getField(jwtDecoder, "jwtValidator");
-		Jwt.Builder builder = jwt().claim("aud", Collections.singletonList("https://test-audience.com"));
-		if (issuerUri != null) {
-			builder.claim("iss", new URL(issuerUri));
-		}
-		if (customValidator != null) {
-			builder.claim("custom_claim", "custom_claim_value");
-		}
-		Jwt jwt = builder.build();
-		assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse();
-		Collection> delegates = (Collection>) ReflectionTestUtils
-			.getField(jwtValidator, "tokenValidators");
-		validateDelegates(issuerUri, delegates, customValidator);
-	}
-
-	private void validateDelegates(String issuerUri, Collection> delegates,
-			OAuth2TokenValidator customValidator) {
-		assertThat(delegates).hasAtLeastOneElementOfType(JwtClaimValidator.class);
-		OAuth2TokenValidator delegatingValidator = delegates.stream()
-			.filter((v) -> v instanceof DelegatingOAuth2TokenValidator)
-			.findFirst()
-			.get();
-		if (issuerUri != null) {
-			assertThat(delegatingValidator).extracting("tokenValidators")
-				.asInstanceOf(InstanceOfAssertFactories.collection(OAuth2TokenValidator.class))
-				.hasAtLeastOneElementOfType(JwtIssuerValidator.class);
-		}
-		List> claimValidators = delegates.stream()
-			.filter((d) -> d instanceof JwtClaimValidator)
-			.collect(Collectors.toList());
-		assertThat(claimValidators).anyMatch((v) -> "aud".equals(ReflectionTestUtils.getField(v, "claim")));
-		if (customValidator != null) {
-			assertThat(claimValidators)
-				.anyMatch((v) -> "custom_claim".equals(ReflectionTestUtils.getField(v, "claim")));
-		}
-	}
-
 	@Test
 	void autoConfigurationShouldConfigureAudienceValidatorIfPropertyProvidedAndPublicKey() throws Exception {
 		this.server = new MockWebServer();
@@ -622,7 +590,8 @@ void autoConfigurationShouldConfigureAudienceValidatorIfPropertyProvidedAndPubli
 			.run((context) -> {
 				assertThat(context).hasSingleBean(JwtDecoder.class);
 				JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class);
-				validate(null, jwtDecoder, null);
+				validate(jwt().claim("aud", List.of("https://test-audience.com")), jwtDecoder,
+						(validators) -> assertThat(validators).satisfiesOnlyOnce(audClaimValidator()));
 			});
 	}
 
@@ -732,6 +701,37 @@ static Jwt.Builder jwt() {
 			.subject("mock-test-subject");
 	}
 
+	@SuppressWarnings("unchecked")
+	private void validate(Jwt.Builder builder, JwtDecoder jwtDecoder,
+			ThrowingConsumer>> validatorsConsumer) {
+		DelegatingOAuth2TokenValidator jwtValidator = (DelegatingOAuth2TokenValidator) ReflectionTestUtils
+			.getField(jwtDecoder, "jwtValidator");
+		assertThat(jwtValidator.validate(builder.build()).hasErrors()).isFalse();
+		validatorsConsumer.accept(extractValidators(jwtValidator));
+	}
+
+	@SuppressWarnings("unchecked")
+	private List> extractValidators(DelegatingOAuth2TokenValidator delegatingValidator) {
+		Collection> delegates = (Collection>) ReflectionTestUtils
+			.getField(delegatingValidator, "tokenValidators");
+		List> extracted = new ArrayList<>();
+		for (OAuth2TokenValidator delegate : delegates) {
+			if (delegate instanceof DelegatingOAuth2TokenValidator delegatingDelegate) {
+				extracted.addAll(extractValidators(delegatingDelegate));
+			}
+			else {
+				extracted.add(delegate);
+			}
+		}
+		return extracted;
+	}
+
+	private Consumer> audClaimValidator() {
+		return (validator) -> assertThat(validator).isInstanceOf(JwtClaimValidator.class)
+			.extracting("claim")
+			.isEqualTo("aud");
+	}
+
 	@Configuration(proxyBeanMethods = false)
 	@EnableWebSecurity
 	static class TestConfig {
@@ -786,7 +786,7 @@ SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception
 	}
 
 	@Configuration(proxyBeanMethods = false)
-	static class CustomTokenValidatorsConfig {
+	static class CustomJwtClaimValidatorConfig {
 
 		@Bean
 		JwtClaimValidator customJwtClaimValidator() {

From fc8a8d363f5a947096e8721319ca6c6d0df26f5d Mon Sep 17 00:00:00 2001
From: Andy Wilkinson 
Date: Wed, 5 Jul 2023 15:50:32 +0100
Subject: [PATCH 0099/1215] Polish

---
 .../boot/configurationmetadata/changelog/ChangelogWriter.java   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java
index fd79845d0ecf..033d5e20977e 100644
--- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java
+++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java
@@ -89,7 +89,7 @@ private Map> collateByType(Changelog difference
 	private void writeDeprecated(List differences) {
 		List rows = sortProperties(differences, Difference::newProperty).stream()
 			.filter(this::isDeprecatedInRelease)
-			.collect(Collectors.toList());
+			.toList();
 		writeTable("| Key | Replacement | Reason", rows, this::writeDeprecated);
 	}
 

From a1a5acf1282975097ade701c4bee56e7da4cb085 Mon Sep 17 00:00:00 2001
From: Arjen Poutsma 
Date: Tue, 4 Jul 2023 15:50:53 +0200
Subject: [PATCH 0100/1215] Add initial support for RestClient

Introduce initial support for Spring Framework's `RestClient`, in the
form of a `RestClientCustomizer` and `RestClientAutoConfiguration`.

See gh-36213
---
 .../client/RestClientAutoConfiguration.java   |  70 +++++++++++
 .../RestClientAutoConfigurationTests.java     | 114 ++++++++++++++++++
 .../boot/web/client/RestClientCustomizer.java |  38 ++++++
 3 files changed, 222 insertions(+)
 create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java
 create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java
 create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestClientCustomizer.java

diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java
new file mode 100644
index 000000000000..bcd98abbf547
--- /dev/null
+++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2012-2023 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.web.client;
+
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
+import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
+import org.springframework.boot.web.client.RestClientCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.context.annotation.Scope;
+import org.springframework.web.client.RestClient;
+
+/**
+ * {@link EnableAutoConfiguration Auto-configuration} for {@link RestClient}.
+ * 

+ * This will produce a {@link RestClient.Builder RestClient.Builder} bean with the + * {@code prototype} scope, meaning each injection point will receive a newly cloned + * instance of the builder. + * + * @author Arjen Poutsma + * @since 3.2.0 + */ +@AutoConfiguration(after = HttpMessageConvertersAutoConfiguration.class) +@ConditionalOnClass(RestClient.class) +@Conditional(RestClientAutoConfiguration.NotReactiveWebApplicationCondition.class) +public class RestClientAutoConfiguration { + + @Bean + @Scope("prototype") + @ConditionalOnMissingBean + public RestClient.Builder webClientBuilder(ObjectProvider customizerProvider) { + RestClient.Builder builder = RestClient.builder(); + customizerProvider.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder; + } + + static class NotReactiveWebApplicationCondition extends NoneNestedConditions { + + NotReactiveWebApplicationCondition() { + super(ConfigurationPhase.PARSE_CONFIGURATION); + } + + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) + private static class ReactiveWebApplication { + + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java new file mode 100644 index 000000000000..2a256bb3fb31 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2012-2023 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.web.client; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.web.client.RestClientCustomizer; +import org.springframework.boot.web.codec.CodecCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RestClientAutoConfiguration} + * + * @author Arjen Poutsma + */ +class RestClientAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class)); + + @Test + void shouldCreateBuilder() { + this.contextRunner.run((context) -> { + RestClient.Builder builder = context.getBean(RestClient.Builder.class); + RestClient restClient = builder.build(); + assertThat(restClient).isNotNull(); + }); + } + + @Test + void restClientShouldApplyCustomizers() { + this.contextRunner.withUserConfiguration(RestClientCustomizerConfig.class).run((context) -> { + RestClient.Builder builder = context.getBean(RestClient.Builder.class); + RestClientCustomizer customizer = context.getBean("webClientCustomizer", RestClientCustomizer.class); + builder.build(); + then(customizer).should().customize(any(RestClient.Builder.class)); + }); + } + + @Test + void shouldGetPrototypeScopedBean() { + this.contextRunner.withUserConfiguration(RestClientCustomizerConfig.class).run((context) -> { + RestClient.Builder firstBuilder = context.getBean(RestClient.Builder.class); + RestClient.Builder secondBuilder = context.getBean(RestClient.Builder.class); + assertThat(firstBuilder).isNotEqualTo(secondBuilder); + }); + } + + @Test + void shouldNotCreateClientBuilderIfAlreadyPresent() { + this.contextRunner.withUserConfiguration(CustomRestClientBuilderConfig.class).run((context) -> { + RestClient.Builder builder = context.getBean(RestClient.Builder.class); + assertThat(builder).isInstanceOf(MyWebClientBuilder.class); + }); + } + + @Configuration(proxyBeanMethods = false) + static class CodecConfiguration { + + @Bean + CodecCustomizer myCodecCustomizer() { + return mock(CodecCustomizer.class); + } + + } + + @Configuration(proxyBeanMethods = false) + static class RestClientCustomizerConfig { + + @Bean + RestClientCustomizer webClientCustomizer() { + return mock(RestClientCustomizer.class); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomRestClientBuilderConfig { + + @Bean + MyWebClientBuilder myWebClientBuilder() { + return mock(MyWebClientBuilder.class); + } + + } + + interface MyWebClientBuilder extends RestClient.Builder { + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestClientCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestClientCustomizer.java new file mode 100644 index 000000000000..e19b9f5a02c7 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestClientCustomizer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2023 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.web.client; + +import org.springframework.web.client.RestClient; + +/** + * Callback interface that can be used to customize a + * {@link org.springframework.web.client.RestClient.Builder RestClient.Builder}. + * + * @author Arjen Poutsma + * @since 3.2.0 + */ +@FunctionalInterface +public interface RestClientCustomizer { + + /** + * Callback to customize a {@link org.springframework.web.client.RestClient.Builder + * RestClient.Builder} instance. + * @param restClientBuilder the client builder to customize + */ + void customize(RestClient.Builder restClientBuilder); + +} From 2d2f0502623232d994b7f0ed40d99032838fe745 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 5 Jul 2023 10:42:06 +0100 Subject: [PATCH 0101/1215] Polish 'Add initial support for RestClient' See gh-36213 --- .../NotReactiveWebApplicationCondition.java | 40 +++++++++++++++++++ .../client/RestClientAutoConfiguration.java | 28 ++++--------- .../client/RestTemplateAutoConfiguration.java | 17 -------- 3 files changed, 48 insertions(+), 37 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/NotReactiveWebApplicationCondition.java 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 new file mode 100644 index 000000000000..622f4ee19304 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/NotReactiveWebApplicationCondition.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2023 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.web.client; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.NoneNestedConditions; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; + +/** + * {@link SpringBootCondition} that applies only when running in a non-reactive web + * application. + * + * @author Phillip Webb + */ +class NotReactiveWebApplicationCondition extends NoneNestedConditions { + + NotReactiveWebApplicationCondition() { + super(ConfigurationPhase.PARSE_CONFIGURATION); + } + + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) + private static class ReactiveWebApplication { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java index bcd98abbf547..e62dc869f4de 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java @@ -21,9 +21,9 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; -import org.springframework.boot.autoconfigure.condition.NoneNestedConditions; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.web.client.ClientHttpRequestFactories; +import org.springframework.boot.web.client.ClientHttpRequestFactorySettings; import org.springframework.boot.web.client.RestClientCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; @@ -33,38 +33,26 @@ /** * {@link EnableAutoConfiguration Auto-configuration} for {@link RestClient}. *

- * This will produce a {@link RestClient.Builder RestClient.Builder} bean with the - * {@code prototype} scope, meaning each injection point will receive a newly cloned - * instance of the builder. + * This will produce a {@link org.springframework.web.client.RestClient.Builder + * RestClient.Builder} bean with the {@code prototype} scope, meaning each injection point + * will receive a newly cloned instance of the builder. * * @author Arjen Poutsma * @since 3.2.0 */ @AutoConfiguration(after = HttpMessageConvertersAutoConfiguration.class) @ConditionalOnClass(RestClient.class) -@Conditional(RestClientAutoConfiguration.NotReactiveWebApplicationCondition.class) +@Conditional(NotReactiveWebApplicationCondition.class) public class RestClientAutoConfiguration { @Bean @Scope("prototype") @ConditionalOnMissingBean public RestClient.Builder webClientBuilder(ObjectProvider customizerProvider) { - RestClient.Builder builder = RestClient.builder(); + RestClient.Builder builder = RestClient.builder() + .requestFactory(ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS)); customizerProvider.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder; } - static class NotReactiveWebApplicationCondition extends NoneNestedConditions { - - NotReactiveWebApplicationCondition() { - super(ConfigurationPhase.PARSE_CONFIGURATION); - } - - @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) - private static class ReactiveWebApplication { - - } - - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java index bbefd47fccf4..fab1a2c4a475 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java @@ -21,12 +21,8 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; -import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; -import org.springframework.boot.autoconfigure.condition.NoneNestedConditions; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; -import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration.NotReactiveWebApplicationCondition; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.client.RestTemplateCustomizer; import org.springframework.boot.web.client.RestTemplateRequestCustomizer; @@ -69,17 +65,4 @@ public RestTemplateBuilder restTemplateBuilder(RestTemplateBuilderConfigurer res return restTemplateBuilderConfigurer.configure(builder); } - static class NotReactiveWebApplicationCondition extends NoneNestedConditions { - - NotReactiveWebApplicationCondition() { - super(ConfigurationPhase.PARSE_CONFIGURATION); - } - - @ConditionalOnWebApplication(type = Type.REACTIVE) - private static class ReactiveWebApplication { - - } - - } - } From 5e01c66552247cb30512f2f8569ac36f97808819 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 5 Jul 2023 14:11:28 +0100 Subject: [PATCH 0102/1215] Add RestClient HttpMessageConverters support Update `RestClientAutoConfiguration` to apply `HttpMessageConverters` configuration. See gh-36213 --- ...MessageConvertersRestClientCustomizer.java | 60 +++++++++++++++ .../client/RestClientAutoConfiguration.java | 11 +++ ...geConvertersRestClientCustomizerTests.java | 77 +++++++++++++++++++ .../RestClientAutoConfigurationTests.java | 64 +++++++++++++++ 4 files changed, 212 insertions(+) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/HttpMessageConvertersRestClientCustomizer.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/HttpMessageConvertersRestClientCustomizerTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/HttpMessageConvertersRestClientCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/HttpMessageConvertersRestClientCustomizer.java new file mode 100644 index 000000000000..c5372d5d7f84 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/HttpMessageConvertersRestClientCustomizer.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2023 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.web.client; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +import org.springframework.boot.web.client.RestClientCustomizer; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.util.Assert; +import org.springframework.web.client.RestClient; + +/** + * {@link RestClientCustomizer} to apply {@link HttpMessageConverter + * HttpMessageConverters}. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public class HttpMessageConvertersRestClientCustomizer implements RestClientCustomizer { + + private final Iterable> messageConverters; + + public HttpMessageConvertersRestClientCustomizer(HttpMessageConverter... messageConverters) { + Assert.notNull(messageConverters, "MessageConverters must not be null"); + this.messageConverters = Arrays.asList(messageConverters); + } + + HttpMessageConvertersRestClientCustomizer(HttpMessageConverters messageConverters) { + this.messageConverters = messageConverters; + } + + @Override + public void customize(RestClient.Builder restClientBuilder) { + restClientBuilder.messageConverters(this::configureMessageConverters); + } + + private void configureMessageConverters(List> messageConverters) { + if (this.messageConverters != null) { + messageConverters.clear(); + this.messageConverters.forEach(messageConverters::add); + } + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java index e62dc869f4de..b33fc882f942 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java @@ -21,6 +21,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.web.client.ClientHttpRequestFactories; import org.springframework.boot.web.client.ClientHttpRequestFactorySettings; @@ -28,6 +29,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Scope; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.web.client.RestClient; /** @@ -45,6 +48,14 @@ @Conditional(NotReactiveWebApplicationCondition.class) public class RestClientAutoConfiguration { + @Bean + @ConditionalOnMissingBean + @Order(Ordered.LOWEST_PRECEDENCE) + public HttpMessageConvertersRestClientCustomizer httpMessageConvertersRestClientCustomizer( + ObjectProvider messageConverters) { + return new HttpMessageConvertersRestClientCustomizer(messageConverters.getIfUnique()); + } + @Bean @Scope("prototype") @ConditionalOnMissingBean diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/HttpMessageConvertersRestClientCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/HttpMessageConvertersRestClientCustomizerTests.java new file mode 100644 index 000000000000..f4138baa1819 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/HttpMessageConvertersRestClientCustomizerTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2023 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.web.client; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.web.client.RestClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link HttpMessageConvertersRestClientCustomizer} + * + * @author Phillip Webb + */ +class HttpMessageConvertersRestClientCustomizerTests { + + @Test + void createWhenNullMessageConvertersArrayThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new HttpMessageConvertersRestClientCustomizer((HttpMessageConverter[]) null)) + .withMessage("MessageConverters must not be null"); + } + + @Test + void createWhenNullMessageConvertersDoesNotCustomize() { + HttpMessageConverter c0 = mock(); + assertThat(apply(new HttpMessageConvertersRestClientCustomizer((HttpMessageConverters) null), c0)) + .containsExactly(c0); + } + + @Test + void customizeConfiguresMessageConverters() { + HttpMessageConverter c0 = mock(); + HttpMessageConverter c1 = mock(); + HttpMessageConverter c2 = mock(); + assertThat(apply(new HttpMessageConvertersRestClientCustomizer(c1, c2), c0)).containsExactly(c1, c2); + } + + @SuppressWarnings("unchecked") + private List> apply(HttpMessageConvertersRestClientCustomizer customizer, + HttpMessageConverter... converters) { + List> messageConverters = new ArrayList<>(Arrays.asList(converters)); + RestClient.Builder restClientBuilder = mock(); + ArgumentCaptor>>> captor = ArgumentCaptor.forClass(Consumer.class); + given(restClientBuilder.messageConverters(captor.capture())).willReturn(restClientBuilder); + customizer.customize(restClientBuilder); + captor.getValue().accept(messageConverters); + return messageConverters; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java index 2a256bb3fb31..368c29d88d63 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java @@ -16,14 +16,21 @@ package org.springframework.boot.autoconfigure.web.client; +import java.util.List; + import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.http.HttpMessageConverters; +import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.web.client.RestClientCustomizer; import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @@ -77,6 +84,49 @@ void shouldNotCreateClientBuilderIfAlreadyPresent() { }); } + @Test + @SuppressWarnings("unchecked") + void restClientWhenMessageConvertersDefinedShouldHaveMessageConverters() { + this.contextRunner.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class)) + .withUserConfiguration(RestClientConfig.class) + .run((context) -> { + RestClient restClient = context.getBean(RestClient.class); + List> expectedConverters = context.getBean(HttpMessageConverters.class) + .getConverters(); + List> actualConverters = (List>) ReflectionTestUtils + .getField(restClient, "messageConverters"); + assertThat(actualConverters).containsExactlyElementsOf(expectedConverters); + }); + } + + @Test + @SuppressWarnings("unchecked") + void restClientWhenNoMessageConvertersDefinedShouldHaveDefaultMessageConverters() { + this.contextRunner.withUserConfiguration(RestClientConfig.class).run((context) -> { + RestClient restClient = context.getBean(RestClient.class); + RestClient defaultRestClient = RestClient.builder().build(); + List> actualConverters = (List>) ReflectionTestUtils + .getField(restClient, "messageConverters"); + List> expectedConverters = (List>) ReflectionTestUtils + .getField(defaultRestClient, "messageConverters"); + assertThat(actualConverters).hasSameSizeAs(expectedConverters); + }); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + void restClientWhenHasCustomMessageConvertersShouldHaveMessageConverters() { + this.contextRunner.withConfiguration(AutoConfigurations.of(HttpMessageConvertersAutoConfiguration.class)) + .withUserConfiguration(CustomHttpMessageConverter.class, RestClientConfig.class) + .run((context) -> { + RestClient restClient = context.getBean(RestClient.class); + List> actualConverters = (List>) ReflectionTestUtils + .getField(restClient, "messageConverters"); + assertThat(actualConverters).extracting(HttpMessageConverter::getClass) + .contains((Class) CustomHttpMessageConverter.class); + }); + } + @Configuration(proxyBeanMethods = false) static class CodecConfiguration { @@ -111,4 +161,18 @@ interface MyWebClientBuilder extends RestClient.Builder { } + @Configuration(proxyBeanMethods = false) + static class RestClientConfig { + + @Bean + RestClient restClient(RestClient.Builder restClientBuilder) { + return restClientBuilder.build(); + } + + } + + static class CustomHttpMessageConverter extends StringHttpMessageConverter { + + } + } From 7c1b168ed64b07489152ff53e18692d98f90febe Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 5 Jul 2023 20:31:21 +0100 Subject: [PATCH 0103/1215] Overhaul reference documentation for RestClient Reorder "Calling REST services" documentation and add a new section covering `RestClient`. See gh-36213 --- .../docs/asciidoc/anchor-rewrite.properties | 3 + .../src/docs/asciidoc/io/rest-client.adoc | 173 +++++++++++------- .../io/restclient/restclient/Details.java | 21 +++ .../io/restclient/restclient/MyService.java | 35 ++++ .../docs/io/restclient/restclient/Details.kt | 19 ++ .../io/restclient/restclient/MyService.kt | 38 ++++ 6 files changed, 222 insertions(+), 67 deletions(-) create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/Details.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/MyService.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/Details.kt create mode 100644 spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/MyService.kt diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties index 0c84776b2f36..44164b239812 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties @@ -1020,3 +1020,6 @@ howto.testing.testcontainers.dynamic-properties=features.testing.testcontainers. # gh-32905 container-images.efficient-images.unpacking=deployment.efficient.unpacking + +# Spring Boot 3.1 - 3.2 migrations +io.rest-client.resttemplate.http-client=io.rest-client.clienthttprequestfactory diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/rest-client.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/rest-client.adoc index 0147af61f5e6..13c58443247a 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/rest-client.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/rest-client.adoc @@ -1,80 +1,23 @@ [[io.rest-client]] == Calling REST Services -If your application calls remote REST services, Spring Boot makes that very convenient using a `RestTemplate` or a `WebClient`. - -[[io.rest-client.resttemplate]] -=== RestTemplate -If you need to call remote REST services from your application, you can use the Spring Framework's {spring-framework-api}/web/client/RestTemplate.html[`RestTemplate`] class. -Since `RestTemplate` instances often need to be customized before being used, Spring Boot does not provide any single auto-configured `RestTemplate` bean. -It does, however, auto-configure a `RestTemplateBuilder`, which can be used to create `RestTemplate` instances when needed. -The auto-configured `RestTemplateBuilder` ensures that sensible `HttpMessageConverters` are applied to `RestTemplate` instances. - -The following code shows a typical example: - -include::code:MyService[] - -`RestTemplateBuilder` includes a number of useful methods that can be used to quickly configure a `RestTemplate`. -For example, to add BASIC authentication support, you can use `builder.basicAuthentication("user", "password").build()`. - - - -[[io.rest-client.resttemplate.http-client]] -==== RestTemplate HTTP Client -Spring Boot will auto-detect which HTTP client to use with `RestTemplate` depending on the libraries available on the application classpath. -In order of preference, the following clients are supported: - -. Apache HttpClient -. OkHttp -. Jetty HttpClient -. Simple JDK client (`HttpURLConnection`) - -If multiple clients are available on the classpath, the most preferred client will be used. - - - -[[io.rest-client.resttemplate.customization]] -==== RestTemplate Customization -There are three main approaches to `RestTemplate` customization, depending on how broadly you want the customizations to apply. - -To make the scope of any customizations as narrow as possible, inject the auto-configured `RestTemplateBuilder` and then call its methods as required. -Each method call returns a new `RestTemplateBuilder` instance, so the customizations only affect this use of the builder. - -To make an application-wide, additive customization, use a `RestTemplateCustomizer` bean. -All such beans are automatically registered with the auto-configured `RestTemplateBuilder` and are applied to any templates that are built with it. - -The following example shows a customizer that configures the use of a proxy for all hosts except `192.168.0.5`: - -include::code:MyRestTemplateCustomizer[] - -Finally, you can define your own `RestTemplateBuilder` bean. -Doing so will replace the auto-configured builder. -If you want any `RestTemplateCustomizer` beans to be applied to your custom builder, as the auto-configuration would have done, configure it using a `RestTemplateBuilderConfigurer`. -The following example exposes a `RestTemplateBuilder` that matches what Spring Boot's auto-configuration would have done, except that custom connect and read timeouts are also specified: - -include::code:MyRestTemplateBuilderConfiguration[] - -The most extreme (and rarely used) option is to create your own `RestTemplateBuilder` bean without using a configurer. -In addition to replacing the auto-configured builder, this also prevents any `RestTemplateCustomizer` beans from being used. - - - -[[io.rest-client.resttemplate.ssl]] -==== RestTemplate SSL Support -If you need custom SSL configuration on the `RestTemplate`, you can apply an <> to the `RestTemplateBuilder` as shown in this example: - -include::code:MyService[] +Spring Boot provides various convenient ways to call remote REST services. +If you are developing a non-blocking reactive application and you're using Spring WebFlux, then you can use `WebClient`. +If you prefer blocking APIs then you can use `RestClient` or `RestTemplate`. [[io.rest-client.webclient]] === WebClient -If you have Spring WebFlux on your classpath, you can also choose to use `WebClient` to call remote REST services. -Compared to `RestTemplate`, this client has a more functional feel and is fully reactive. +If you have Spring WebFlux on your classpath we recommend that you use `WebClient` to call remote REST services. +The `WebClient` interface provides a functional style API and is fully reactive. You can learn more about the `WebClient` in the dedicated {spring-framework-docs}/web-reactive.html#webflux-client[section in the Spring Framework docs]. -Spring Boot creates and pre-configures a `WebClient.Builder` for you. +TIP: If you are not writing a reactive Spring WebFlux application you can use the a <> instead of a `WebClient`. +This provides a similar functional API, but is blocking rather than reactive. + +Spring Boot creates and pre-configures a prototype `WebClient.Builder` bean for you. It is strongly advised to inject it in your components and use it to create `WebClient` instances. -Spring Boot is configuring that builder to share HTTP resources, reflect codecs setup in the same fashion as the server ones (see <>), and more. +Spring Boot is configuring that builder to share HTTP resources and reflect codecs setup in the same fashion as the server ones (see <>), and more. The following code shows a typical example: @@ -131,3 +74,99 @@ The following code shows a typical example: include::code:MyService[] + + +[[io.rest-client.restclient]] +=== RestClient +If you are not using Spring WebFlux or Project Reactor in your application we recommend that you use `RestClient` to call remote REST services. + +The `RestClient` interface provides a functional style blocking API. + +Spring Boot creates and pre-configures a prototype `RestClient.Builder` bean for you. +It is strongly advised to inject it in your components and use it to create `RestClient` instances. +Spring Boot is configuring that builder with `HttpMessageConverters` and an appropriate `ClientHttpRequestFactory`. + +The following code shows a typical example: + +include::code:MyService[] + + + +[[io.rest-client.restclient.customization]] +==== RestClient Customization +There are three main approaches to `RestClient` customization, depending on how broadly you want the customizations to apply. + +To make the scope of any customizations as narrow as possible, inject the auto-configured `RestClient.Builder` and then call its methods as required. +`RestClient.Builder` instances are stateful: Any change on the builder is reflected in all clients subsequently created with it. +If you want to create several clients with the same builder, you can also consider cloning the builder with `RestClient.Builder other = builder.clone();`. + +To make an application-wide, additive customization to all `RestClient.Builder` instances, you can declare `RestClientCustomizer` beans and change the `RestClient.Builder` locally at the point of injection. + +Finally, you can fall back to the original API and use `RestClient.create()`. +In that case, no auto-configuration or `RestClientCustomizer` is applied. + + + +[[io.rest-client.resttemplate]] +=== RestTemplate +Spring Framework's {spring-framework-api}/web/client/RestTemplate.html[`RestTemplate`] class predates `RestClient` and is the classic way that many applications use to call remote REST services. +You might choose to use `RestTemplate` when you have existing code that you don't want to migrate to `RestClient`, or because you're already familiar with the `RestTemplate` API. + +Since `RestTemplate` instances often need to be customized before being used, Spring Boot does not provide any single auto-configured `RestTemplate` bean. +It does, however, auto-configure a `RestTemplateBuilder`, which can be used to create `RestTemplate` instances when needed. +The auto-configured `RestTemplateBuilder` ensures that sensible `HttpMessageConverters` and an appropriate `ClientHttpRequestFactory` are applied to `RestTemplate` instances. + +The following code shows a typical example: + +include::code:MyService[] + +`RestTemplateBuilder` includes a number of useful methods that can be used to quickly configure a `RestTemplate`. +For example, to add BASIC authentication support, you can use `builder.basicAuthentication("user", "password").build()`. + + + +[[io.rest-client.resttemplate.customization]] +==== RestTemplate Customization +There are three main approaches to `RestTemplate` customization, depending on how broadly you want the customizations to apply. + +To make the scope of any customizations as narrow as possible, inject the auto-configured `RestTemplateBuilder` and then call its methods as required. +Each method call returns a new `RestTemplateBuilder` instance, so the customizations only affect this use of the builder. + +To make an application-wide, additive customization, use a `RestTemplateCustomizer` bean. +All such beans are automatically registered with the auto-configured `RestTemplateBuilder` and are applied to any templates that are built with it. + +The following example shows a customizer that configures the use of a proxy for all hosts except `192.168.0.5`: + +include::code:MyRestTemplateCustomizer[] + +Finally, you can define your own `RestTemplateBuilder` bean. +Doing so will replace the auto-configured builder. +If you want any `RestTemplateCustomizer` beans to be applied to your custom builder, as the auto-configuration would have done, configure it using a `RestTemplateBuilderConfigurer`. +The following example exposes a `RestTemplateBuilder` that matches what Spring Boot's auto-configuration would have done, except that custom connect and read timeouts are also specified: + +include::code:MyRestTemplateBuilderConfiguration[] + +The most extreme (and rarely used) option is to create your own `RestTemplateBuilder` bean without using a configurer. +In addition to replacing the auto-configured builder, this also prevents any `RestTemplateCustomizer` beans from being used. + + + +[[io.rest-client.resttemplate.ssl]] +==== RestTemplate SSL Support +If you need custom SSL configuration on the `RestTemplate`, you can apply an <> to the `RestTemplateBuilder` as shown in this example: + +include::code:MyService[] + + + +[[io.rest-client.clienthttprequestfactory]] +=== HTTP Client Detection for RestClient and RestTemplate +Spring Boot will auto-detect which HTTP client to use with `RestClient` and `RestTemplate` depending on the libraries available on the application classpath. +In order of preference, the following clients are supported: + +. Apache HttpClient +. OkHttp +. Jetty HttpClient +. Simple JDK client (`HttpURLConnection`) + +If multiple clients are available on the classpath, the most preferred client will be used. diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/Details.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/Details.java new file mode 100644 index 000000000000..28c038969b28 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/Details.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2023 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.docs.io.restclient.restclient; + +public class Details { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/MyService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/MyService.java new file mode 100644 index 000000000000..98bd2049406c --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/MyService.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2023 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.docs.io.restclient.restclient; + +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; + +@Service +public class MyService { + + private final RestClient restClient; + + public MyService(RestClient.Builder restClientBuilder) { + this.restClient = restClientBuilder.baseUrl("https://example.org").build(); + } + + public Details someRestCall(String name) { + return this.restClient.get().uri("/{name}/details", name).retrieve().body(Details.class); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/Details.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/Details.kt new file mode 100644 index 000000000000..219b0a9ffe29 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/Details.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2012-2023 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.docs.io.restclient.restclient + +class Details diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/MyService.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/MyService.kt new file mode 100644 index 000000000000..cb1854c03c5e --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/MyService.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2022 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.docs.io.restclient.restclient + +import org.springframework.boot.docs.io.restclient.restclient.ssl.Details +import org.springframework.stereotype.Service +import org.springframework.web.client.RestClient + +@Service +class MyService(restClientBuilder: RestClient.Builder) { + + private val restClient: RestClient + + init { + restClient = restClientBuilder.baseUrl("https://example.org").build() + } + + fun someRestCall(name: String?): Details { + return restClient.get().uri("/{name}/details", name) + .retrieve().body(Details::class.java)!! + } + +} + From cfdc173e34363b490d00c9825145d4ff9ba63905 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 5 Jul 2023 13:09:56 +0100 Subject: [PATCH 0104/1215] Add RestClient SSL support Add `RestClientSsl` support class to help apply an `SslBundle` to a `RestClient.Builder`. See gh-36213 --- .../client/AutoConfiguredRestClientSsl.java | 55 +++++++++++++++ .../web/client/RestClientSsl.java | 68 +++++++++++++++++++ .../src/docs/asciidoc/io/rest-client.adoc | 16 +++++ .../restclient/restclient/ssl/MyService.java | 36 ++++++++++ .../restclient/ssl/settings/Details.java | 21 ++++++ .../restclient/ssl/settings/MyService.java | 45 ++++++++++++ .../io/restclient/restclient/ssl/Details.kt | 19 ++++++ .../io/restclient/restclient/ssl/MyService.kt | 39 +++++++++++ .../restclient/ssl/settings/Details.kt | 19 ++++++ .../restclient/ssl/settings/MyService.kt | 46 +++++++++++++ 10 files changed, 364 insertions(+) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/AutoConfiguredRestClientSsl.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientSsl.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/ssl/MyService.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/ssl/settings/Details.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/ssl/settings/MyService.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/ssl/Details.kt create mode 100644 spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/ssl/MyService.kt create mode 100644 spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/ssl/settings/Details.kt create mode 100644 spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/ssl/settings/MyService.kt diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/AutoConfiguredRestClientSsl.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/AutoConfiguredRestClientSsl.java new file mode 100644 index 000000000000..8fc9dd9663b2 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/AutoConfiguredRestClientSsl.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2023 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.web.client; + +import java.util.function.Consumer; + +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.web.client.ClientHttpRequestFactories; +import org.springframework.boot.web.client.ClientHttpRequestFactorySettings; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.web.client.RestClient; + +/** + * An auto-configured {@link RestClientSsl} implementation. + * + * @author Phillip Webb + */ +class AutoConfiguredRestClientSsl implements RestClientSsl { + + private final SslBundles sslBundles; + + AutoConfiguredRestClientSsl(SslBundles sslBundles) { + this.sslBundles = sslBundles; + } + + @Override + public Consumer fromBundle(String bundleName) { + return fromBundle(this.sslBundles.getBundle(bundleName)); + } + + @Override + public Consumer fromBundle(SslBundle bundle) { + return (builder) -> { + ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.DEFAULTS.withSslBundle(bundle); + ClientHttpRequestFactory requestFactory = ClientHttpRequestFactories.get(settings); + builder.requestFactory(requestFactory); + }; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientSsl.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientSsl.java new file mode 100644 index 000000000000..bcf97ddca19e --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientSsl.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2023 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.web.client; + +import java.util.function.Consumer; + +import org.springframework.boot.ssl.NoSuchSslBundleException; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.web.client.ClientHttpRequestFactories; +import org.springframework.boot.web.client.ClientHttpRequestFactorySettings; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.web.client.RestClient; + +/** + * Interface that can be used to {@link RestClient.Builder#apply apply} SSL configuration + * to a {@link org.springframework.web.client.RestClient.Builder RestClient.Builder}. + *

+ * Typically used as follows:

+ * @Bean
+ * public MyBean myBean(RestClient.Builder restClientBuilder, RestClientSsl ssl) {
+ *     RestClient restClientrestClient= restClientBuilder.apply(ssl.forBundle("mybundle")).build();
+ *     return new MyBean(webClient);
+ * }
+ * 
NOTE: Apply SSL configuration will replace any previously + * {@link RestClient.Builder#requestFactory configured} {@link ClientHttpRequestFactory}. + * If you need to configure {@link ClientHttpRequestFactory} with more than just SSL + * consider using a {@link ClientHttpRequestFactorySettings} with + * {@link ClientHttpRequestFactories}. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public interface RestClientSsl { + + /** + * Return a {@link Consumer} that will apply SSL configuration for the named + * {@link SslBundle} to a {@link org.springframework.web.client.RestClient.Builder + * RestClient.Builder}. + * @param bundleName the name of the SSL bundle to apply + * @return a {@link Consumer} to apply the configuration + * @throws NoSuchSslBundleException if a bundle with the provided name does not exist + */ + Consumer fromBundle(String bundleName) throws NoSuchSslBundleException; + + /** + * Return a {@link Consumer} that will apply SSL configuration for the + * {@link SslBundle} to a {@link org.springframework.web.client.RestClient.Builder + * RestClient.Builder}. + * @param bundle the SSL bundle to apply + * @return a {@link Consumer} to apply the configuration + */ + Consumer fromBundle(SslBundle bundle); + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/rest-client.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/rest-client.adoc index 13c58443247a..b371623f77c4 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/rest-client.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/rest-client.adoc @@ -107,6 +107,22 @@ In that case, no auto-configuration or `RestClientCustomizer` is applied. +[[io.rest-client.restclient.ssl]] +==== RestClient SSL Support +If you need custom SSL configuration on the `ClientHttpRequestFactory` used by the `RestClient`, you can inject a `RestClientSsl` instance that can be used with the builder's `apply` method. + +The `RestClientSsl` interface provides access to any <> that you have defined in your `application.properties` or `application.yaml` file. + +The following code shows a typical example: + +include::code:MyService[] + +If you need to apply other customization in addition to an SSL bundle, you can use the `ClientHttpRequestFactorySettings` class with `ClientHttpRequestFactories`: + +include::code:settings/MyService[] + + + [[io.rest-client.resttemplate]] === RestTemplate Spring Framework's {spring-framework-api}/web/client/RestTemplate.html[`RestTemplate`] class predates `RestClient` and is the classic way that many applications use to call remote REST services. diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/ssl/MyService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/ssl/MyService.java new file mode 100644 index 000000000000..0fa7fa50cbba --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/ssl/MyService.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2023 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.docs.io.restclient.restclient.ssl; + +import org.springframework.boot.autoconfigure.web.client.RestClientSsl; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; + +@Service +public class MyService { + + private final RestClient restClient; + + public MyService(RestClient.Builder restClientBuilder, RestClientSsl ssl) { + this.restClient = restClientBuilder.baseUrl("https://example.org").apply(ssl.fromBundle("mybundle")).build(); + } + + public Details someRestCall(String name) { + return this.restClient.get().uri("/{name}/details", name).retrieve().body(Details.class); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/ssl/settings/Details.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/ssl/settings/Details.java new file mode 100644 index 000000000000..1b0bbdb7533b --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/ssl/settings/Details.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2023 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.docs.io.restclient.restclient.ssl.settings; + +public class Details { + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/ssl/settings/MyService.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/ssl/settings/MyService.java new file mode 100644 index 000000000000..8fef86df53e3 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/ssl/settings/MyService.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2023 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.docs.io.restclient.restclient.ssl.settings; + +import java.time.Duration; + +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.web.client.ClientHttpRequestFactories; +import org.springframework.boot.web.client.ClientHttpRequestFactorySettings; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; + +@Service +public class MyService { + + private final RestClient restClient; + + public MyService(RestClient.Builder restClientBuilder, SslBundles sslBundles) { + ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.DEFAULTS + .withReadTimeout(Duration.ofMinutes(2)) + .withSslBundle(sslBundles.getBundle("mybundle")); + ClientHttpRequestFactory requestFactory = ClientHttpRequestFactories.get(settings); + this.restClient = restClientBuilder.baseUrl("https://example.org").requestFactory(requestFactory).build(); + } + + public Details someRestCall(String name) { + return this.restClient.get().uri("/{name}/details", name).retrieve().body(Details.class); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/ssl/Details.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/ssl/Details.kt new file mode 100644 index 000000000000..613bbadb3fd7 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/ssl/Details.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2012-2023 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.docs.io.restclient.restclient.ssl + +class Details diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/ssl/MyService.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/ssl/MyService.kt new file mode 100644 index 000000000000..220a44252e7f --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/ssl/MyService.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2023 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.docs.io.restclient.restclient.ssl + +import org.springframework.boot.autoconfigure.web.client.RestClientSsl +import org.springframework.boot.docs.io.restclient.restclient.ssl.settings.Details +import org.springframework.stereotype.Service +import org.springframework.web.client.RestClient + +@Service +class MyService(restClientBuilder: RestClient.Builder, ssl: RestClientSsl) { + + private val restClient: RestClient + + init { + restClient = restClientBuilder.baseUrl("https://example.org") + .apply(ssl.fromBundle("mybundle")).build() + } + + fun someRestCall(name: String?): Details { + return restClient.get().uri("/{name}/details", name) + .retrieve().body(Details::class.java)!! + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/ssl/settings/Details.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/ssl/settings/Details.kt new file mode 100644 index 000000000000..3a73e355e1c1 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/ssl/settings/Details.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2012-2023 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.docs.io.restclient.restclient.ssl.settings + +class Details diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/ssl/settings/MyService.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/ssl/settings/MyService.kt new file mode 100644 index 000000000000..e153262f8248 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/io/restclient/restclient/ssl/settings/MyService.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2023 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.docs.io.restclient.restclient.ssl.settings + +import org.springframework.boot.ssl.SslBundles +import org.springframework.boot.web.client.ClientHttpRequestFactories +import org.springframework.boot.web.client.ClientHttpRequestFactorySettings +import org.springframework.stereotype.Service +import org.springframework.web.client.RestClient +import java.time.Duration + +@Service +class MyService(restClientBuilder: RestClient.Builder, sslBundles: SslBundles) { + + private val restClient: RestClient + + init { + val settings = ClientHttpRequestFactorySettings.DEFAULTS + .withReadTimeout(Duration.ofMinutes(2)) + .withSslBundle(sslBundles.getBundle("mybundle")) + val requestFactory = ClientHttpRequestFactories.get(settings) + restClient = restClientBuilder + .baseUrl("https://example.org") + .requestFactory(requestFactory).build() + } + + fun someRestCall(name: String?): Details { + return restClient.get().uri("/{name}/details", name).retrieve().body(Details::class.java)!! + } + +} + From 89880a773ca1ac48f09af5d7d8f379f91b0075b7 Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Wed, 5 Jul 2023 18:56:49 -0400 Subject: [PATCH 0105/1215] Add RestClientAutoConfiguration to AutoConfiguration.imports See gh-36249 --- ....springframework.boot.autoconfigure.AutoConfiguration.imports | 1 + 1 file changed, 1 insertion(+) 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 f0018406978d..5997fb8ea2db 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 @@ -123,6 +123,7 @@ org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration +org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration From 3b90919313bd3294c848f4c6c4829a5feb8ffeb8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 6 Jul 2023 12:22:54 +0100 Subject: [PATCH 0106/1215] Polish RestClient auto-config and tests For consistency, replace webClient and WebClient with restClient and RestClient. This also address a bean name clash between RestClientAutoConfiguration's RestClient.Builder bean and WebClientAutoConfiguration's WebClient.Builder bean that were both previously named webClientBuilder. --- .../web/client/RestClientAutoConfiguration.java | 2 +- .../web/client/RestClientAutoConfigurationTests.java | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java index b33fc882f942..1543caa80da0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java @@ -59,7 +59,7 @@ public HttpMessageConvertersRestClientCustomizer httpMessageConvertersRestClient @Bean @Scope("prototype") @ConditionalOnMissingBean - public RestClient.Builder webClientBuilder(ObjectProvider customizerProvider) { + public RestClient.Builder restClientBuilder(ObjectProvider customizerProvider) { RestClient.Builder builder = RestClient.builder() .requestFactory(ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS)); customizerProvider.orderedStream().forEach((customizer) -> customizer.customize(builder)); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java index 368c29d88d63..b64a62d1f3c3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java @@ -61,7 +61,7 @@ void shouldCreateBuilder() { void restClientShouldApplyCustomizers() { this.contextRunner.withUserConfiguration(RestClientCustomizerConfig.class).run((context) -> { RestClient.Builder builder = context.getBean(RestClient.Builder.class); - RestClientCustomizer customizer = context.getBean("webClientCustomizer", RestClientCustomizer.class); + RestClientCustomizer customizer = context.getBean("restClientCustomizer", RestClientCustomizer.class); builder.build(); then(customizer).should().customize(any(RestClient.Builder.class)); }); @@ -80,7 +80,7 @@ void shouldGetPrototypeScopedBean() { void shouldNotCreateClientBuilderIfAlreadyPresent() { this.contextRunner.withUserConfiguration(CustomRestClientBuilderConfig.class).run((context) -> { RestClient.Builder builder = context.getBean(RestClient.Builder.class); - assertThat(builder).isInstanceOf(MyWebClientBuilder.class); + assertThat(builder).isInstanceOf(MyRestClientBuilder.class); }); } @@ -141,7 +141,7 @@ CodecCustomizer myCodecCustomizer() { static class RestClientCustomizerConfig { @Bean - RestClientCustomizer webClientCustomizer() { + RestClientCustomizer restClientCustomizer() { return mock(RestClientCustomizer.class); } @@ -151,13 +151,13 @@ RestClientCustomizer webClientCustomizer() { static class CustomRestClientBuilderConfig { @Bean - MyWebClientBuilder myWebClientBuilder() { - return mock(MyWebClientBuilder.class); + MyRestClientBuilder myRestClientBuilder() { + return mock(MyRestClientBuilder.class); } } - interface MyWebClientBuilder extends RestClient.Builder { + interface MyRestClientBuilder extends RestClient.Builder { } From 3bbfee5e938f03048c1b8b0cc976b536633df73f Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 29 Jun 2023 15:11:20 +0200 Subject: [PATCH 0107/1215] Support JDK HttpClient in ClientHttpRequestFactories See gh-36118 --- .../client/ClientHttpRequestFactories.java | 37 +++++++++++++ ...entHttpRequestFactoriesJdkClientTests.java | 52 +++++++++++++++++++ ...ClientHttpRequestFactoriesSimpleTests.java | 4 +- ...geSenderBuilderSimpleIntegrationTests.java | 2 +- 4 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesJdkClientTests.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java index 347f6c4b0c58..63b6595ae9b3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java @@ -49,6 +49,7 @@ import org.springframework.http.client.AbstractClientHttpRequestFactoryWrapper; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.client.JdkClientHttpRequestFactory; import org.springframework.http.client.JettyClientHttpRequestFactory; import org.springframework.http.client.OkHttp3ClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory; @@ -79,6 +80,10 @@ public final class ClientHttpRequestFactories { private static final boolean JETTY_CLIENT_PRESENT = ClassUtils.isPresent(JETTY_CLIENT_CLASS, null); + static final String JDK_CLIENT_CLASS = "java.net.http.HttpClient"; + + private static final boolean JDK_CLIENT_PRESENT = ClassUtils.isPresent(JDK_CLIENT_CLASS, null); + private ClientHttpRequestFactories() { } @@ -99,6 +104,9 @@ public static ClientHttpRequestFactory get(ClientHttpRequestFactorySettings sett if (JETTY_CLIENT_PRESENT) { return Jetty.get(settings); } + if (JDK_CLIENT_PRESENT) { + return Jdk.get(settings); + } return Simple.get(settings); } @@ -126,6 +134,9 @@ public static T get(Class requestFactory if (requestFactoryType == JettyClientHttpRequestFactory.class) { return (T) Jetty.get(settings); } + if (requestFactoryType == JdkClientHttpRequestFactory.class) { + return (T) Jdk.get(settings); + } if (requestFactoryType == SimpleClientHttpRequestFactory.class) { return (T) Simple.get(settings); } @@ -254,6 +265,32 @@ private static JettyClientHttpRequestFactory createRequestFactory(SslBundle sslB } + /** + * Support for {@link JdkClientHttpRequestFactory}. + */ + static class Jdk { + + static JdkClientHttpRequestFactory get(ClientHttpRequestFactorySettings settings) { + java.net.http.HttpClient httpClient = createHttpClient(settings.connectTimeout(), settings.sslBundle()); + JdkClientHttpRequestFactory requestFactory = new JdkClientHttpRequestFactory(httpClient); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(settings::readTimeout).asInt(Duration::toMillis).to(requestFactory::setReadTimeout); + return requestFactory; + } + + private static java.net.http.HttpClient createHttpClient(Duration connectTimeout, SslBundle sslBundle) { + java.net.http.HttpClient.Builder builder = java.net.http.HttpClient.newBuilder(); + if (connectTimeout != null) { + builder.connectTimeout(connectTimeout); + } + if (sslBundle != null) { + builder.sslContext(sslBundle.createSslContext()); + } + return builder.build(); + } + + } + /** * Support for {@link SimpleClientHttpRequestFactory}. */ diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesJdkClientTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesJdkClientTests.java new file mode 100644 index 000000000000..a3dd693ec7b2 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesJdkClientTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2023 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.web.client; + +import java.net.http.HttpClient; +import java.time.Duration; + +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; +import org.springframework.http.client.JdkClientHttpRequestFactory; +import org.springframework.test.util.ReflectionTestUtils; + +/** + * Tests for {@link ClientHttpRequestFactories} when JDK HttpClient is the + * predominant HTTP client. + * + * @author Andy Wilkinson + */ +@ClassPathExclusions({ "httpclient5-*.jar", "okhttp-*.jar" }) +class ClientHttpRequestFactoriesJdkClientTests + extends AbstractClientHttpRequestFactoriesTests { + + ClientHttpRequestFactoriesJdkClientTests() { + super(JdkClientHttpRequestFactory.class); + } + + @Override + protected long connectTimeout(JdkClientHttpRequestFactory requestFactory) { + HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(requestFactory, "httpClient"); + return httpClient.connectTimeout().map(Duration::toMillis).orElse(-1L); + } + + @Override + @SuppressWarnings("unchecked") + protected long readTimeout(JdkClientHttpRequestFactory requestFactory) { + return ((Duration) ReflectionTestUtils.getField(requestFactory, "readTimeout")).toMillis(); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesSimpleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesSimpleTests.java index f00882bc7df5..189901f20c42 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesSimpleTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesSimpleTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -26,7 +26,7 @@ * * @author Andy Wilkinson */ -@ClassPathExclusions({ "httpclient5-*.jar", "okhttp-*.jar", "jetty-client-*.jar" }) +@ClassPathExclusions(files = {"httpclient5-*.jar", "jetty-client-*.jar", "okhttp-*.jar"}, packages = "java.net.http") class ClientHttpRequestFactoriesSimpleTests extends AbstractClientHttpRequestFactoriesTests { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderSimpleIntegrationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderSimpleIntegrationTests.java index 0014d47cd7ef..5872c3a092dc 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderSimpleIntegrationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderSimpleIntegrationTests.java @@ -34,7 +34,7 @@ * * @author Stephane Nicoll */ -@ClassPathExclusions(files = { "httpclient5-*.jar", "jetty-client-*.jar", "okhttp*.jar" }) +@ClassPathExclusions(files = { "httpclient5-*.jar", "jetty-client-*.jar", "okhttp*.jar" }, packages = "java.net.http") class HttpWebServiceMessageSenderBuilderSimpleIntegrationTests { private final HttpWebServiceMessageSenderBuilder builder = new HttpWebServiceMessageSenderBuilder(); From bb2c4cc742b9b2770ea3809d8c0d9b74855d514e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 4 Jul 2023 18:12:27 +0100 Subject: [PATCH 0108/1215] Polish "Support JDK HttpClient in ClientHttpRequestFactories" See gh-36118 --- .../client/ClientHttpRequestFactories.java | 7 --- ...entHttpRequestFactoriesJdkClientTests.java | 52 ------------------- ...ClientHttpRequestFactoriesSimpleTests.java | 2 +- .../ClientHttpRequestFactoriesTests.java | 8 +++ ...geSenderBuilderSimpleIntegrationTests.java | 2 +- 5 files changed, 10 insertions(+), 61 deletions(-) delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesJdkClientTests.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java index 63b6595ae9b3..1af4d92bc901 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java @@ -80,10 +80,6 @@ public final class ClientHttpRequestFactories { private static final boolean JETTY_CLIENT_PRESENT = ClassUtils.isPresent(JETTY_CLIENT_CLASS, null); - static final String JDK_CLIENT_CLASS = "java.net.http.HttpClient"; - - private static final boolean JDK_CLIENT_PRESENT = ClassUtils.isPresent(JDK_CLIENT_CLASS, null); - private ClientHttpRequestFactories() { } @@ -104,9 +100,6 @@ public static ClientHttpRequestFactory get(ClientHttpRequestFactorySettings sett if (JETTY_CLIENT_PRESENT) { return Jetty.get(settings); } - if (JDK_CLIENT_PRESENT) { - return Jdk.get(settings); - } return Simple.get(settings); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesJdkClientTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesJdkClientTests.java deleted file mode 100644 index a3dd693ec7b2..000000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesJdkClientTests.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2012-2023 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.web.client; - -import java.net.http.HttpClient; -import java.time.Duration; - -import org.springframework.boot.testsupport.classpath.ClassPathExclusions; -import org.springframework.http.client.JdkClientHttpRequestFactory; -import org.springframework.test.util.ReflectionTestUtils; - -/** - * Tests for {@link ClientHttpRequestFactories} when JDK HttpClient is the - * predominant HTTP client. - * - * @author Andy Wilkinson - */ -@ClassPathExclusions({ "httpclient5-*.jar", "okhttp-*.jar" }) -class ClientHttpRequestFactoriesJdkClientTests - extends AbstractClientHttpRequestFactoriesTests { - - ClientHttpRequestFactoriesJdkClientTests() { - super(JdkClientHttpRequestFactory.class); - } - - @Override - protected long connectTimeout(JdkClientHttpRequestFactory requestFactory) { - HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(requestFactory, "httpClient"); - return httpClient.connectTimeout().map(Duration::toMillis).orElse(-1L); - } - - @Override - @SuppressWarnings("unchecked") - protected long readTimeout(JdkClientHttpRequestFactory requestFactory) { - return ((Duration) ReflectionTestUtils.getField(requestFactory, "readTimeout")).toMillis(); - } - -} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesSimpleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesSimpleTests.java index 189901f20c42..bb4425484511 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesSimpleTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesSimpleTests.java @@ -26,7 +26,7 @@ * * @author Andy Wilkinson */ -@ClassPathExclusions(files = {"httpclient5-*.jar", "jetty-client-*.jar", "okhttp-*.jar"}, packages = "java.net.http") +@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar", "okhttp-*.jar" }) class ClientHttpRequestFactoriesSimpleTests extends AbstractClientHttpRequestFactoriesTests { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesTests.java index 546b862d987a..239023e8b9c0 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesTests.java @@ -27,6 +27,7 @@ import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.client.JdkClientHttpRequestFactory; import org.springframework.http.client.OkHttp3ClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory; @@ -75,6 +76,13 @@ void getOfOkHttpFactoryReturnsOkHttpFactory() { assertThat(requestFactory).isInstanceOf(OkHttp3ClientHttpRequestFactory.class); } + @Test + void getOfJdkFactoryReturnsJdkFactory() { + ClientHttpRequestFactory requestFactory = ClientHttpRequestFactories.get(JdkClientHttpRequestFactory.class, + ClientHttpRequestFactorySettings.DEFAULTS); + assertThat(requestFactory).isInstanceOf(JdkClientHttpRequestFactory.class); + } + @Test void getOfUnknownTypeCreatesFactory() { ClientHttpRequestFactory requestFactory = ClientHttpRequestFactories.get(TestClientHttpRequestFactory.class, diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderSimpleIntegrationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderSimpleIntegrationTests.java index 5872c3a092dc..6c3a0b2ef18e 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderSimpleIntegrationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderSimpleIntegrationTests.java @@ -34,7 +34,7 @@ * * @author Stephane Nicoll */ -@ClassPathExclusions(files = { "httpclient5-*.jar", "jetty-client-*.jar", "okhttp*.jar" }, packages = "java.net.http") +@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar", "okhttp*.jar" }) class HttpWebServiceMessageSenderBuilderSimpleIntegrationTests { private final HttpWebServiceMessageSenderBuilder builder = new HttpWebServiceMessageSenderBuilder(); From 62674de4728ad542a57e4c438eed1c20c3cde07c Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Thu, 6 Jul 2023 15:57:25 -0400 Subject: [PATCH 0109/1215] Skip int conversion in ClientHttpRequestFactories @poutsma added `JdkClientHttpRequestFactory.setReadTimeout(Duration)` so the conversion to and from int is no longer needed. See gh-36270 --- .../boot/web/client/ClientHttpRequestFactories.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java index 0d9847c6237d..93e4e407c862 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java @@ -285,7 +285,7 @@ static JdkClientHttpRequestFactory get(ClientHttpRequestFactorySettings settings java.net.http.HttpClient httpClient = createHttpClient(settings.connectTimeout(), settings.sslBundle()); JdkClientHttpRequestFactory requestFactory = new JdkClientHttpRequestFactory(httpClient); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(settings::readTimeout).asInt(Duration::toMillis).to(requestFactory::setReadTimeout); + map.from(settings::readTimeout).to(requestFactory::setReadTimeout); return requestFactory; } From f4e05c91c74ee65e242febd4d46543e8f74c26c7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 6 Jul 2023 20:17:10 +0100 Subject: [PATCH 0110/1215] Use converter beans in preference to ObjectToObjectConverter Previously, with the converter beans in a conversion service that appears after the bean factory's conversion service, they would not be called for a conversion that could be handled by the ObjectToObjectConverter in the bean factory's conversion service. This commit creates a new FormattingConversionService that is empty except for the converter beans and places it first in the list. It's followed by the bean factory's conversion service. The shared application conversion service is added to the end of the list to pick up any conversions that the previous two services could not handle. This should maintain backwards compatibility with the previous arrangement where the converter beans were added to an application conversion service that went after the bean factory's conversion service. Fixes gh-34631 --- .../properties/ConversionServiceDeducer.java | 16 ++++-- .../ConfigurationPropertiesTests.java | 57 ++++++++++++------- .../ConversionServiceDeducerTests.java | 10 ++-- 3 files changed, 54 insertions(+), 29 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java index 3593faf4230a..775680c794ae 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java @@ -31,6 +31,7 @@ import org.springframework.core.convert.converter.GenericConverter; import org.springframework.format.Formatter; import org.springframework.format.FormatterRegistry; +import org.springframework.format.support.FormattingConversionService; /** * Utility to deduce the {@link ConversionService} to use for configuration properties @@ -59,15 +60,22 @@ List getConversionServices() { private List getConversionServices(ConfigurableApplicationContext applicationContext) { List conversionServices = new ArrayList<>(); - if (applicationContext.getBeanFactory().getConversionService() != null) { - conversionServices.add(applicationContext.getBeanFactory().getConversionService()); - } ConverterBeans converterBeans = new ConverterBeans(applicationContext); if (!converterBeans.isEmpty()) { - ApplicationConversionService beansConverterService = new ApplicationConversionService(); + FormattingConversionService beansConverterService = new FormattingConversionService(); converterBeans.addTo(beansConverterService); conversionServices.add(beansConverterService); } + if (applicationContext.getBeanFactory().getConversionService() != null) { + conversionServices.add(applicationContext.getBeanFactory().getConversionService()); + } + if (!converterBeans.isEmpty()) { + // Converters beans used to be added to a custom ApplicationConversionService + // after the BeanFactory's ConversionService. For backwards compatibility, we + // add an ApplicationConversationService as a fallback in the same place in + // the list. + conversionServices.add(ApplicationConversionService.getSharedInstance()); + } return conversionServices; } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java index a0683316bbc1..0e7a8fe02a0d 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java @@ -644,24 +644,36 @@ void customProtocolResolver() { @Test void loadShouldUseConverterBean() { - prepareConverterContext(ConverterConfiguration.class, PersonProperties.class); + prepareConverterContext(PersonConverterConfiguration.class, PersonProperties.class); Person person = this.context.getBean(PersonProperties.class).getPerson(); assertThat(person.firstName).isEqualTo("John"); assertThat(person.lastName).isEqualTo("Smith"); } @Test - void loadWhenBeanFactoryConversionServiceAndConverterBean() { + void loadWhenBeanFactoryConversionServiceAndConverterBeanCanUseBeanFactoryConverter() { DefaultConversionService conversionService = new DefaultConversionService(); conversionService.addConverter(new AlienConverter()); this.context.getBeanFactory().setConversionService(conversionService); - load(new Class[] { ConverterConfiguration.class, PersonAndAlienProperties.class }, "test.person=John Smith", - "test.alien=Alf Tanner"); + load(new Class[] { PersonConverterConfiguration.class, PersonAndAlienProperties.class }, + "test.person=John Smith", "test.alien=Alf Tanner"); PersonAndAlienProperties properties = this.context.getBean(PersonAndAlienProperties.class); assertThat(properties.getPerson().firstName).isEqualTo("John"); assertThat(properties.getPerson().lastName).isEqualTo("Smith"); - assertThat(properties.getAlien().firstName).isEqualTo("Alf"); - assertThat(properties.getAlien().lastName).isEqualTo("Tanner"); + assertThat(properties.getAlien().name).isEqualTo("rennaT flA"); + } + + @Test + void loadWhenBeanFactoryConversionServiceAndConverterBeanCanUseConverterBean() { + DefaultConversionService conversionService = new DefaultConversionService(); + conversionService.addConverter(new PersonConverter()); + this.context.getBeanFactory().setConversionService(conversionService); + load(new Class[] { AlienConverterConfiguration.class, PersonAndAlienProperties.class }, + "test.person=John Smith", "test.alien=Alf Tanner"); + PersonAndAlienProperties properties = this.context.getBean(PersonAndAlienProperties.class); + assertThat(properties.getPerson().firstName).isEqualTo("John"); + assertThat(properties.getPerson().lastName).isEqualTo("Smith"); + assertThat(properties.getAlien().name).isEqualTo("rennaT flA"); } @Test @@ -1440,7 +1452,7 @@ public Resource resolve(String location, ResourceLoader resourceLoader) { } @Configuration(proxyBeanMethods = false) - static class ConverterConfiguration { + static class PersonConverterConfiguration { @Bean @ConfigurationPropertiesBinding @@ -1450,6 +1462,17 @@ Converter personConverter() { } + @Configuration(proxyBeanMethods = false) + static class AlienConverterConfiguration { + + @Bean + @ConfigurationPropertiesBinding + Converter alienConverter() { + return new AlienConverter(); + } + + } + @Configuration(proxyBeanMethods = false) static class NonQualifiedConverterConfiguration { @@ -2398,8 +2421,7 @@ static class AlienConverter implements Converter { @Override public Alien convert(String source) { - String[] content = StringUtils.split(source, " "); - return new Alien(content[0], content[1]); + return new Alien(new StringBuilder(source).reverse().toString()); } } @@ -2467,21 +2489,14 @@ String getLastName() { static class Alien { - private final String firstName; - - private final String lastName; - - Alien(String firstName, String lastName) { - this.firstName = firstName; - this.lastName = lastName; - } + private final String name; - String getFirstName() { - return this.firstName; + Alien(String name) { + this.name = name; } - String getLastName() { - return this.lastName; + String getName() { + return this.name; } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConversionServiceDeducerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConversionServiceDeducerTests.java index 30286fe950b5..06b27871c9ed 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConversionServiceDeducerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConversionServiceDeducerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -30,6 +30,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.Converter; +import org.springframework.format.support.FormattingConversionService; import static org.assertj.core.api.Assertions.assertThat; @@ -69,14 +70,15 @@ void getConversionServiceWhenHasNoConversionServiceBeanAndNoQualifiedBeansAndBea } @Test - void getConversionServiceWhenHasQualifiedConverterBeansContainsCustomizedApplicationService() { + void getConversionServiceWhenHasQualifiedConverterBeansContainsCustomizedFormattingService() { ApplicationContext applicationContext = new AnnotationConfigApplicationContext( CustomConverterConfiguration.class); ConversionServiceDeducer deducer = new ConversionServiceDeducer(applicationContext); List conversionServices = deducer.getConversionServices(); - assertThat(conversionServices).hasSize(1); - assertThat(conversionServices.get(0)).isNotSameAs(ApplicationConversionService.getSharedInstance()); + assertThat(conversionServices).hasSize(2); + assertThat(conversionServices.get(0)).isExactlyInstanceOf(FormattingConversionService.class); assertThat(conversionServices.get(0).canConvert(InputStream.class, OutputStream.class)).isTrue(); + assertThat(conversionServices.get(1)).isSameAs(ApplicationConversionService.getSharedInstance()); } @Configuration(proxyBeanMethods = false) From a285a7389c7b72f8a7fb43915396cdca82f3214b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 10 Jul 2023 14:02:25 +0100 Subject: [PATCH 0111/1215] Start building against Spring LDAP 3.2.0 snapshots See gh-36299 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 58ab83852479..e2be6869345d 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1433,7 +1433,7 @@ bom { ] } } - library("Spring LDAP", "3.1.0") { + library("Spring LDAP", "3.2.0-SNAPSHOT") { group("org.springframework.ldap") { modules = [ "spring-ldap-core", From 7e5bfc4b2e88eb4a841df3108ee4344d2e486817 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 10 Jul 2023 16:19:05 +0100 Subject: [PATCH 0112/1215] Start building against Reactor Bom 2023.0.0 snapshots See gh-36300 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e2be6869345d..64e63f6405d7 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1205,7 +1205,7 @@ bom { ] } } - library("Reactor Bom", "2022.0.8") { + library("Reactor Bom", "2023.0.0-SNAPSHOT") { group("io.projectreactor") { imports = [ "reactor-bom" From ad72d22c9049262c5ebaa39e57b8fd78d97b2400 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 17:00:11 +0200 Subject: [PATCH 0113/1215] Upgrade to Micrometer 1.12.0-M1 Closes gh-36188 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 64e63f6405d7..d777030ddd33 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -972,7 +972,7 @@ bom { ] } } - library("Micrometer", "1.12.0-SNAPSHOT") { + library("Micrometer", "1.12.0-M1") { group("io.micrometer") { modules = [ "micrometer-registry-stackdriver" { From 122f6599c0e69cbe141ae8943d876f3f77d33d70 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 17:00:43 +0200 Subject: [PATCH 0114/1215] Upgrade to Micrometer Tracing 1.2.0-M1 Closes gh-36199 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d777030ddd33..c396ebe000c9 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -984,7 +984,7 @@ bom { ] } } - library("Micrometer Tracing", "1.2.0-SNAPSHOT") { + library("Micrometer Tracing", "1.2.0-M1") { group("io.micrometer") { imports = [ "micrometer-tracing-bom" From c59d474ec4e586a437c50afd6b19ae6973d4db64 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 17:01:10 +0200 Subject: [PATCH 0115/1215] Upgrade to Reactor Bom 2023.0.0-M1 Closes gh-36300 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c396ebe000c9..cfa28cc5a1f0 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1205,7 +1205,7 @@ bom { ] } } - library("Reactor Bom", "2023.0.0-SNAPSHOT") { + library("Reactor Bom", "2023.0.0-M1") { group("io.projectreactor") { imports = [ "reactor-bom" From 556fcc92bb786b55f0d14784a70ff0040641a0c4 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:13:22 +0200 Subject: [PATCH 0116/1215] Upgrade to ActiveMQ 5.18.2 Closes gh-36348 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index cfa28cc5a1f0..b9b729ec3296 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -14,7 +14,7 @@ bom { issueLabels = ["type: dependency-upgrade"] } } - library("ActiveMQ", "5.18.1") { + library("ActiveMQ", "5.18.2") { group("org.apache.activemq") { modules = [ "activemq-amqp", From 49f5002241a9a064a0bab52de5d202c2c694f79b Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:13:22 +0200 Subject: [PATCH 0117/1215] Upgrade to Artemis 2.29.0 Closes gh-36349 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index b9b729ec3296..2d09c05c392b 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -65,7 +65,7 @@ bom { ] } } - library("Artemis", "2.28.0") { + library("Artemis", "2.29.0") { group("org.apache.activemq") { modules = [ "artemis-amqp-protocol", From ee1044f4a55ff81c6dc8700bbef47f98b7174dda Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:13:23 +0200 Subject: [PATCH 0118/1215] Upgrade to Commons Codec 1.16.0 Closes gh-36350 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 2d09c05c392b..fd699ea2a444 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -172,7 +172,7 @@ bom { ] } } - library("Commons Codec", "1.15") { + library("Commons Codec", "1.16.0") { group("commons-codec") { modules = [ "commons-codec" From 5e2575371dcb50c8e9e26b6a0db493e515bc81ec Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:13:28 +0200 Subject: [PATCH 0119/1215] Upgrade to Dependency Management Plugin 1.1.1 Closes gh-36363 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index fd699ea2a444..85b438ad7643 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -223,7 +223,7 @@ bom { ] } } - library("Dependency Management Plugin", "1.1.0") { + library("Dependency Management Plugin", "1.1.1") { group("io.spring.gradle") { modules = [ "dependency-management-plugin" From 85194c703bca5095973cb93b16db361b10891c46 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:13:28 +0200 Subject: [PATCH 0120/1215] Upgrade to Elasticsearch Client 8.8.2 Closes gh-36351 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 85b438ad7643..04008356de13 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -262,7 +262,7 @@ bom { ] } } - library("Elasticsearch Client", "8.8.1") { + library("Elasticsearch Client", "8.8.2") { group("org.elasticsearch.client") { modules = [ "elasticsearch-rest-client" { From c6ffcb94c0dab481e7c3eb0009e45ed31e415a0a Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:13:40 +0200 Subject: [PATCH 0121/1215] Upgrade to GraphQL Java 20.4 Closes gh-36365 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 04008356de13..9404079fe497 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -320,7 +320,7 @@ bom { ] } } - library("GraphQL Java", "20.2") { + library("GraphQL Java", "20.4") { group("com.graphql-java") { modules = [ "graphql-java" From d08c2f072350eb73f7b5d79e9731ac3cc896e67f Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:13:45 +0200 Subject: [PATCH 0122/1215] Upgrade to Groovy 4.0.13 Closes gh-36366 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 9404079fe497..a7dff9f051f6 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -327,7 +327,7 @@ bom { ] } } - library("Groovy", "4.0.12") { + library("Groovy", "4.0.13") { group("org.apache.groovy") { imports = [ "groovy-bom" From 2073f1d194792bd5dfffeb3cd66fccd1a1a0e1e9 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:13:50 +0200 Subject: [PATCH 0123/1215] Upgrade to H2 2.2.220 Closes gh-36367 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index a7dff9f051f6..5352c16fc8c0 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -341,7 +341,7 @@ bom { ] } } - library("H2", "2.1.214") { + library("H2", "2.2.220") { group("com.h2database") { modules = [ "h2" From 2f9d016581cc55a675c8576f493cc2302f07e50e Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:13:57 +0200 Subject: [PATCH 0124/1215] Upgrade to Hibernate 6.2.6.Final Closes gh-36368 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5352c16fc8c0..5f84cef2fe01 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -365,7 +365,7 @@ bom { ] } } - library("Hibernate", "6.2.5.Final") { + library("Hibernate", "6.2.6.Final") { group("org.hibernate.orm") { modules = [ "hibernate-agroal", From c6128904eebe8022becd39468f4e502889fed778 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:14:02 +0200 Subject: [PATCH 0125/1215] Upgrade to Hibernate Validator 8.0.1.Final Closes gh-36369 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5f84cef2fe01..55e0cb591358 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -386,7 +386,7 @@ bom { ] } } - library("Hibernate Validator", "8.0.0.Final") { + library("Hibernate Validator", "8.0.1.Final") { group("org.hibernate.validator") { modules = [ "hibernate-validator", From d6d93fb1125d800b4a05bf62459b44130a9ba696 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:14:08 +0200 Subject: [PATCH 0126/1215] Upgrade to Infinispan 14.0.12.Final Closes gh-36370 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 55e0cb591358..a3bfaab3703c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -453,7 +453,7 @@ bom { ] } } - library("Infinispan", "14.0.11.Final") { + library("Infinispan", "14.0.12.Final") { group("org.infinispan") { imports = [ "infinispan-bom" From 3e9eeea2b0df11aea6b853d7d3aebfb3ed6021c5 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:14:13 +0200 Subject: [PATCH 0127/1215] Upgrade to Jakarta WebSocket 2.1.1 Closes gh-36371 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index a3bfaab3703c..6f6e2752a422 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -558,7 +558,7 @@ bom { ] } } - library("Jakarta WebSocket", "2.1.0") { + library("Jakarta WebSocket", "2.1.1") { group("jakarta.websocket") { modules = [ "jakarta.websocket-api", From f05ec7e3001947928ef05b78498059cef0dbd720 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:14:18 +0200 Subject: [PATCH 0128/1215] Upgrade to Janino 3.1.10 Closes gh-36372 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 6f6e2752a422..3f6ff8eb4f11 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -594,7 +594,7 @@ bom { ] } } - library("Janino", "3.1.9") { + library("Janino", "3.1.10") { group("org.codehaus.janino") { modules = [ "commons-compiler", From 590526717de45aa789efb11d6e7992cfd3a4b7b1 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:14:23 +0200 Subject: [PATCH 0129/1215] Upgrade to JBoss Logging 3.5.3.Final Closes gh-36373 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 3f6ff8eb4f11..6a6230807924 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -631,7 +631,7 @@ bom { ] } } - library("JBoss Logging", "3.5.1.Final") { + library("JBoss Logging", "3.5.3.Final") { group("org.jboss.logging") { modules = [ "jboss-logging" From 1c0217f40aa9ec3626fdada9e1cd895b05d0fa6a Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:14:28 +0200 Subject: [PATCH 0130/1215] Upgrade to jOOQ 3.18.5 Closes gh-36374 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 6a6230807924..d9ae39a7c2db 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -680,7 +680,7 @@ bom { ] } } - library("jOOQ", "3.18.4") { + library("jOOQ", "3.18.5") { group("org.jooq") { modules = [ "jooq", From 11898c0b56cc86b99f5d6f8282daa085329cdba2 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:14:32 +0200 Subject: [PATCH 0131/1215] Upgrade to Json-smart 2.5.0 Closes gh-36375 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d9ae39a7c2db..890393268cf4 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -701,7 +701,7 @@ bom { ] } } - library("Json-smart", "2.4.11") { + library("Json-smart", "2.5.0") { group("net.minidev") { modules = [ "json-smart" From 9a8b3246a43ec6a2db190fbb5408ed5ffd017cae Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:14:39 +0200 Subject: [PATCH 0132/1215] Upgrade to Kotlin Coroutines 1.7.2 Closes gh-36376 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 890393268cf4..bcf608622783 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -787,7 +787,7 @@ bom { ] } } - library("Kotlin Coroutines", "1.7.1") { + library("Kotlin Coroutines", "1.7.2") { group("org.jetbrains.kotlinx") { imports = [ "kotlinx-coroutines-bom" From 5e3445ad4a6d651b6a20ca7f77c7e274498dc5db Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:14:49 +0200 Subject: [PATCH 0133/1215] Upgrade to Maven Clean Plugin 3.3.1 Closes gh-36378 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index bcf608622783..01e701351ac7 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -860,7 +860,7 @@ bom { ] } } - library("Maven Clean Plugin", "3.2.0") { + library("Maven Clean Plugin", "3.3.1") { group("org.apache.maven.plugins") { plugins = [ "maven-clean-plugin" From 2d472c526f226541c1a7e8aa24d027714f246fa8 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:14:55 +0200 Subject: [PATCH 0134/1215] Upgrade to MongoDB 4.10.2 Closes gh-36379 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 01e701351ac7..d5e6d9a390dc 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -998,7 +998,7 @@ bom { ] } } - library("MongoDB", "4.9.1") { + library("MongoDB", "4.10.2") { group("org.mongodb") { modules = [ "bson", From 85d9cddd252b2b2ee8fa0cfd692d57e37aa89fc3 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:15:01 +0200 Subject: [PATCH 0135/1215] Upgrade to Neo4j Java Driver 5.10.0 Closes gh-36380 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d5e6d9a390dc..52b0639da7e5 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1044,7 +1044,7 @@ bom { ] } } - library("Neo4j Java Driver", "5.9.0") { + library("Neo4j Java Driver", "5.10.0") { group("org.neo4j.driver") { modules = [ "neo4j-java-driver" From 7cb5b96919219d377c9cddf17c05430c2d9b114d Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:15:07 +0200 Subject: [PATCH 0136/1215] Upgrade to OpenTelemetry 1.28.0 Closes gh-36381 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 52b0639da7e5..b8edb3f85fcc 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1065,7 +1065,7 @@ bom { ] } } - library("OpenTelemetry", "1.27.0") { + library("OpenTelemetry", "1.28.0") { group("io.opentelemetry") { imports = [ "opentelemetry-bom" From f689f68695d0f6036c280c0a842699ed691a0858 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:15:14 +0200 Subject: [PATCH 0137/1215] Upgrade to Rabbit Stream Client 0.11.0 Closes gh-36382 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index b8edb3f85fcc..6dbc9ae4b644 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1191,7 +1191,7 @@ bom { ] } } - library("Rabbit Stream Client", "0.10.0") { + library("Rabbit Stream Client", "0.11.0") { group("com.rabbitmq") { modules = [ "stream-client" From 725774de5b492b0223d46fcb53d6833e867668c2 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:15:18 +0200 Subject: [PATCH 0138/1215] Upgrade to Tomcat 10.1.11 Closes gh-36383 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d4321ef602e8..7e5a78d8f27f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,6 @@ org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 kotlinVersion=1.8.22 nativeBuildToolsVersion=0.9.23 springFrameworkVersion=6.1.0-SNAPSHOT -tomcatVersion=10.1.10 +tomcatVersion=10.1.11 kotlin.stdlib.default.dependency=false From 6684404f67757bfdcaeb491320dc7447591a2fe0 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:15:23 +0200 Subject: [PATCH 0139/1215] Upgrade to WebJars Locator Core 0.53 Closes gh-36384 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 6dbc9ae4b644..38f2a7b17351 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1565,7 +1565,7 @@ bom { ] } } - library("WebJars Locator Core", "0.52") { + library("WebJars Locator Core", "0.53") { group("org.webjars") { modules = [ "webjars-locator-core" From 83b7b902c0b5806b0c2709b03cbaba7dd6071106 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Jul 2023 18:15:28 +0200 Subject: [PATCH 0140/1215] Upgrade to XML Maven Plugin 1.1.0 Closes gh-36385 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 38f2a7b17351..ced8584a9ff1 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1579,7 +1579,7 @@ bom { ] } } - library("XML Maven Plugin", "1.0.2") { + library("XML Maven Plugin", "1.1.0") { group("org.codehaus.mojo") { plugins = [ "xml-maven-plugin" From 0d8bae5953c8d82be1f645170d4cc346ff9925c2 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 13 Jul 2023 15:34:23 +0200 Subject: [PATCH 0141/1215] Prohibit upgrades to Liquibase 4.23.0 Closes gh-36377 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index ced8584a9ff1..347a1b4327ba 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -803,7 +803,7 @@ bom { } library("Liquibase", "4.20.0") { prohibit { - versionRange "[4.21.0,4.22.0]" + versionRange "[4.21.0,4.23.0]" because "https://github.com/liquibase/liquibase/issues/4135" } group("org.liquibase") { From 605ceaf471c8fae060a0d432fadae4e72785cfca Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 13 Jul 2023 16:13:00 +0200 Subject: [PATCH 0142/1215] Upgrade to Spring Framework 6.1.0-M2 Closes gh-36198 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7e5a78d8f27f..030ee45e65a8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 kotlinVersion=1.8.22 nativeBuildToolsVersion=0.9.23 -springFrameworkVersion=6.1.0-SNAPSHOT +springFrameworkVersion=6.1.0-M2 tomcatVersion=10.1.11 kotlin.stdlib.default.dependency=false From 1ced770f1584b17891d3f333026d15fabc1005da Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Fri, 14 Jul 2023 19:11:57 +0200 Subject: [PATCH 0143/1215] Upgrade to Spring Data Bom 2023.1.0-M1 Closes gh-36190 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 347a1b4327ba..ce5ed0a3a23e 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1389,7 +1389,7 @@ bom { ] } } - library("Spring Data Bom", "2023.1.0-SNAPSHOT") { + library("Spring Data Bom", "2023.1.0-M1") { group("org.springframework.data") { imports = [ "spring-data-bom" From 7692171119d9cebc69212d6c787055adb6d79da4 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Fri, 14 Jul 2023 19:12:38 +0200 Subject: [PATCH 0144/1215] Upgrade to Spring HATEOAS 2.2.0-M1 Closes gh-36192 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index ce5ed0a3a23e..2adbffd429e4 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1411,7 +1411,7 @@ bom { ] } } - library("Spring HATEOAS", "2.2.0-SNAPSHOT") { + library("Spring HATEOAS", "2.2.0-M1") { group("org.springframework.hateoas") { modules = [ "spring-hateoas" From b8c4fb6b9a20bad8ffa6d781a8bf38681562ffed Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 14 Jul 2023 19:56:44 +0100 Subject: [PATCH 0145/1215] Upgrade to Liquibase 4.23.0 Closes gh-36377 --- .../spring-boot-actuator-autoconfigure/build.gradle | 1 + spring-boot-project/spring-boot-dependencies/build.gradle | 6 +----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index 078187aff0cd..a165eeee1b00 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -106,6 +106,7 @@ dependencies { optional("org.hibernate.validator:hibernate-validator") optional("org.influxdb:influxdb-java") optional("org.liquibase:liquibase-core") { + exclude group: "javax.activation", module: "javax.activation-api" exclude group: "javax.xml.bind", module: "jaxb-api" } optional("org.mongodb:mongodb-driver-reactivestreams") diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 2adbffd429e4..549041507fc8 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -801,11 +801,7 @@ bom { ] } } - library("Liquibase", "4.20.0") { - prohibit { - versionRange "[4.21.0,4.23.0]" - because "https://github.com/liquibase/liquibase/issues/4135" - } + library("Liquibase", "4.23.0") { group("org.liquibase") { modules = [ "liquibase-cdi", From f33874e98ee6e5026089a4bc9091559744cd5c75 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 17 Jul 2023 09:47:54 +0100 Subject: [PATCH 0146/1215] Allow auto-configured applicationTaskExecutor to use virtual threads With this commit, when virtual threads are enabled, the auto-configured applicationTaskExecutor changes from a ThreadPoolTaskExecutor to a SimpleAsyncTaskExecutor with virtual threads enabled. As before, any TaskDecorator bean is applied to the auto-configured executor and the spring.task.execution.thread-name-prefix property is applied. Other spring.task.execution.* properties are ignored as they are specific to a pool-based executor. Closes gh-35710 --- .../task/TaskExecutionAutoConfiguration.java | 3 + .../task/TaskExecutorConfigurations.java | 72 +++++++++++++++++ .../TaskExecutionAutoConfigurationTests.java | 78 ++++++++++++++++++- .../task-execution-and-scheduling.adoc | 7 +- 4 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java index 1ebb19871931..0ee647f712c0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java @@ -28,6 +28,7 @@ import org.springframework.boot.task.TaskExecutorBuilder; import org.springframework.boot.task.TaskExecutorCustomizer; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Lazy; import org.springframework.core.task.TaskDecorator; import org.springframework.core.task.TaskExecutor; @@ -44,6 +45,8 @@ @ConditionalOnClass(ThreadPoolTaskExecutor.class) @AutoConfiguration @EnableConfigurationProperties(TaskExecutionProperties.class) +@Import({ TaskExecutorConfigurations.VirtualThreadTaskExecutorConfiguration.class, + TaskExecutorConfigurations.ThreadPoolTaskExecutorConfiguration.class }) public class TaskExecutionAutoConfiguration { /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java new file mode 100644 index 000000000000..f80bbaf493b5 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2023 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.task; + +import java.util.concurrent.Executor; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnVirtualThreads; +import org.springframework.boot.task.TaskExecutorBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.TaskDecorator; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +/** + * {@link TaskExecutor} configurations to be imported by + * {@link TaskExecutionAutoConfiguration} in a specific order. + * + * @author Andy Wilkinson + */ +class TaskExecutorConfigurations { + + @ConditionalOnVirtualThreads + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(Executor.class) + static class VirtualThreadTaskExecutorConfiguration { + + @Bean(name = { TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME, + AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME }) + SimpleAsyncTaskExecutor applicationTaskExecutor(TaskExecutionProperties properties, + ObjectProvider taskDecorator) { + SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(properties.getThreadNamePrefix()); + executor.setVirtualThreads(true); + executor.setTaskDecorator(taskDecorator.getIfUnique()); + return executor; + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(Executor.class) + static class ThreadPoolTaskExecutorConfiguration { + + @Lazy + @Bean(name = { TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME, + AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME }) + ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) { + return builder.build(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java index c1d0507903ac..8a1e09e922f0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java @@ -17,11 +17,16 @@ package org.springframework.boot.autoconfigure.task; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.config.BeanDefinition; @@ -34,6 +39,7 @@ import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.core.task.SyncTaskExecutor; import org.springframework.core.task.TaskDecorator; import org.springframework.core.task.TaskExecutor; @@ -98,7 +104,7 @@ void taskExecutorBuilderShouldUseTaskDecorator() { } @Test - void taskExecutorAutoConfiguredIsLazy() { + void whenThreadPoolTaskExecutorIsAutoConfiguredThenItIsLazy() { this.contextRunner.run((context) -> { assertThat(context).hasSingleBean(Executor.class).hasBean("applicationTaskExecutor"); BeanDefinition beanDefinition = context.getSourceApplicationContext() @@ -109,6 +115,51 @@ void taskExecutorAutoConfiguredIsLazy() { }); } + @Test + @DisabledForJreRange(max = JRE.JAVA_20) + void whenVirtualThreadsAreEnabledThenSimpleAsyncTaskExecutorWithVirtualThreadsIsAutoConfigured() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> { + assertThat(context).hasSingleBean(Executor.class).hasBean("applicationTaskExecutor"); + assertThat(context).getBean("applicationTaskExecutor").isInstanceOf(SimpleAsyncTaskExecutor.class); + SimpleAsyncTaskExecutor taskExecutor = context.getBean("applicationTaskExecutor", + SimpleAsyncTaskExecutor.class); + assertThat(virtualThreadName(taskExecutor)).startsWith("task-"); + }); + } + + @Test + @DisabledForJreRange(max = JRE.JAVA_20) + void whenTaskNamePrefixIsConfiguredThenSimpleAsyncTaskExecutorWithVirtualThreadsUsesIt() { + this.contextRunner + .withPropertyValues("spring.threads.virtual.enabled=true", + "spring.task.execution.thread-name-prefix=custom-") + .run((context) -> { + SimpleAsyncTaskExecutor taskExecutor = context.getBean("applicationTaskExecutor", + SimpleAsyncTaskExecutor.class); + assertThat(virtualThreadName(taskExecutor)).startsWith("custom-"); + }); + } + + @Test + @DisabledForJreRange(max = JRE.JAVA_20) + void whenVirtualThreadsAreAvailableButNotEnabledThenThreadPoolTaskExecutorIsAutoConfigured() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(Executor.class).hasBean("applicationTaskExecutor"); + assertThat(context).getBean("applicationTaskExecutor").isInstanceOf(ThreadPoolTaskExecutor.class); + }); + } + + @Test + @DisabledForJreRange(max = JRE.JAVA_20) + void whenTaskDecoratorIsDefinedThenSimpleAsyncTaskExecutorWithVirtualThreadsUsesIt() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true") + .withUserConfiguration(TaskDecoratorConfig.class) + .run((context) -> { + SimpleAsyncTaskExecutor executor = context.getBean(SimpleAsyncTaskExecutor.class); + assertThat(executor).extracting("taskDecorator").isSameAs(context.getBean(TaskDecorator.class)); + }); + } + @Test void taskExecutorWhenHasCustomTaskExecutorShouldBackOff() { this.contextRunner.withUserConfiguration(CustomTaskExecutorConfig.class).run((context) -> { @@ -117,6 +168,17 @@ void taskExecutorWhenHasCustomTaskExecutorShouldBackOff() { }); } + @Test + @DisabledForJreRange(max = JRE.JAVA_20) + void whenVirtualThreadsAreEnabledAndCustomTaskExecutorIsDefinedThenSimpleAsyncTaskExecutorThatUsesVirtualThreadsBacksOff() { + this.contextRunner.withUserConfiguration(CustomTaskExecutorConfig.class) + .withPropertyValues("spring.threads.virtual.enabled=true") + .run((context) -> { + assertThat(context).hasSingleBean(Executor.class); + assertThat(context.getBean(Executor.class)).isSameAs(context.getBean("customTaskExecutor")); + }); + } + @Test void taskExecutorBuilderShouldApplyCustomizer() { this.contextRunner.withUserConfiguration(TaskExecutorCustomizerConfig.class).run((context) -> { @@ -159,6 +221,20 @@ private ContextConsumer assertTaskExecutor( }; } + private String virtualThreadName(SimpleAsyncTaskExecutor taskExecutor) throws InterruptedException { + AtomicReference threadReference = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + taskExecutor.execute(() -> { + Thread currentThread = Thread.currentThread(); + threadReference.set(currentThread); + latch.countDown(); + }); + latch.await(30, TimeUnit.SECONDS); + Thread thread = threadReference.get(); + assertThat(thread).extracting("virtual").as("%s is virtual", thread).isEqualTo(true); + return thread.getName(); + } + @Configuration(proxyBeanMethods = false) static class CustomTaskExecutorBuilderConfig { diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc index 71c66a419add..543f0dba659d 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc @@ -1,6 +1,9 @@ [[features.task-execution-and-scheduling]] == Task Execution and Scheduling -In the absence of an `Executor` bean in the context, Spring Boot auto-configures a `ThreadPoolTaskExecutor` with sensible defaults that can be automatically associated to asynchronous task execution (`@EnableAsync`) and Spring MVC asynchronous request processing. +In the absence of an `Executor` bean in the context, Spring Boot auto-configures an `AsyncTaskExecutor`. +When virtual threads are enabled (using Java 21+ and configprop:spring.threads.virtual.enabled[] set to `true`) this will be a `SimpleAsyncTaskExecutor` that uses virtual threads. +Otherwise, it will be a `ThreadPoolTaskExecutor` with sensible defaults. +In either case, the auto-configured executor will be automatically used for asynchronous task execution (`@EnableAsync`) and Spring MVC asynchronous request processing. [TIP] ==== @@ -10,7 +13,7 @@ Depending on your target arrangement, you could change your `Executor` into a `T The auto-configured `TaskExecutorBuilder` allows you to easily create instances that reproduce what the auto-configuration does by default. ==== -The thread pool uses 8 core threads that can grow and shrink according to the load. +When a `ThreadPoolTaskExecutor` is auto-configured, the thread pool uses 8 core threads that can grow and shrink according to the load. Those default settings can be fine-tuned using the `spring.task.execution` namespace, as shown in the following example: [source,yaml,indent=0,subs="verbatim",configprops,configblocks] From 8115f8f1465c980000b6eec40fd98612acfc994a Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Wed, 12 Jul 2023 12:49:18 -0700 Subject: [PATCH 0147/1215] Add property for base time unit in OTLP registry Micrometer added a new configuration option to its OTLP registry to enable configuring the base time unit. These changes provide a configuration property to support to it. See gh-36393 --- .../metrics/export/otlp/OtlpProperties.java | 14 ++++++++++++++ .../export/otlp/OtlpPropertiesConfigAdapter.java | 6 ++++++ .../otlp/OtlpPropertiesConfigAdapterTests.java | 14 ++++++++++++++ .../metrics/export/otlp/OtlpPropertiesTests.java | 1 + 4 files changed, 35 insertions(+) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpProperties.java index 701d45c30896..b39faa557713 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpProperties.java @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.otlp; import java.util.Map; +import java.util.concurrent.TimeUnit; import io.micrometer.registry.otlp.AggregationTemporality; @@ -55,6 +56,11 @@ public class OtlpProperties extends StepRegistryProperties { */ private Map headers; + /** + * Time unit for exported metrics. + */ + private TimeUnit baseTimeUnit = TimeUnit.MILLISECONDS; + public String getUrl() { return this.url; } @@ -87,4 +93,12 @@ public void setHeaders(Map headers) { this.headers = headers; } + public TimeUnit getBaseTimeUnit() { + return this.baseTimeUnit; + } + + public void setBaseTimeUnit(TimeUnit baseTimeUnit) { + this.baseTimeUnit = baseTimeUnit; + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java index 814298d364e3..e21455e80f44 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.otlp; import java.util.Map; +import java.util.concurrent.TimeUnit; import io.micrometer.registry.otlp.AggregationTemporality; import io.micrometer.registry.otlp.OtlpConfig; @@ -60,4 +61,9 @@ public Map headers() { return get(OtlpProperties::getHeaders, OtlpConfig.super::headers); } + @Override + public TimeUnit baseTimeUnit() { + return get(OtlpProperties::getBaseTimeUnit, OtlpConfig.super::baseTimeUnit); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java index d2fc02a7f412..87f527dcd197 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.otlp; import java.util.Map; +import java.util.concurrent.TimeUnit; import io.micrometer.registry.otlp.AggregationTemporality; import org.junit.jupiter.api.Test; @@ -67,4 +68,17 @@ void whenPropertiesHeadersIsSetAdapterHeadersReturnsIt() { assertThat(new OtlpPropertiesConfigAdapter(properties).headers()).containsEntry("header", "value"); } + @Test + void whenPropertiesBaseTimeUnitIsNotSetAdapterBaseTimeUnitReturnsMillis() { + OtlpProperties properties = new OtlpProperties(); + assertThat(new OtlpPropertiesConfigAdapter(properties).baseTimeUnit()).isSameAs(TimeUnit.MILLISECONDS); + } + + @Test + void whenPropertiesBaseTimeUnitIsSetAdapterBaseTimeUnitReturnsIt() { + OtlpProperties properties = new OtlpProperties(); + properties.setBaseTimeUnit(TimeUnit.SECONDS); + assertThat(new OtlpPropertiesConfigAdapter(properties).baseTimeUnit()).isSameAs(TimeUnit.SECONDS); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesTests.java index 69f945b66ce5..3046e2279dca 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesTests.java @@ -37,6 +37,7 @@ void defaultValuesAreConsistent() { assertStepRegistryDefaultValues(properties, config); assertThat(properties.getUrl()).isEqualTo(config.url()); assertThat(properties.getAggregationTemporality()).isSameAs(config.aggregationTemporality()); + assertThat(properties.getBaseTimeUnit()).isSameAs(config.baseTimeUnit()); } } From bc2899c1ef67b49f80b28708f6e4078947b9aa8e Mon Sep 17 00:00:00 2001 From: Bernardo Bulgarelli Date: Wed, 5 Jul 2023 20:28:08 -0300 Subject: [PATCH 0148/1215] Deprecate DelegatingApplicationContextInitializer and DelegatingApplicationListener See gh-36251 --- .../config/DelegatingApplicationContextInitializer.java | 3 +++ .../boot/context/config/DelegatingApplicationListener.java | 3 +++ .../config/DelegatingApplicationContextInitializerTests.java | 3 +++ .../context/config/DelegatingApplicationListenerTests.java | 3 +++ 4 files changed, 12 insertions(+) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/DelegatingApplicationContextInitializer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/DelegatingApplicationContextInitializer.java index 5f0cf707a7ee..a5bb72cf7ac5 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/DelegatingApplicationContextInitializer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/DelegatingApplicationContextInitializer.java @@ -38,7 +38,10 @@ * @author Dave Syer * @author Phillip Webb * @since 1.0.0 + * + * @deprecated since 3.2 for removal in 3.4 as property based initialization is no longer recommended */ +@Deprecated(since = "3.2.0", forRemoval = true) public class DelegatingApplicationContextInitializer implements ApplicationContextInitializer, Ordered { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/DelegatingApplicationListener.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/DelegatingApplicationListener.java index 634962b0b6fa..793321785139 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/DelegatingApplicationListener.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/DelegatingApplicationListener.java @@ -40,7 +40,10 @@ * @author Dave Syer * @author Phillip Webb * @since 1.0.0 + * + * @deprecated since 3.2 for removal in 3.4 as property based initialization is no longer recommended */ +@Deprecated(since = "3.2.0", forRemoval = true) public class DelegatingApplicationListener implements ApplicationListener, Ordered { // NOTE: Similar to org.springframework.web.context.ContextLoader diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/DelegatingApplicationContextInitializerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/DelegatingApplicationContextInitializerTests.java index 748d1e65ced2..2a3eb495ef4c 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/DelegatingApplicationContextInitializerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/DelegatingApplicationContextInitializerTests.java @@ -35,7 +35,10 @@ * Tests for {@link DelegatingApplicationContextInitializer}. * * @author Phillip Webb + * + * @deprecated since 3.2 for removal in 3.4 as property based initialization is no longer recommended */ +@Deprecated(since = "3.2.0", forRemoval = true) class DelegatingApplicationContextInitializerTests { private final DelegatingApplicationContextInitializer initializer = new DelegatingApplicationContextInitializer(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/DelegatingApplicationListenerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/DelegatingApplicationListenerTests.java index 284c9377d47d..0b63f1a563fa 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/DelegatingApplicationListenerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/DelegatingApplicationListenerTests.java @@ -36,7 +36,10 @@ * Tests for {@link DelegatingApplicationListener}. * * @author Dave Syer + * + * @deprecated since 3.2 for removal in 3.4 as property based initialization is no longer recommended */ +@Deprecated(since = "3.2.0", forRemoval = true) class DelegatingApplicationListenerTests { private final DelegatingApplicationListener listener = new DelegatingApplicationListener(); From 60df7e3bce6873a21351e2164e835233c7e3159f Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 17 Jul 2023 13:33:23 +0200 Subject: [PATCH 0149/1215] Polish contribution See gh-36251 --- .../src/docs/asciidoc/howto/application.adoc | 1 - .../config/DelegatingApplicationContextInitializer.java | 6 +++--- .../boot/context/config/DelegatingApplicationListener.java | 6 +++--- .../DelegatingApplicationContextInitializerTests.java | 3 +-- .../context/config/DelegatingApplicationListenerTests.java | 3 +-- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/application.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/application.adoc index 638864778836..26b1da2c2e23 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/application.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/application.adoc @@ -61,7 +61,6 @@ Spring Boot loads a number of such customizations for use internally from `META- There is more than one way to register additional customizations: * Programmatically, per application, by calling the `addListeners` and `addInitializers` methods on `SpringApplication` before you run it. -* Declaratively, per application, by setting the `context.initializer.classes` or `context.listener.classes` properties. * Declaratively, for all applications, by adding a `META-INF/spring.factories` and packaging a jar file that the applications all use as a library. The `SpringApplication` sends some special `ApplicationEvents` to the listeners (some even before the context is created) and then registers the listeners for events published by the `ApplicationContext` as well. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/DelegatingApplicationContextInitializer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/DelegatingApplicationContextInitializer.java index a5bb72cf7ac5..3cf1721734bf 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/DelegatingApplicationContextInitializer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/DelegatingApplicationContextInitializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. @@ -38,8 +38,8 @@ * @author Dave Syer * @author Phillip Webb * @since 1.0.0 - * - * @deprecated since 3.2 for removal in 3.4 as property based initialization is no longer recommended + * @deprecated since 3.2.0 for removal in 3.4.0 as property based initialization is no + * longer recommended */ @Deprecated(since = "3.2.0", forRemoval = true) public class DelegatingApplicationContextInitializer diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/DelegatingApplicationListener.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/DelegatingApplicationListener.java index 793321785139..41c85cc354bc 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/DelegatingApplicationListener.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/DelegatingApplicationListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -40,8 +40,8 @@ * @author Dave Syer * @author Phillip Webb * @since 1.0.0 - * - * @deprecated since 3.2 for removal in 3.4 as property based initialization is no longer recommended + * @deprecated since 3.2.0 for removal in 3.4.0 as property based initialization is no + * longer recommended */ @Deprecated(since = "3.2.0", forRemoval = true) public class DelegatingApplicationListener implements ApplicationListener, Ordered { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/DelegatingApplicationContextInitializerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/DelegatingApplicationContextInitializerTests.java index 2a3eb495ef4c..2436489db8bc 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/DelegatingApplicationContextInitializerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/DelegatingApplicationContextInitializerTests.java @@ -35,10 +35,9 @@ * Tests for {@link DelegatingApplicationContextInitializer}. * * @author Phillip Webb - * - * @deprecated since 3.2 for removal in 3.4 as property based initialization is no longer recommended */ @Deprecated(since = "3.2.0", forRemoval = true) +@SuppressWarnings("removal") class DelegatingApplicationContextInitializerTests { private final DelegatingApplicationContextInitializer initializer = new DelegatingApplicationContextInitializer(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/DelegatingApplicationListenerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/DelegatingApplicationListenerTests.java index 0b63f1a563fa..c56460014a8b 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/DelegatingApplicationListenerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/DelegatingApplicationListenerTests.java @@ -36,10 +36,9 @@ * Tests for {@link DelegatingApplicationListener}. * * @author Dave Syer - * - * @deprecated since 3.2 for removal in 3.4 as property based initialization is no longer recommended */ @Deprecated(since = "3.2.0", forRemoval = true) +@SuppressWarnings("removal") class DelegatingApplicationListenerTests { private final DelegatingApplicationListener listener = new DelegatingApplicationListener(); From 0dae89e837b83816633dc4160f5e2348a2546364 Mon Sep 17 00:00:00 2001 From: Vedran Pavic Date: Fri, 28 Apr 2023 11:13:59 +0200 Subject: [PATCH 0150/1215] Add auto-configuration for ObservedAspect This adds support for auto-configuring `ObservedAspect` when AspectJ is on the classpath, which enables the usage of `@Observed`. See gh-35191 --- .../ObservationAutoConfiguration.java | 15 +++++++++++++++ .../ObservationAutoConfigurationTests.java | 16 ++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java index a164afa63e84..b9230b45dd12 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfiguration.java @@ -27,9 +27,11 @@ import io.micrometer.observation.ObservationHandler; import io.micrometer.observation.ObservationPredicate; import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.aop.ObservedAspect; import io.micrometer.tracing.Tracer; import io.micrometer.tracing.handler.TracingAwareMeterObservationHandler; import io.micrometer.tracing.handler.TracingObservationHandler; +import org.aspectj.weaver.Advice; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; @@ -51,6 +53,7 @@ * @author Moritz Halbritter * @author Brian Clozel * @author Jonatan Ivanov + * @author Vedran Pavic * @since 3.0.0 */ @AutoConfiguration(after = { CompositeMeterRegistryAutoConfiguration.class, MicrometerTracingAutoConfiguration.class }) @@ -149,4 +152,16 @@ TracingAwareMeterObservationHandler tracingAwareMeterObserv } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(Advice.class) + static class ObservedAspectConfiguration { + + @Bean + @ConditionalOnMissingBean + ObservedAspect observedAspect(ObservationRegistry observationRegistry) { + return new ObservedAspect(observationRegistry); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java index 8060d268908d..3ebf46455062 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java @@ -34,9 +34,11 @@ import io.micrometer.observation.ObservationHandler.FirstMatchingCompositeObservationHandler; import io.micrometer.observation.ObservationPredicate; import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.aop.ObservedAspect; import io.micrometer.tracing.Tracer; import io.micrometer.tracing.handler.TracingAwareMeterObservationHandler; import io.micrometer.tracing.handler.TracingObservationHandler; +import org.aspectj.weaver.Advice; import org.junit.jupiter.api.Test; import org.mockito.Answers; @@ -58,6 +60,7 @@ * * @author Moritz Halbritter * @author Jonatan Ivanov + * @author Vedran Pavic */ class ObservationAutoConfigurationTests { @@ -77,6 +80,7 @@ void beansShouldNotBeSuppliedWhenMicrometerObservationIsNotOnClassPath() { assertThat(context).hasSingleBean(MeterRegistry.class); assertThat(context).doesNotHaveBean(ObservationRegistry.class); assertThat(context).doesNotHaveBean(ObservationHandler.class); + assertThat(context).doesNotHaveBean(ObservedAspect.class); assertThat(context).doesNotHaveBean(ObservationHandlerGrouping.class); }); } @@ -88,6 +92,7 @@ void supplyObservationRegistryWhenMicrometerCoreAndTracingAreNotOnClassPath() { ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); Observation.start("test-observation", observationRegistry).stop(); assertThat(context).doesNotHaveBean(ObservationHandler.class); + assertThat(context).hasSingleBean(ObservedAspect.class); assertThat(context).doesNotHaveBean(ObservationHandlerGrouping.class); }); } @@ -99,6 +104,7 @@ void supplyMeterHandlerAndGroupingWhenMicrometerCoreIsOnClassPathButTracingIsNot Observation.start("test-observation", observationRegistry).stop(); assertThat(context).hasSingleBean(ObservationHandler.class); assertThat(context).hasSingleBean(DefaultMeterObservationHandler.class); + assertThat(context).hasSingleBean(ObservedAspect.class); assertThat(context).hasSingleBean(ObservationHandlerGrouping.class); assertThat(context).hasBean("metricsObservationHandlerGrouping"); }); @@ -110,6 +116,7 @@ void supplyOnlyTracingObservationHandlerGroupingWhenMicrometerCoreIsNotOnClassPa ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); Observation.start("test-observation", observationRegistry).stop(); assertThat(context).doesNotHaveBean(ObservationHandler.class); + assertThat(context).hasSingleBean(ObservedAspect.class); assertThat(context).hasSingleBean(ObservationHandlerGrouping.class); assertThat(context).hasBean("tracingObservationHandlerGrouping"); }); @@ -123,6 +130,7 @@ void supplyMeterHandlerAndGroupingWhenMicrometerCoreAndTracingAreOnClassPath() { // TracingAwareMeterObservationHandler that we don't test here Observation.start("test-observation", observationRegistry); assertThat(context).hasSingleBean(ObservationHandler.class); + assertThat(context).hasSingleBean(ObservedAspect.class); assertThat(context).hasSingleBean(TracingAwareMeterObservationHandler.class); assertThat(context).hasSingleBean(ObservationHandlerGrouping.class); assertThat(context).hasBean("metricsAndTracingObservationHandlerGrouping"); @@ -138,6 +146,7 @@ void supplyMeterHandlerAndGroupingWhenMicrometerCoreAndTracingAreOnClassPathButT Observation.start("test-observation", observationRegistry).stop(); assertThat(context).hasSingleBean(ObservationHandler.class); assertThat(context).hasSingleBean(DefaultMeterObservationHandler.class); + assertThat(context).hasSingleBean(ObservedAspect.class); assertThat(context).hasSingleBean(ObservationHandlerGrouping.class); assertThat(context).hasBean("metricsAndTracingObservationHandlerGrouping"); }); @@ -155,6 +164,7 @@ void autoConfiguresDefaultMeterObservationHandler() { assertThat(meterRegistry.get("test-observation").timer().count()).isOne(); assertThat(context).hasSingleBean(DefaultMeterObservationHandler.class); assertThat(context).hasSingleBean(ObservationHandler.class); + assertThat(context).hasSingleBean(ObservedAspect.class); }); } @@ -164,6 +174,12 @@ void allowsDefaultMeterObservationHandlerToBeDisabled() { .run((context) -> assertThat(context).doesNotHaveBean(ObservationHandler.class)); } + @Test + void allowsObservedAspectToBeDisabled() { + this.contextRunner.withClassLoader(new FilteredClassLoader(Advice.class)) + .run((context) -> assertThat(context).doesNotHaveBean(ObservedAspect.class)); + } + @Test void autoConfiguresObservationPredicates() { this.contextRunner.withUserConfiguration(ObservationPredicates.class).run((context) -> { From c726a13395e82e63698be25906982a5508e34a32 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 17 Jul 2023 16:06:08 +0200 Subject: [PATCH 0151/1215] Polish "Add auto-configuration for ObservedAspect" See gh-35191 --- .../ObservationAutoConfigurationTests.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java index 3ebf46455062..3cb18e2e0acf 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationAutoConfigurationTests.java @@ -180,6 +180,14 @@ void allowsObservedAspectToBeDisabled() { .run((context) -> assertThat(context).doesNotHaveBean(ObservedAspect.class)); } + @Test + void allowsObservedAspectToBeCustomized() { + this.contextRunner.withUserConfiguration(CustomObservedAspectConfiguration.class) + .run((context) -> assertThat(context).hasSingleBean(ObservedAspect.class) + .getBean(ObservedAspect.class) + .isSameAs(context.getBean("customObservedAspect"))); + } + @Test void autoConfiguresObservationPredicates() { this.contextRunner.withUserConfiguration(ObservationPredicates.class).run((context) -> { @@ -353,6 +361,16 @@ ObservationFilter observationFilterTwo() { } + @Configuration(proxyBeanMethods = false) + static class CustomObservedAspectConfiguration { + + @Bean + ObservedAspect customObservedAspect(ObservationRegistry observationRegistry) { + return new ObservedAspect(observationRegistry); + } + + } + @Configuration(proxyBeanMethods = false) static class CustomGlobalObservationConvention { From 007e3409028b8a7ba45974ad1ad7e785ac604de8 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 17 Jul 2023 19:58:30 +0200 Subject: [PATCH 0152/1215] Upgrade to Dependency Management Plugin 1.1.2 Closes gh-36437 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 549041507fc8..d74f1dd225f8 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -223,7 +223,7 @@ bom { ] } } - library("Dependency Management Plugin", "1.1.1") { + library("Dependency Management Plugin", "1.1.2") { group("io.spring.gradle") { modules = [ "dependency-management-plugin" From c95c65838b4b9c4f9fe95ffe5cf0b9d4be570ef4 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 17 Jul 2023 19:58:35 +0200 Subject: [PATCH 0153/1215] Upgrade to Lettuce 6.2.5.RELEASE Closes gh-36438 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d74f1dd225f8..ba1e3001a1d3 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -794,7 +794,7 @@ bom { ] } } - library("Lettuce", "6.2.4.RELEASE") { + library("Lettuce", "6.2.5.RELEASE") { group("io.lettuce") { modules = [ "lettuce-core" From 6ed8838209bfe224655f218fa066f1be8609aef5 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 17 Jul 2023 19:58:41 +0200 Subject: [PATCH 0154/1215] Upgrade to Spring AMQP 3.0.6 Closes gh-36439 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index ba1e3001a1d3..fb79aac8bde3 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1364,7 +1364,7 @@ bom { ] } } - library("Spring AMQP", "3.0.5") { + library("Spring AMQP", "3.0.6") { group("org.springframework.amqp") { imports = [ "spring-amqp-bom" From 4659d1bb934c26226c5fce027e3f593e8fba471f Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 17 Jul 2023 19:58:42 +0200 Subject: [PATCH 0155/1215] Upgrade to Spring Kafka 3.0.9 Closes gh-36194 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index fb79aac8bde3..f78f622fe552 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1421,7 +1421,7 @@ bom { ] } } - library("Spring Kafka", "3.0.9-SNAPSHOT") { + library("Spring Kafka", "3.0.9") { group("org.springframework.kafka") { modules = [ "spring-kafka", From 0b4bbe99b3eba8b311444602867ee8b32e6cde8b Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 18 Jul 2023 06:24:30 +0200 Subject: [PATCH 0156/1215] Upgrade to Spring Security 6.2.0-M1 Closes gh-36195 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f78f622fe552..4589803405bb 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1453,7 +1453,7 @@ bom { ] } } - library("Spring Security", "6.2.0-SNAPSHOT") { + library("Spring Security", "6.2.0-M1") { group("org.springframework.security") { imports = [ "spring-security-bom" From d205d10519e3a46010e7611b50ea6b47b584464a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 18 Jul 2023 10:18:37 +0100 Subject: [PATCH 0157/1215] Configure WebFlux's blocking execution to use applicationTaskExecutor Closes gh-36331 --- .../reactive/WebFluxAutoConfiguration.java | 14 ++++ .../WebFluxAutoConfigurationTests.java | 77 +++++++++++++++++++ .../task-execution-and-scheduling.adoc | 6 +- 3 files changed, 94 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java index 54b1effcb9cb..c3d77d94fad1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java @@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration; +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; @@ -54,12 +55,14 @@ import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.format.FormatterRegistry; import org.springframework.format.support.FormattingConversionService; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.util.ClassUtils; import org.springframework.validation.Validator; import org.springframework.web.filter.reactive.HiddenHttpMethodFilter; +import org.springframework.web.reactive.config.BlockingExecutionConfigurer; import org.springframework.web.reactive.config.DelegatingWebFluxConfiguration; import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.reactive.config.ResourceHandlerRegistration; @@ -184,6 +187,17 @@ public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { this.codecCustomizers.orderedStream().forEach((customizer) -> customizer.customize(configurer)); } + @Override + public void configureBlockingExecution(BlockingExecutionConfigurer configurer) { + if (this.beanFactory.containsBean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)) { + Object taskExecutor = this.beanFactory + .getBean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME); + if (taskExecutor instanceof AsyncTaskExecutor asyncTaskExecutor) { + configurer.setExecutor(asyncTaskExecutor); + } + } + } + @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { if (!this.resourceProperties.isAddMappings()) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java index 15759a908e2d..65be8c0f79ea 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -43,6 +44,7 @@ import org.springframework.aop.support.AopUtils; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration; +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; import org.springframework.boot.autoconfigure.web.ServerProperties; @@ -62,6 +64,7 @@ import org.springframework.core.annotation.Order; import org.springframework.core.convert.ConversionService; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.format.Parser; import org.springframework.format.Printer; import org.springframework.format.support.FormattingConversionService; @@ -79,6 +82,7 @@ import org.springframework.web.filter.reactive.HiddenHttpMethodFilter; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.reactive.config.BlockingExecutionConfigurer; import org.springframework.web.reactive.config.DelegatingWebFluxConfiguration; import org.springframework.web.reactive.config.WebFluxConfigurationSupport; import org.springframework.web.reactive.config.WebFluxConfigurer; @@ -667,6 +671,47 @@ void problemDetailsBacksOffWhenExceptionHandler() { .hasSingleBean(CustomExceptionHandler.class)); } + @Test + void asyncTaskExecutorWithApplicationTaskExecutor() { + this.contextRunner.withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class)) + .run((context) -> { + assertThat(context).hasSingleBean(AsyncTaskExecutor.class); + assertThat(context.getBean(RequestMappingHandlerAdapter.class)).extracting("scheduler.executor") + .isSameAs(context.getBean("applicationTaskExecutor")); + }); + } + + @Test + void asyncTaskExecutorWithNonMatchApplicationTaskExecutorBean() { + this.contextRunner.withUserConfiguration(CustomApplicationTaskExecutorConfig.class) + .withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class)) + .run((context) -> { + assertThat(context).doesNotHaveBean(AsyncTaskExecutor.class); + assertThat(context.getBean(RequestMappingHandlerAdapter.class)).extracting("scheduler.executor") + .isNotSameAs(context.getBean("applicationTaskExecutor")); + }); + } + + @Test + void asyncTaskExecutorWithWebFluxConfigurerCanOverrideExecutor() { + this.contextRunner.withUserConfiguration(CustomAsyncTaskExecutorConfigurer.class) + .withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class)) + .run((context) -> assertThat(context.getBean(RequestMappingHandlerAdapter.class)) + .extracting("scheduler.executor") + .isSameAs(context.getBean(CustomAsyncTaskExecutorConfigurer.class).taskExecutor)); + } + + @Test + void asyncTaskExecutorWithCustomNonApplicationTaskExecutor() { + this.contextRunner.withUserConfiguration(CustomAsyncTaskExecutorConfig.class) + .withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class)) + .run((context) -> { + assertThat(context).hasSingleBean(AsyncTaskExecutor.class); + assertThat(context.getBean(RequestMappingHandlerAdapter.class)).extracting("scheduler.executor") + .isNotSameAs(context.getBean("customTaskExecutor")); + }); + } + private ContextConsumer assertExchangeWithSession( Consumer exchange) { return (context) -> { @@ -981,4 +1026,36 @@ void exceptionHandlerIntercept(JoinPoint joinPoint, Object returnValue) { } + @Configuration(proxyBeanMethods = false) + static class CustomApplicationTaskExecutorConfig { + + @Bean + Executor applicationTaskExecutor() { + return mock(Executor.class); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomAsyncTaskExecutorConfig { + + @Bean + AsyncTaskExecutor customTaskExecutor() { + return mock(AsyncTaskExecutor.class); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomAsyncTaskExecutorConfigurer implements WebFluxConfigurer { + + private final AsyncTaskExecutor taskExecutor = mock(AsyncTaskExecutor.class); + + @Override + public void configureBlockingExecution(BlockingExecutionConfigurer configurer) { + configurer.setExecutor(this.taskExecutor); + } + + } + } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc index 543f0dba659d..5eb6f53468d1 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc @@ -3,12 +3,12 @@ In the absence of an `Executor` bean in the context, Spring Boot auto-configures an `AsyncTaskExecutor`. When virtual threads are enabled (using Java 21+ and configprop:spring.threads.virtual.enabled[] set to `true`) this will be a `SimpleAsyncTaskExecutor` that uses virtual threads. Otherwise, it will be a `ThreadPoolTaskExecutor` with sensible defaults. -In either case, the auto-configured executor will be automatically used for asynchronous task execution (`@EnableAsync`) and Spring MVC asynchronous request processing. +In either case, the auto-configured executor will be automatically used for asynchronous task execution (`@EnableAsync`), Spring MVC asynchronous request processing, and Spring WebFlux blocking execution. [TIP] ==== -If you have defined a custom `Executor` in the context, regular task execution (that is `@EnableAsync`) will use it transparently but the Spring MVC support will not be configured as it requires an `AsyncTaskExecutor` implementation (named `applicationTaskExecutor`). -Depending on your target arrangement, you could change your `Executor` into a `ThreadPoolTaskExecutor` or define both a `ThreadPoolTaskExecutor` and an `AsyncConfigurer` wrapping your custom `Executor`. +If you have defined a custom `Executor` in the context, regular task execution (that is `@EnableAsync`) will use it transparently but the Spring MVC and Spring WebFlux support will not be configured as they require an `AsyncTaskExecutor` implementation (named `applicationTaskExecutor`). +Depending on your target arrangement, you could change your `Executor` into an `AsyncTaskExecutor` or define both an `AsyncTaskExecutor` and an `AsyncConfigurer` wrapping your custom `Executor`. The auto-configured `TaskExecutorBuilder` allows you to easily create instances that reproduce what the auto-configuration does by default. ==== From fb640c04e757eecfcd68fcfe0f0dfb87bcab4c9c Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 13 Jul 2023 18:40:02 +0200 Subject: [PATCH 0158/1215] Upgrade to Flyway 9.20.1 Closes gh-36364 Co-authored-by: Andy Wilkinson --- .../spring-boot-autoconfigure/build.gradle | 1 + .../flyway/FlywayAutoConfiguration.java | 87 ++++++++++++++++--- ...va => Flyway91AutoConfigurationTests.java} | 9 +- .../flyway/FlywayAutoConfigurationTests.java | 53 +++++++++-- .../flyway/FlywayPropertiesTests.java | 5 +- .../spring-boot-dependencies/build.gradle | 3 +- 6 files changed, 131 insertions(+), 27 deletions(-) rename spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/{Flyway920AutoConfigurationTests.java => Flyway91AutoConfigurationTests.java} (86%) diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index 03e84c83456a..a30895a521d8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -104,6 +104,7 @@ dependencies { exclude group: "commons-logging", module: "commons-logging" } optional("org.flywaydb:flyway-core") + optional("org.flywaydb:flyway-database-oracle") optional("org.flywaydb:flyway-sqlserver") optional("org.freemarker:freemarker") optional("org.glassfish.jersey.containers:jersey-container-servlet-core") diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java index d007fb61953d..a4f15eaf61c8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java @@ -24,6 +24,9 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; import javax.sql.DataSource; @@ -32,6 +35,8 @@ import org.flywaydb.core.api.callback.Callback; import org.flywaydb.core.api.configuration.FluentConfiguration; import org.flywaydb.core.api.migration.JavaMigration; +import org.flywaydb.core.extensibility.ConfigurationExtension; +import org.flywaydb.database.oracle.OracleConfigurationExtension; import org.flywaydb.database.sqlserver.SQLServerConfigurationExtension; import org.springframework.aot.hint.RuntimeHints; @@ -61,6 +66,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportRuntimeHints; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.io.ResourceLoader; @@ -71,6 +78,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; +import org.springframework.util.function.SingletonSupplier; /** * {@link EnableAutoConfiguration Auto-configuration} for Flyway database migrations. @@ -116,6 +124,12 @@ public FlywaySchemaManagementProvider flywayDefaultDdlModeProvider(ObjectProvide @EnableConfigurationProperties(FlywayProperties.class) public static class FlywayConfiguration { + private final FlywayProperties properties; + + FlywayConfiguration(FlywayProperties properties) { + this.properties = properties; + } + @Bean ResourceProviderCustomizer resourceProviderCustomizer() { return new ResourceProviderCustomizer(); @@ -123,21 +137,26 @@ ResourceProviderCustomizer resourceProviderCustomizer() { @Bean @ConditionalOnMissingBean(FlywayConnectionDetails.class) - PropertiesFlywayConnectionDetails flywayConnectionDetails(FlywayProperties properties) { - return new PropertiesFlywayConnectionDetails(properties); + PropertiesFlywayConnectionDetails flywayConnectionDetails() { + return new PropertiesFlywayConnectionDetails(this.properties); + } + + @Bean + @ConditionalOnClass(name = "org.flywaydb.database.oracle.OracleConfigurationExtension") + OracleFlywayConfigurationCustomizer oracleFlywayConfigurationCustomizer() { + return new OracleFlywayConfigurationCustomizer(this.properties); } @Bean - Flyway flyway(FlywayProperties properties, FlywayConnectionDetails connectionDetails, - ResourceLoader resourceLoader, ObjectProvider dataSource, - @FlywayDataSource ObjectProvider flywayDataSource, + Flyway flyway(FlywayConnectionDetails connectionDetails, ResourceLoader resourceLoader, + ObjectProvider dataSource, @FlywayDataSource ObjectProvider flywayDataSource, ObjectProvider fluentConfigurationCustomizers, ObjectProvider javaMigrations, ObjectProvider callbacks, ResourceProviderCustomizer resourceProviderCustomizer) { FluentConfiguration configuration = new FluentConfiguration(resourceLoader.getClassLoader()); configureDataSource(configuration, flywayDataSource.getIfAvailable(), dataSource.getIfUnique(), connectionDetails); - configureProperties(configuration, properties); + configureProperties(configuration, this.properties); configureCallbacks(configuration, callbacks.orderedStream().toList()); configureJavaMigrations(configuration, javaMigrations.orderedStream().toList()); fluentConfigurationCustomizers.orderedStream().forEach((customizer) -> customizer.customize(configuration)); @@ -242,12 +261,6 @@ private void configureProperties(FluentConfiguration configuration, FlywayProper map.from(properties.getDryRunOutput()).to(configuration::dryRunOutput); map.from(properties.getErrorOverrides()).to(configuration::errorOverrides); map.from(properties.getLicenseKey()).to(configuration::licenseKey); - // No method references for Oracle props for compatibility with Flyway 9.20+ - map.from(properties.getOracleSqlplus()).to((oracleSqlplus) -> configuration.oracleSqlplus(oracleSqlplus)); - map.from(properties.getOracleSqlplusWarn()) - .to((oracleSqlplusWarn) -> configuration.oracleSqlplusWarn(oracleSqlplusWarn)); - map.from(properties.getOracleKerberosCacheFile()) - .to((oracleKerberosCacheFile) -> configuration.oracleKerberosCacheFile(oracleKerberosCacheFile)); map.from(properties.getStream()).to(configuration::stream); map.from(properties.getUndoSqlMigrationPrefix()).to(configuration::undoSqlMigrationPrefix); map.from(properties.getCherryPick()).to(configuration::cherryPick); @@ -445,4 +458,54 @@ public String getDriverClassName() { } + @Order(Ordered.HIGHEST_PRECEDENCE) + static final class OracleFlywayConfigurationCustomizer implements FlywayConfigurationCustomizer { + + private final FlywayProperties properties; + + OracleFlywayConfigurationCustomizer(FlywayProperties properties) { + this.properties = properties; + } + + @Override + public void customize(FluentConfiguration configuration) { + ConfigurationExtensionMapper map = new ConfigurationExtensionMapper<>( + PropertyMapper.get().alwaysApplyingWhenNonNull(), () -> { + OracleConfigurationExtension extension = configuration.getPluginRegister() + .getPlugin(OracleConfigurationExtension.class); + Assert.notNull(extension, "Flyway Oracle extension missing"); + return extension; + }); + map.apply(this.properties.getOracleSqlplus(), OracleConfigurationExtension::setSqlplus); + map.apply(this.properties.getOracleSqlplusWarn(), OracleConfigurationExtension::setSqlplusWarn); + map.apply(this.properties.getOracleWalletLocation(), OracleConfigurationExtension::setWalletLocation); + map.apply(this.properties.getOracleKerberosCacheFile(), OracleConfigurationExtension::setKerberosCacheFile); + } + + } + + static class ConfigurationExtensionMapper { + + private final PropertyMapper map; + + private final Supplier extensionProvider; + + ConfigurationExtensionMapper(PropertyMapper map, Supplier extensionProvider) { + this.map = map; + this.extensionProvider = SingletonSupplier.of(extensionProvider); + } + + void apply(V value, BiConsumer mapper) { + this.map.from(value).to(withExtension(mapper)); + } + + private Consumer withExtension(BiConsumer mapper) { + return (value) -> { + T extension = this.extensionProvider.get(); + mapper.accept(extension, value); + }; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway920AutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway91AutoConfigurationTests.java similarity index 86% rename from spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway920AutoConfigurationTests.java rename to spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway91AutoConfigurationTests.java index 770a618e4725..ee7cb7ff751a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway920AutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/Flyway91AutoConfigurationTests.java @@ -23,18 +23,19 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; import org.springframework.boot.testsupport.classpath.ClassPathOverrides; import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link FlywayAutoConfiguration} with Flyway 9.20. + * Tests for {@link FlywayAutoConfiguration} with Flyway 9.19. * * @author Andy Wilkinson */ -@ClassPathOverrides({ "org.flywaydb:flyway-core:9.20.0", "org.flywaydb:flyway-sqlserver:9.20.0", - "com.h2database:h2:2.1.210" }) -class Flyway920AutoConfigurationTests { +@ClassPathExclusions("flyway-*.jar") +@ClassPathOverrides("org.flywaydb:flyway-core:9.19.4") +class Flyway91AutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(FlywayAutoConfiguration.class)) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java index b2149465d8b2..27c947d60901 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java @@ -31,8 +31,10 @@ import org.flywaydb.core.api.callback.Callback; import org.flywaydb.core.api.callback.Context; import org.flywaydb.core.api.callback.Event; +import org.flywaydb.core.api.configuration.FluentConfiguration; import org.flywaydb.core.api.migration.JavaMigration; import org.flywaydb.core.internal.license.FlywayTeamsUpgradeRequiredException; +import org.flywaydb.database.oracle.OracleConfigurationExtension; import org.flywaydb.database.sqlserver.SQLServerConfigurationExtension; import org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform; import org.jooq.DSLContext; @@ -49,6 +51,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayAutoConfigurationRuntimeHints; +import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.OracleFlywayConfigurationCustomizer; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; @@ -84,6 +87,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -602,18 +606,56 @@ void licenseKeyIsCorrectlyMapped(CapturedOutput output) { + "Enterprise features, download Flyway Teams Edition & Flyway Enterprise Edition")); } + @Test + void oracleExtensionIsNotLoadedByDefault() { + FluentConfiguration configuration = mock(FluentConfiguration.class); + new OracleFlywayConfigurationCustomizer(new FlywayProperties()).customize(configuration); + then(configuration).shouldHaveNoInteractions(); + } + @Test void oracleSqlplusIsCorrectlyMapped() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) .withPropertyValues("spring.flyway.oracle-sqlplus=true") - .run(validateFlywayTeamsPropertyOnly("oracle.sqlplus")); + .run((context) -> assertThat(context.getBean(Flyway.class) + .getConfiguration() + .getPluginRegister() + .getPlugin(OracleConfigurationExtension.class) + .getSqlplus()).isTrue()); + } @Test void oracleSqlplusWarnIsCorrectlyMapped() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) .withPropertyValues("spring.flyway.oracle-sqlplus-warn=true") - .run(validateFlywayTeamsPropertyOnly("oracle.sqlplusWarn")); + .run((context) -> assertThat(context.getBean(Flyway.class) + .getConfiguration() + .getPluginRegister() + .getPlugin(OracleConfigurationExtension.class) + .getSqlplusWarn()).isTrue()); + } + + @Test + void oracleWallerLocationIsCorrectlyMapped() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.flyway.oracle-wallet-location=/tmp/my.wallet") + .run((context) -> assertThat(context.getBean(Flyway.class) + .getConfiguration() + .getPluginRegister() + .getPlugin(OracleConfigurationExtension.class) + .getWalletLocation()).isEqualTo("/tmp/my.wallet")); + } + + @Test + void oracleKerberosCacheFileIsCorrectlyMapped() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.flyway.oracle-kerberos-cache-file=/tmp/cache") + .run((context) -> assertThat(context.getBean(Flyway.class) + .getConfiguration() + .getPluginRegister() + .getPlugin(OracleConfigurationExtension.class) + .getKerberosCacheFile()).isEqualTo("/tmp/cache")); } @Test @@ -683,13 +725,6 @@ void kerberosConfigFileIsCorrectlyMapped() { .run(validateFlywayTeamsPropertyOnly("kerberosConfigFile")); } - @Test - void oracleKerberosCacheFileIsCorrectlyMapped() { - this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.flyway.oracle-kerberos-cache-file=/tmp/cache") - .run(validateFlywayTeamsPropertyOnly("oracle.kerberosCacheFile")); - } - @Test void outputQueryResultsIsCorrectlyMapped() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java index 0c7bb4110a2d..b6bca04ce2f3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java @@ -109,6 +109,9 @@ void expectedPropertiesAreManaged() { PropertyAccessorFactory.forBeanPropertyAccess(new ClassicConfiguration())); // Properties specific settings ignoreProperties(properties, "url", "driverClassName", "user", "password", "enabled"); + // Property that moved to a separate Oracle plugin + ignoreProperties(properties, "oracleSqlplus", "oracleSqlplusWarn", "oracleKerberosCacheFile", + "oracleWalletLocation"); // Property that moved to a separate SQL plugin ignoreProperties(properties, "sqlServerKerberosLoginFile"); // High level object we can't set with properties @@ -128,7 +131,7 @@ void expectedPropertiesAreManaged() { // Handled as createSchemas ignoreProperties(configuration, "shouldCreateSchemas"); // Getters for the DataSource settings rather than actual properties - ignoreProperties(configuration, "password", "url", "user"); + ignoreProperties(configuration, "databaseType", "password", "url", "user"); // Properties not exposed by Flyway ignoreProperties(configuration, "failOnMissingTarget"); List configurationKeys = new ArrayList<>(configuration.keySet()); diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 4589803405bb..8a55db136838 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -279,10 +279,11 @@ bom { ] } } - library("Flyway", "9.19.4") { + library("Flyway", "9.20.1") { group("org.flywaydb") { modules = [ "flyway-core", + "flyway-database-oracle", "flyway-firebird", "flyway-mysql", "flyway-sqlserver" From 71406977c24ead037268287922e9a350155c9c4f Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 18 Jul 2023 13:51:42 +0200 Subject: [PATCH 0159/1215] Harmonize configuration of Flyway SQL Server extension Closes gh-36440 --- .../flyway/FlywayAutoConfiguration.java | 43 +++++++++++++------ .../flyway/FlywayAutoConfigurationTests.java | 8 ++++ 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java index a4f15eaf61c8..9b768756ca2b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java @@ -141,6 +141,12 @@ PropertiesFlywayConnectionDetails flywayConnectionDetails() { return new PropertiesFlywayConnectionDetails(this.properties); } + @Bean + @ConditionalOnClass(name = "org.flywaydb.database.sqlserver.SQLServerConfigurationExtension") + SqlServerFlywayConfigurationCustomizer sqlServerFlywayConfigurationCustomizer() { + return new SqlServerFlywayConfigurationCustomizer(this.properties); + } + @Bean @ConditionalOnClass(name = "org.flywaydb.database.oracle.OracleConfigurationExtension") OracleFlywayConfigurationCustomizer oracleFlywayConfigurationCustomizer() { @@ -267,10 +273,6 @@ private void configureProperties(FluentConfiguration configuration, FlywayProper map.from(properties.getJdbcProperties()).whenNot(Map::isEmpty).to(configuration::jdbcProperties); map.from(properties.getKerberosConfigFile()).to(configuration::kerberosConfigFile); map.from(properties.getOutputQueryResults()).to(configuration::outputQueryResults); - map.from(properties.getSqlServerKerberosLoginFile()) - .whenNonNull() - .to((sqlServerKerberosLoginFile) -> configureSqlServerKerberosLoginFile(configuration, - sqlServerKerberosLoginFile)); map.from(properties.getSkipExecutingMigrations()).to(configuration::skipExecutingMigrations); map.from(properties.getIgnoreMigrationPatterns()) .whenNot(List::isEmpty) @@ -289,14 +291,6 @@ private void configureExecuteInTransaction(FluentConfiguration configuration, Fl } } - private void configureSqlServerKerberosLoginFile(FluentConfiguration configuration, - String sqlServerKerberosLoginFile) { - SQLServerConfigurationExtension sqlServerConfigurationExtension = configuration.getPluginRegister() - .getPlugin(SQLServerConfigurationExtension.class); - Assert.state(sqlServerConfigurationExtension != null, "Flyway SQL Server extension missing"); - sqlServerConfigurationExtension.getKerberos().getLogin().setFile(sqlServerKerberosLoginFile); - } - private void configureCallbacks(FluentConfiguration configuration, List callbacks) { if (!callbacks.isEmpty()) { configuration.callbacks(callbacks.toArray(new Callback[0])); @@ -484,6 +478,31 @@ public void customize(FluentConfiguration configuration) { } + @Order(Ordered.HIGHEST_PRECEDENCE) + static final class SqlServerFlywayConfigurationCustomizer implements FlywayConfigurationCustomizer { + + private final FlywayProperties properties; + + SqlServerFlywayConfigurationCustomizer(FlywayProperties properties) { + this.properties = properties; + } + + @Override + public void customize(FluentConfiguration configuration) { + ConfigurationExtensionMapper map = new ConfigurationExtensionMapper<>( + PropertyMapper.get().alwaysApplyingWhenNonNull(), () -> { + SQLServerConfigurationExtension extension = configuration.getPluginRegister() + .getPlugin(SQLServerConfigurationExtension.class); + Assert.notNull(extension, "Flyway SQL Server extension missing"); + return extension; + }); + + map.apply(this.properties.getSqlServerKerberosLoginFile(), + (extension, file) -> extension.getKerberos().getLogin().setFile(file)); + } + + } + static class ConfigurationExtensionMapper { private final PropertyMapper map; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java index 27c947d60901..62c03c62b66e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java @@ -52,6 +52,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayAutoConfigurationRuntimeHints; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.OracleFlywayConfigurationCustomizer; +import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.SqlServerFlywayConfigurationCustomizer; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; @@ -732,6 +733,13 @@ void outputQueryResultsIsCorrectlyMapped() { .run(validateFlywayTeamsPropertyOnly("outputQueryResults")); } + @Test + void sqlServerExtensionIsNotLoadedByDefault() { + FluentConfiguration configuration = mock(FluentConfiguration.class); + new SqlServerFlywayConfigurationCustomizer(new FlywayProperties()).customize(configuration); + then(configuration).shouldHaveNoInteractions(); + } + @Test void sqlServerKerberosLoginFileIsCorrectlyMapped() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) From 289d458a60d2641a040d4176816e07e85d97600e Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 18 Jul 2023 15:04:42 +0200 Subject: [PATCH 0160/1215] Start building against Spring Framework 6.1.0-M3 snapshots See gh-36443 --- gradle.properties | 2 +- .../convert/ConversionServiceParameterValueMapperTests.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 030ee45e65a8..7e5a78d8f27f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 kotlinVersion=1.8.22 nativeBuildToolsVersion=0.9.23 -springFrameworkVersion=6.1.0-M2 +springFrameworkVersion=6.1.0-SNAPSHOT tomcatVersion=10.1.11 kotlin.stdlib.default.dependency=false diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/convert/ConversionServiceParameterValueMapperTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/convert/ConversionServiceParameterValueMapperTests.java index 3ba4df37a969..c2c1bc8d6ca2 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/convert/ConversionServiceParameterValueMapperTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/invoke/convert/ConversionServiceParameterValueMapperTests.java @@ -30,6 +30,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; @@ -55,7 +56,7 @@ void mapParameterShouldDelegateToConversionService() { void mapParameterWhenConversionServiceFailsShouldThrowParameterMappingException() { ConversionService conversionService = mock(ConversionService.class); RuntimeException error = new RuntimeException(); - given(conversionService.convert(any(), any())).willThrow(error); + given(conversionService.convert(any(Object.class), eq(Integer.class))).willThrow(error); ConversionServiceParameterValueMapper mapper = new ConversionServiceParameterValueMapper(conversionService); assertThatExceptionOfType(ParameterMappingException.class) .isThrownBy(() -> mapper.mapParameterValue(new TestOperationParameter(Integer.class), "123")) From 8da706603e451cabed8ddde06c9971b4fc7fcc89 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 18 Jul 2023 15:44:38 +0200 Subject: [PATCH 0161/1215] Add support for flyway.postgresql.transactional.lock Closes gh-32629 --- .../flyway/FlywayAutoConfiguration.java | 31 +++++++++++++++++++ .../flyway/FlywayProperties.java | 27 ++++++++++++++++ .../flyway/FlywayAutoConfigurationTests.java | 20 ++++++++++++ .../flyway/FlywayPropertiesTests.java | 2 ++ 4 files changed, 80 insertions(+) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java index 9b768756ca2b..a47791944d01 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java @@ -36,6 +36,7 @@ import org.flywaydb.core.api.configuration.FluentConfiguration; import org.flywaydb.core.api.migration.JavaMigration; import org.flywaydb.core.extensibility.ConfigurationExtension; +import org.flywaydb.core.internal.database.postgresql.PostgreSQLConfigurationExtension; import org.flywaydb.database.oracle.OracleConfigurationExtension; import org.flywaydb.database.sqlserver.SQLServerConfigurationExtension; @@ -153,6 +154,12 @@ OracleFlywayConfigurationCustomizer oracleFlywayConfigurationCustomizer() { return new OracleFlywayConfigurationCustomizer(this.properties); } + @Bean + @ConditionalOnClass(name = "org.flywaydb.core.internal.database.postgresql.PostgreSQLConfigurationExtension") + PostgresqlFlywayConfigurationCustomizer postgresqlFlywayConfigurationCustomizer() { + return new PostgresqlFlywayConfigurationCustomizer(this.properties); + } + @Bean Flyway flyway(FlywayConnectionDetails connectionDetails, ResourceLoader resourceLoader, ObjectProvider dataSource, @FlywayDataSource ObjectProvider flywayDataSource, @@ -478,6 +485,30 @@ public void customize(FluentConfiguration configuration) { } + @Order(Ordered.HIGHEST_PRECEDENCE) + static final class PostgresqlFlywayConfigurationCustomizer implements FlywayConfigurationCustomizer { + + private final FlywayProperties properties; + + PostgresqlFlywayConfigurationCustomizer(FlywayProperties properties) { + this.properties = properties; + } + + @Override + public void customize(FluentConfiguration configuration) { + ConfigurationExtensionMapper map = new ConfigurationExtensionMapper<>( + PropertyMapper.get().alwaysApplyingWhenNonNull(), () -> { + PostgreSQLConfigurationExtension extension = configuration.getPluginRegister() + .getPlugin(PostgreSQLConfigurationExtension.class); + Assert.notNull(extension, "PostgreSQL extension missing"); + return extension; + }); + map.apply(this.properties.getPostgresql().getTransactionalLock(), + PostgreSQLConfigurationExtension::setTransactionalLock); + } + + } + @Order(Ordered.HIGHEST_PRECEDENCE) static final class SqlServerFlywayConfigurationCustomizer implements FlywayConfigurationCustomizer { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java index b629e7c425f8..b1a1dd7afd4b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java @@ -372,6 +372,8 @@ public class FlywayProperties { */ private Boolean detectEncoding; + private final Postgresql postgresql = new Postgresql(); + public boolean isEnabled() { return this.enabled; } @@ -868,4 +870,29 @@ public void setDetectEncoding(final Boolean detectEncoding) { this.detectEncoding = detectEncoding; } + public Postgresql getPostgresql() { + return this.postgresql; + } + + /** + * {@code PostgreSQLConfigurationExtension} properties. + */ + public static class Postgresql { + + /** + * Whether transactional advisory locks should be used. If set to false, + * session-level locks are used instead. + */ + private Boolean transactionalLock; + + public Boolean getTransactionalLock() { + return this.transactionalLock; + } + + public void setTransactionalLock(Boolean transactionalLock) { + this.transactionalLock = transactionalLock; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java index 62c03c62b66e..52bf67ce14ee 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java @@ -33,6 +33,7 @@ import org.flywaydb.core.api.callback.Event; import org.flywaydb.core.api.configuration.FluentConfiguration; import org.flywaydb.core.api.migration.JavaMigration; +import org.flywaydb.core.internal.database.postgresql.PostgreSQLConfigurationExtension; import org.flywaydb.core.internal.license.FlywayTeamsUpgradeRequiredException; import org.flywaydb.database.oracle.OracleConfigurationExtension; import org.flywaydb.database.sqlserver.SQLServerConfigurationExtension; @@ -52,6 +53,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayAutoConfigurationRuntimeHints; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.OracleFlywayConfigurationCustomizer; +import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.PostgresqlFlywayConfigurationCustomizer; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.SqlServerFlywayConfigurationCustomizer; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; @@ -733,6 +735,24 @@ void outputQueryResultsIsCorrectlyMapped() { .run(validateFlywayTeamsPropertyOnly("outputQueryResults")); } + @Test + void postgresqlExtensionIsNotLoadedByDefault() { + FluentConfiguration configuration = mock(FluentConfiguration.class); + new PostgresqlFlywayConfigurationCustomizer(new FlywayProperties()).customize(configuration); + then(configuration).shouldHaveNoInteractions(); + } + + @Test + void postgresqlTransactionalLockIsCorrectlyMapped() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.flyway.postgresql.transactional-lock=false") + .run((context) -> assertThat(context.getBean(Flyway.class) + .getConfiguration() + .getPluginRegister() + .getPlugin(PostgreSQLConfigurationExtension.class) + .isTransactionalLock()).isFalse()); + } + @Test void sqlServerExtensionIsNotLoadedByDefault() { FluentConfiguration configuration = mock(FluentConfiguration.class); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java index b6bca04ce2f3..d36ecc54cc57 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java @@ -112,6 +112,8 @@ void expectedPropertiesAreManaged() { // Property that moved to a separate Oracle plugin ignoreProperties(properties, "oracleSqlplus", "oracleSqlplusWarn", "oracleKerberosCacheFile", "oracleWalletLocation"); + // Postgresql extension + ignoreProperties(properties, "postgresql"); // Property that moved to a separate SQL plugin ignoreProperties(properties, "sqlServerKerberosLoginFile"); // High level object we can't set with properties From c6e47b86d732736666e58e4875f363974863370c Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 18 Jul 2023 16:09:26 +0200 Subject: [PATCH 0162/1215] Move Flyway configuration extension properties to dedicated namespace This commit harmonizes the handling of ConfigurationExtension for Flyway. The existing Oracle and SQLServer extensions are now mapped from flway.oracle and flyway.sqlserver, respectively. The existing properties have been deprecated in favor of the new location. Closes gh-36444 --- .../flyway/FlywayAutoConfiguration.java | 12 +- .../flyway/FlywayProperties.java | 156 +++++++++++++----- .../flyway/FlywayAutoConfigurationTests.java | 63 +++++++ .../flyway/FlywayPropertiesTests.java | 12 +- 4 files changed, 194 insertions(+), 49 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java index a47791944d01..1f499261c910 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java @@ -52,6 +52,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayAutoConfigurationRuntimeHints; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayDataSourceCondition; +import org.springframework.boot.autoconfigure.flyway.FlywayProperties.Oracle; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; @@ -477,10 +478,11 @@ public void customize(FluentConfiguration configuration) { Assert.notNull(extension, "Flyway Oracle extension missing"); return extension; }); - map.apply(this.properties.getOracleSqlplus(), OracleConfigurationExtension::setSqlplus); - map.apply(this.properties.getOracleSqlplusWarn(), OracleConfigurationExtension::setSqlplusWarn); - map.apply(this.properties.getOracleWalletLocation(), OracleConfigurationExtension::setWalletLocation); - map.apply(this.properties.getOracleKerberosCacheFile(), OracleConfigurationExtension::setKerberosCacheFile); + Oracle oracle = this.properties.getOracle(); + map.apply(oracle.getSqlplus(), OracleConfigurationExtension::setSqlplus); + map.apply(oracle.getSqlplusWarn(), OracleConfigurationExtension::setSqlplusWarn); + map.apply(oracle.getWalletLocation(), OracleConfigurationExtension::setWalletLocation); + map.apply(oracle.getKerberosCacheFile(), OracleConfigurationExtension::setKerberosCacheFile); } } @@ -528,7 +530,7 @@ public void customize(FluentConfiguration configuration) { return extension; }); - map.apply(this.properties.getSqlServerKerberosLoginFile(), + map.apply(this.properties.getSqlserver().getKerberosLoginFile(), (extension, file) -> extension.getKerberos().getLogin().setFile(file)); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java index b1a1dd7afd4b..bf1ea687e509 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java @@ -28,6 +28,7 @@ import java.util.Map; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.boot.convert.DurationUnit; /** @@ -295,17 +296,6 @@ public class FlywayProperties { */ private String licenseKey; - /** - * Whether to enable support for Oracle SQL*Plus commands. Requires Flyway Teams. - */ - private Boolean oracleSqlplus; - - /** - * Whether to issue a warning rather than an error when a not-yet-supported Oracle - * SQL*Plus statement is encountered. Requires Flyway Teams. - */ - private Boolean oracleSqlplusWarn; - /** * Whether to stream SQL migrations when executing them. Requires Flyway Teams. */ @@ -332,28 +322,12 @@ public class FlywayProperties { */ private String kerberosConfigFile; - /** - * Path of the Oracle Kerberos cache file. Requires Flyway Teams. - */ - private String oracleKerberosCacheFile; - - /** - * Location of the Oracle Wallet, used to sign in to the database automatically. - * Requires Flyway Teams. - */ - private String oracleWalletLocation; - /** * Whether Flyway should output a table with the results of queries when executing * migrations. Requires Flyway Teams. */ private Boolean outputQueryResults; - /** - * Path to the SQL Server Kerberos login file. Requires Flyway Teams. - */ - private String sqlServerKerberosLoginFile; - /** * Whether Flyway should skip executing the contents of the migrations and only update * the schema history table. Requires Flyway teams. @@ -372,8 +346,12 @@ public class FlywayProperties { */ private Boolean detectEncoding; + private final Oracle oracle = new Oracle(); + private final Postgresql postgresql = new Postgresql(); + private final Sqlserver sqlserver = new Sqlserver(); + public boolean isEnabled() { return this.enabled; } @@ -758,28 +736,37 @@ public void setLicenseKey(String licenseKey) { this.licenseKey = licenseKey; } + @DeprecatedConfigurationProperty(replacement = "spring.flyway.oracle.sqlplus") + @Deprecated(since = "3.2.0", forRemoval = true) public Boolean getOracleSqlplus() { - return this.oracleSqlplus; + return getOracle().getSqlplus(); } + @Deprecated(since = "3.2.0", forRemoval = true) public void setOracleSqlplus(Boolean oracleSqlplus) { - this.oracleSqlplus = oracleSqlplus; + getOracle().setSqlplus(oracleSqlplus); } + @DeprecatedConfigurationProperty(replacement = "spring.flyway.oracle.sqlplus-warn") + @Deprecated(since = "3.2.0", forRemoval = true) public Boolean getOracleSqlplusWarn() { - return this.oracleSqlplusWarn; + return getOracle().getSqlplusWarn(); } + @Deprecated(since = "3.2.0", forRemoval = true) public void setOracleSqlplusWarn(Boolean oracleSqlplusWarn) { - this.oracleSqlplusWarn = oracleSqlplusWarn; + getOracle().setSqlplusWarn(oracleSqlplusWarn); } + @DeprecatedConfigurationProperty(replacement = "spring.flyway.oracle.wallet-location") + @Deprecated(since = "3.2.0", forRemoval = true) public String getOracleWalletLocation() { - return this.oracleWalletLocation; + return getOracle().getWalletLocation(); } + @Deprecated(since = "3.2.0", forRemoval = true) public void setOracleWalletLocation(String oracleWalletLocation) { - this.oracleWalletLocation = oracleWalletLocation; + getOracle().setWalletLocation(oracleWalletLocation); } public Boolean getStream() { @@ -822,12 +809,15 @@ public void setKerberosConfigFile(String kerberosConfigFile) { this.kerberosConfigFile = kerberosConfigFile; } + @DeprecatedConfigurationProperty(replacement = "spring.flyway.oracle.kerberos-cache-file") + @Deprecated(since = "3.2.0", forRemoval = true) public String getOracleKerberosCacheFile() { - return this.oracleKerberosCacheFile; + return getOracle().getKerberosCacheFile(); } + @Deprecated(since = "3.2.0", forRemoval = true) public void setOracleKerberosCacheFile(String oracleKerberosCacheFile) { - this.oracleKerberosCacheFile = oracleKerberosCacheFile; + getOracle().setKerberosCacheFile(oracleKerberosCacheFile); } public Boolean getOutputQueryResults() { @@ -838,12 +828,15 @@ public void setOutputQueryResults(Boolean outputQueryResults) { this.outputQueryResults = outputQueryResults; } + @DeprecatedConfigurationProperty(replacement = "spring.flyway.sqlserver.kerberos-login-file") + @Deprecated(since = "3.2.0", forRemoval = true) public String getSqlServerKerberosLoginFile() { - return this.sqlServerKerberosLoginFile; + return getSqlserver().getKerberosLoginFile(); } + @Deprecated(since = "3.2.0", forRemoval = true) public void setSqlServerKerberosLoginFile(String sqlServerKerberosLoginFile) { - this.sqlServerKerberosLoginFile = sqlServerKerberosLoginFile; + getSqlserver().setKerberosLoginFile(sqlServerKerberosLoginFile); } public Boolean getSkipExecutingMigrations() { @@ -870,10 +863,79 @@ public void setDetectEncoding(final Boolean detectEncoding) { this.detectEncoding = detectEncoding; } + public Oracle getOracle() { + return this.oracle; + } + public Postgresql getPostgresql() { return this.postgresql; } + public Sqlserver getSqlserver() { + return this.sqlserver; + } + + /** + * {@code OracleConfigurationExtension} properties. + */ + public static class Oracle { + + /** + * Whether to enable support for Oracle SQL*Plus commands. Requires Flyway Teams. + */ + private Boolean sqlplus; + + /** + * Whether to issue a warning rather than an error when a not-yet-supported Oracle + * SQL*Plus statement is encountered. Requires Flyway Teams. + */ + private Boolean sqlplusWarn; + + /** + * Path of the Oracle Kerberos cache file. Requires Flyway Teams. + */ + private String kerberosCacheFile; + + /** + * Location of the Oracle Wallet, used to sign in to the database automatically. + * Requires Flyway Teams. + */ + private String walletLocation; + + public Boolean getSqlplus() { + return this.sqlplus; + } + + public void setSqlplus(Boolean sqlplus) { + this.sqlplus = sqlplus; + } + + public Boolean getSqlplusWarn() { + return this.sqlplusWarn; + } + + public void setSqlplusWarn(Boolean sqlplusWarn) { + this.sqlplusWarn = sqlplusWarn; + } + + public String getKerberosCacheFile() { + return this.kerberosCacheFile; + } + + public void setKerberosCacheFile(String kerberosCacheFile) { + this.kerberosCacheFile = kerberosCacheFile; + } + + public String getWalletLocation() { + return this.walletLocation; + } + + public void setWalletLocation(String walletLocation) { + this.walletLocation = walletLocation; + } + + } + /** * {@code PostgreSQLConfigurationExtension} properties. */ @@ -895,4 +957,24 @@ public void setTransactionalLock(Boolean transactionalLock) { } + /** + * {@code SQLServerConfigurationExtension} properties. + */ + public static class Sqlserver { + + /** + * Path to the SQL Server Kerberos login file. Requires Flyway Teams. + */ + private String kerberosLoginFile; + + public String getKerberosLoginFile() { + return this.kerberosLoginFile; + } + + public void setKerberosLoginFile(String kerberosLoginFile) { + this.kerberosLoginFile = kerberosLoginFile; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java index 52bf67ce14ee..c462bc02716a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java @@ -618,6 +618,19 @@ void oracleExtensionIsNotLoadedByDefault() { @Test void oracleSqlplusIsCorrectlyMapped() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.flyway.oracle.sqlplus=true") + .run((context) -> assertThat(context.getBean(Flyway.class) + .getConfiguration() + .getPluginRegister() + .getPlugin(OracleConfigurationExtension.class) + .getSqlplus()).isTrue()); + + } + + @Test + @Deprecated(since = "3.2.0", forRemoval = true) + void oracleSqlplusIsCorrectlyMappedWithDeprecatedProperty() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) .withPropertyValues("spring.flyway.oracle-sqlplus=true") .run((context) -> assertThat(context.getBean(Flyway.class) @@ -630,6 +643,18 @@ void oracleSqlplusIsCorrectlyMapped() { @Test void oracleSqlplusWarnIsCorrectlyMapped() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.flyway.oracle.sqlplus-warn=true") + .run((context) -> assertThat(context.getBean(Flyway.class) + .getConfiguration() + .getPluginRegister() + .getPlugin(OracleConfigurationExtension.class) + .getSqlplusWarn()).isTrue()); + } + + @Test + @Deprecated(since = "3.2.0", forRemoval = true) + void oracleSqlplusWarnIsCorrectlyMappedWithDeprecatedProperty() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) .withPropertyValues("spring.flyway.oracle-sqlplus-warn=true") .run((context) -> assertThat(context.getBean(Flyway.class) @@ -641,6 +666,18 @@ void oracleSqlplusWarnIsCorrectlyMapped() { @Test void oracleWallerLocationIsCorrectlyMapped() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.flyway.oracle.wallet-location=/tmp/my.wallet") + .run((context) -> assertThat(context.getBean(Flyway.class) + .getConfiguration() + .getPluginRegister() + .getPlugin(OracleConfigurationExtension.class) + .getWalletLocation()).isEqualTo("/tmp/my.wallet")); + } + + @Test + @Deprecated(since = "3.2.0", forRemoval = true) + void oracleWallerLocationIsCorrectlyMappedWithDeprecatedProperty() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) .withPropertyValues("spring.flyway.oracle-wallet-location=/tmp/my.wallet") .run((context) -> assertThat(context.getBean(Flyway.class) @@ -652,6 +689,18 @@ void oracleWallerLocationIsCorrectlyMapped() { @Test void oracleKerberosCacheFileIsCorrectlyMapped() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.flyway.oracle.kerberos-cache-file=/tmp/cache") + .run((context) -> assertThat(context.getBean(Flyway.class) + .getConfiguration() + .getPluginRegister() + .getPlugin(OracleConfigurationExtension.class) + .getKerberosCacheFile()).isEqualTo("/tmp/cache")); + } + + @Test + @Deprecated(since = "3.2.0", forRemoval = true) + void oracleKerberosCacheFileIsCorrectlyMappedWithDeprecatedProperty() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) .withPropertyValues("spring.flyway.oracle-kerberos-cache-file=/tmp/cache") .run((context) -> assertThat(context.getBean(Flyway.class) @@ -762,6 +811,20 @@ void sqlServerExtensionIsNotLoadedByDefault() { @Test void sqlServerKerberosLoginFileIsCorrectlyMapped() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.flyway.sqlserver.kerberos-login-file=/tmp/config") + .run((context) -> assertThat(context.getBean(Flyway.class) + .getConfiguration() + .getPluginRegister() + .getPlugin(SQLServerConfigurationExtension.class) + .getKerberos() + .getLogin() + .getFile()).isEqualTo("/tmp/config")); + } + + @Test + @Deprecated(since = "3.2.0", forRemoval = true) + void sqlServerKerberosLoginFileIsCorrectlyMappedWithDeprecatedProperty() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) .withPropertyValues("spring.flyway.sql-server-kerberos-login-file=/tmp/config") .run((context) -> assertThat(context.getBean(Flyway.class) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java index d36ecc54cc57..94c021b3f9b6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java @@ -109,13 +109,11 @@ void expectedPropertiesAreManaged() { PropertyAccessorFactory.forBeanPropertyAccess(new ClassicConfiguration())); // Properties specific settings ignoreProperties(properties, "url", "driverClassName", "user", "password", "enabled"); - // Property that moved to a separate Oracle plugin - ignoreProperties(properties, "oracleSqlplus", "oracleSqlplusWarn", "oracleKerberosCacheFile", - "oracleWalletLocation"); - // Postgresql extension - ignoreProperties(properties, "postgresql"); - // Property that moved to a separate SQL plugin - ignoreProperties(properties, "sqlServerKerberosLoginFile"); + // Deprecated properties + ignoreProperties(properties, "oracleKerberosCacheFile", "oracleSqlplus", "oracleSqlplusWarn", + "oracleWalletLocation", "sqlServerKerberosLoginFile"); + // Properties that are managed by specific extensions + ignoreProperties(properties, "oracle", "postgresql", "sqlserver"); // High level object we can't set with properties ignoreProperties(configuration, "callbacks", "classLoader", "dataSource", "javaMigrations", "javaMigrationClassProvider", "pluginRegister", "resourceProvider", "resolvers"); From 283dc37db3bdc4ba08a47ecc93b7057feddb6ece Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 18 Jul 2023 15:32:30 +0100 Subject: [PATCH 0163/1215] Make AnnotatedControllerConfigurer use applicationTaskExecutor Closes gh-36388 --- .../graphql/GraphQlAutoConfiguration.java | 7 +++- .../GraphQlAutoConfigurationTests.java | 32 +++++++++++++++++++ .../task-execution-and-scheduling.adoc | 10 ++++-- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfiguration.java index 37a2b97c409b..a42ef90a10d1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfiguration.java @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.concurrent.Executor; import graphql.GraphQL; import graphql.execution.instrumentation.Instrumentation; @@ -33,10 +34,12 @@ import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.convert.ApplicationConversionService; import org.springframework.context.annotation.Bean; @@ -152,10 +155,12 @@ public ExecutionGraphQlService executionGraphQlService(GraphQlSource graphQlSour @Bean @ConditionalOnMissingBean - public AnnotatedControllerConfigurer annotatedControllerConfigurer() { + public AnnotatedControllerConfigurer annotatedControllerConfigurer( + @Qualifier(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME) ObjectProvider executorProvider) { AnnotatedControllerConfigurer controllerConfigurer = new AnnotatedControllerConfigurer(); controllerConfigurer .addFormatterRegistrar((registry) -> ApplicationConversionService.addBeans(registry, this.beanFactory)); + executorProvider.ifAvailable(controllerConfigurer::setExecutor); return controllerConfigurer; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java index 2719747656e1..100cd9ddda09 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.graphql; import java.nio.charset.StandardCharsets; +import java.util.concurrent.Executor; import graphql.GraphQL; import graphql.execution.instrumentation.ChainedInstrumentation; @@ -34,6 +35,7 @@ import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration.GraphQlResourcesRuntimeHints; +import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -209,6 +211,26 @@ void shouldContributeConnectionTypeDefinitionConfigurer() { }); } + @Test + void whenApplicationTaskExecutorIsDefinedThenAnnotatedControllerConfigurerShouldUseIt() { + this.contextRunner.withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class)) + .run((context) -> { + AnnotatedControllerConfigurer annotatedControllerConfigurer = context + .getBean(AnnotatedControllerConfigurer.class); + assertThat(annotatedControllerConfigurer).extracting("executor") + .isSameAs(context.getBean("applicationTaskExecutor")); + }); + } + + @Test + void whenCustomExecutorIsDefinedThenAnnotatedControllerConfigurerDoesNotUseIt() { + this.contextRunner.withUserConfiguration(CustomExecutorConfiguration.class).run((context) -> { + AnnotatedControllerConfigurer annotatedControllerConfigurer = context + .getBean(AnnotatedControllerConfigurer.class); + assertThat(annotatedControllerConfigurer).extracting("executor").isNull(); + }); + } + @Configuration(proxyBeanMethods = false) static class CustomGraphQlBuilderConfiguration { @@ -294,4 +316,14 @@ public void customize(GraphQlSource.SchemaResourceBuilder builder) { } + @Configuration(proxyBeanMethods = false) + static class CustomExecutorConfiguration { + + @Bean + Executor customExecutor() { + return mock(Executor.class); + } + + } + } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc index 5eb6f53468d1..9ae721385599 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc @@ -3,11 +3,17 @@ In the absence of an `Executor` bean in the context, Spring Boot auto-configures an `AsyncTaskExecutor`. When virtual threads are enabled (using Java 21+ and configprop:spring.threads.virtual.enabled[] set to `true`) this will be a `SimpleAsyncTaskExecutor` that uses virtual threads. Otherwise, it will be a `ThreadPoolTaskExecutor` with sensible defaults. -In either case, the auto-configured executor will be automatically used for asynchronous task execution (`@EnableAsync`), Spring MVC asynchronous request processing, and Spring WebFlux blocking execution. +In either case, the auto-configured executor will be automatically used for: + +- asynchronous task execution (`@EnableAsync`) +- Spring for GraphQL's asynchronous handling of `Callable` return values from controller methods +- Spring MVC's asynchronous request processing +- Spring WebFlux's blocking execution support [TIP] ==== -If you have defined a custom `Executor` in the context, regular task execution (that is `@EnableAsync`) will use it transparently but the Spring MVC and Spring WebFlux support will not be configured as they require an `AsyncTaskExecutor` implementation (named `applicationTaskExecutor`). +If you have defined a custom `Executor` in the context, both regular task execution (that is `@EnableAsync`) and Spring for GraphQL will use it. +However, the Spring MVC and Spring WebFlux support will only use it if it is an `AsyncTaskExecutor` implementation (named `applicationTaskExecutor`). Depending on your target arrangement, you could change your `Executor` into an `AsyncTaskExecutor` or define both an `AsyncTaskExecutor` and an `AsyncConfigurer` wrapping your custom `Executor`. The auto-configured `TaskExecutorBuilder` allows you to easily create instances that reproduce what the auto-configuration does by default. From 4393a2244ca9e67f84cac74f7a0b17a208b742f2 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Thu, 13 Jul 2023 14:27:14 -0600 Subject: [PATCH 0164/1215] Use Docker CLI context to determine daemon host address for image building Configuration files managed by the Docker CLI are now used to determine the host address of the Docker daemon used when building images using buildpacks when a host address is not configured with environment variables or build tool plugin configuration. Closes gh-36445 --- .../buildpack/platform/docker/DockerApi.java | 4 +- .../configuration/DockerConfiguration.java | 63 +++++- .../DockerConfigurationMetadata.java | 211 ++++++++++++++++++ .../docker/configuration/DockerHost.java | 2 +- .../configuration/ResolvedDockerHost.java | 29 ++- .../docker/transport/HttpTransport.java | 5 +- .../transport/LocalHttpClientTransport.java | 44 ++-- .../platform/build/LifecycleTests.java | 8 +- .../DockerConfigurationMetadataTests.java | 104 +++++++++ .../ResolvedDockerHostTests.java | 70 ++++-- .../docker/transport/HttpTransportTests.java | 10 +- .../LocalHttpClientTransportTests.java | 14 +- .../RemoteHttpClientTransportTests.java | 18 +- .../configuration/with-context/config.json | 3 + .../meta.json | 12 + .../with-default-context/config.json | 3 + .../meta.json | 12 + .../docker/cert.pem | 0 .../docker/key.pem | 0 .../configuration/without-context/config.json | 2 + .../docs/asciidoc/packaging-oci-image.adoc | 12 +- .../gradle/tasks/bundling/DockerSpec.java | 14 +- .../tasks/bundling/DockerSpecTests.java | 36 ++- .../docs/asciidoc/packaging-oci-image.adoc | 12 +- .../springframework/boot/maven/Docker.java | 23 +- .../boot/maven/DockerTests.java | 35 ++- 26 files changed, 660 insertions(+), 86 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadata.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadataTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-context/config.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-context/contexts/meta/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/meta.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/config.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/contexts/meta/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/meta.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/contexts/tls/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/docker/cert.pem create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/contexts/tls/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/docker/key.pem create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/without-context/config.json diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java index 4ee957b5daea..7365bf267781 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java @@ -40,7 +40,7 @@ import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.hc.core5.net.URIBuilder; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport; import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport.Response; import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig; @@ -97,7 +97,7 @@ public DockerApi() { * @param dockerHost the Docker daemon host information * @since 2.4.0 */ - public DockerApi(DockerHost dockerHost) { + public DockerApi(DockerHostConfiguration dockerHost) { this(HttpTransport.create(dockerHost)); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfiguration.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfiguration.java index b82a6b28ac22..fa47c349fbd1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfiguration.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -27,7 +27,7 @@ */ public final class DockerConfiguration { - private final DockerHost host; + private final DockerHostConfiguration host; private final DockerRegistryAuthentication builderAuthentication; @@ -39,7 +39,7 @@ public DockerConfiguration() { this(null, null, null, false); } - private DockerConfiguration(DockerHost host, DockerRegistryAuthentication builderAuthentication, + private DockerConfiguration(DockerHostConfiguration host, DockerRegistryAuthentication builderAuthentication, DockerRegistryAuthentication publishAuthentication, boolean bindHostToBuilder) { this.host = host; this.builderAuthentication = builderAuthentication; @@ -47,7 +47,7 @@ private DockerConfiguration(DockerHost host, DockerRegistryAuthentication builde this.bindHostToBuilder = bindHostToBuilder; } - public DockerHost getHost() { + public DockerHostConfiguration getHost() { return this.host; } @@ -65,7 +65,13 @@ public DockerRegistryAuthentication getPublishRegistryAuthentication() { public DockerConfiguration withHost(String address, boolean secure, String certificatePath) { Assert.notNull(address, "Address must not be null"); - return new DockerConfiguration(new DockerHost(address, secure, certificatePath), this.builderAuthentication, + return new DockerConfiguration(DockerHostConfiguration.forAddress(address, secure, certificatePath), + this.builderAuthentication, this.publishAuthentication, this.bindHostToBuilder); + } + + public DockerConfiguration withContext(String context) { + Assert.notNull(context, "Context must not be null"); + return new DockerConfiguration(DockerHostConfiguration.forContext(context), this.builderAuthentication, this.publishAuthentication, this.bindHostToBuilder); } @@ -107,4 +113,51 @@ public DockerConfiguration withEmptyPublishRegistryAuthentication() { new DockerRegistryUserAuthentication("", "", "", ""), this.bindHostToBuilder); } + public static class DockerHostConfiguration { + + private final String address; + + private final String context; + + private final boolean secure; + + private final String certificatePath; + + public DockerHostConfiguration(String address, String context, boolean secure, String certificatePath) { + this.address = address; + this.context = context; + this.secure = secure; + this.certificatePath = certificatePath; + } + + public String getAddress() { + return this.address; + } + + public String getContext() { + return this.context; + } + + public boolean isSecure() { + return this.secure; + } + + public String getCertificatePath() { + return this.certificatePath; + } + + public static DockerHostConfiguration forAddress(String address) { + return new DockerHostConfiguration(address, null, false, null); + } + + public static DockerHostConfiguration forAddress(String address, boolean secure, String certificatePath) { + return new DockerHostConfiguration(address, null, secure, certificatePath); + } + + static DockerHostConfiguration forContext(String context) { + return new DockerHostConfiguration(null, context, false, null); + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadata.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadata.java new file mode 100644 index 000000000000..9ab0fef20192 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadata.java @@ -0,0 +1,211 @@ +/* + * Copyright 2012-2023 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.buildpack.platform.docker.configuration; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.NullNode; + +import org.springframework.boot.buildpack.platform.json.MappedObject; +import org.springframework.boot.buildpack.platform.json.SharedObjectMapper; +import org.springframework.boot.buildpack.platform.system.Environment; + +/** + * Docker configuration stored in metadata files managed by the Docker CLI. + * + * @author Scott Frederick + */ +final class DockerConfigurationMetadata { + + private static final String DOCKER_CONFIG = "DOCKER_CONFIG"; + + private static final String DEFAULT_CONTEXT = "default"; + + private static final String CONFIG_DIR = ".docker"; + + private static final String CONTEXTS_DIR = "contexts"; + + private static final String META_DIR = "meta"; + + private static final String TLS_DIR = "tls"; + + private static final String DOCKER_ENDPOINT = "docker"; + + private static final String CONFIG_FILE_NAME = "config.json"; + + private static final String CONTEXT_FILE_NAME = "meta.json"; + + private final String configLocation; + + private final DockerConfig config; + + private final DockerContext context; + + private DockerConfigurationMetadata(String configLocation, DockerConfig config, DockerContext context) { + this.configLocation = configLocation; + this.config = config; + this.context = context; + } + + DockerConfig getConfiguration() { + return this.config; + } + + DockerContext getContext() { + return this.context; + } + + DockerContext forContext(String context) { + return createDockerContext(this.configLocation, context); + } + + static DockerConfigurationMetadata from(Environment environment) { + String configLocation = (environment.get(DOCKER_CONFIG) != null) ? environment.get(DOCKER_CONFIG) + : Path.of(System.getProperty("user.home"), CONFIG_DIR).toString(); + DockerConfig dockerConfig = createDockerConfig(configLocation); + DockerContext dockerContext = createDockerContext(configLocation, dockerConfig.getCurrentContext()); + return new DockerConfigurationMetadata(configLocation, dockerConfig, dockerContext); + } + + private static DockerConfig createDockerConfig(String configLocation) { + Path path = Path.of(configLocation, CONFIG_FILE_NAME); + if (!path.toFile().exists()) { + return DockerConfig.empty(); + } + try { + return DockerConfig.fromJson(readPathContent(path)); + } + catch (JsonProcessingException ex) { + throw new IllegalStateException("Error parsing Docker configuration file '" + path + "'", ex); + } + } + + private static DockerContext createDockerContext(String configLocation, String currentContext) { + if (currentContext == null || DEFAULT_CONTEXT.equals(currentContext)) { + return DockerContext.empty(); + } + Path metaPath = Path.of(configLocation, CONTEXTS_DIR, META_DIR, asHash(currentContext), CONTEXT_FILE_NAME); + Path tlsPath = Path.of(configLocation, CONTEXTS_DIR, TLS_DIR, asHash(currentContext), DOCKER_ENDPOINT); + if (!metaPath.toFile().exists()) { + throw new IllegalArgumentException("Docker context '" + currentContext + "' does not exist"); + } + try { + DockerContext context = DockerContext.fromJson(readPathContent(metaPath)); + if (tlsPath.toFile().isDirectory()) { + return context.withTlsPath(tlsPath.toString()); + } + return context; + } + catch (JsonProcessingException ex) { + throw new IllegalStateException("Error parsing Docker context metadata file '" + metaPath + "'", ex); + } + } + + private static String asHash(String currentContext) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(currentContext.getBytes(StandardCharsets.UTF_8)); + return HexFormat.of().formatHex(hash); + } + catch (NoSuchAlgorithmException ex) { + return null; + } + } + + private static String readPathContent(Path path) { + try { + return Files.readString(path); + } + catch (IOException ex) { + throw new IllegalStateException("Error reading Docker configuration file '" + path + "'", ex); + } + } + + static final class DockerConfig extends MappedObject { + + private final String currentContext; + + private DockerConfig(JsonNode node) { + super(node, MethodHandles.lookup()); + this.currentContext = valueAt("/currentContext", String.class); + } + + String getCurrentContext() { + return this.currentContext; + } + + static DockerConfig fromJson(String json) throws JsonProcessingException { + return new DockerConfig(SharedObjectMapper.get().readTree(json)); + } + + static DockerConfig empty() { + return new DockerConfig(NullNode.instance); + } + + } + + static final class DockerContext extends MappedObject { + + private final String dockerHost; + + private final Boolean skipTlsVerify; + + private final String tlsPath; + + private DockerContext(JsonNode node, String tlsPath) { + super(node, MethodHandles.lookup()); + this.dockerHost = valueAt("/Endpoints/" + DOCKER_ENDPOINT + "/Host", String.class); + this.skipTlsVerify = valueAt("/Endpoints/" + DOCKER_ENDPOINT + "/SkipTLSVerify", Boolean.class); + this.tlsPath = tlsPath; + } + + String getDockerHost() { + return this.dockerHost; + } + + Boolean isTlsVerify() { + return this.skipTlsVerify != null && !this.skipTlsVerify; + } + + String getTlsPath() { + return this.tlsPath; + } + + DockerContext withTlsPath(String tlsPath) { + return new DockerContext(this.getNode(), tlsPath); + } + + static DockerContext fromJson(String json) throws JsonProcessingException { + return new DockerContext(SharedObjectMapper.get().readTree(json), null); + } + + static DockerContext empty() { + return new DockerContext(NullNode.instance, null); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerHost.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerHost.java index 3db5f5541d57..8d6d381feba4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerHost.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerHost.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/ResolvedDockerHost.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/ResolvedDockerHost.java index 95272b19d0d3..e19d592df08d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/ResolvedDockerHost.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/ResolvedDockerHost.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -21,6 +21,8 @@ import com.sun.jna.Platform; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfigurationMetadata.DockerContext; import org.springframework.boot.buildpack.platform.system.Environment; /** @@ -43,6 +45,12 @@ public class ResolvedDockerHost extends DockerHost { private static final String DOCKER_CERT_PATH = "DOCKER_CERT_PATH"; + private static final String DOCKER_CONTEXT = "DOCKER_CONTEXT"; + + ResolvedDockerHost(String address) { + super(address); + } + ResolvedDockerHost(String address, boolean secure, String certificatePath) { super(address, secure, certificatePath); } @@ -66,11 +74,20 @@ public boolean isLocalFileReference() { } } - public static ResolvedDockerHost from(DockerHost dockerHost) { + public static ResolvedDockerHost from(DockerHostConfiguration dockerHost) { return from(Environment.SYSTEM, dockerHost); } - static ResolvedDockerHost from(Environment environment, DockerHost dockerHost) { + static ResolvedDockerHost from(Environment environment, DockerHostConfiguration dockerHost) { + DockerConfigurationMetadata config = DockerConfigurationMetadata.from(environment); + if (environment.get(DOCKER_CONTEXT) != null) { + DockerContext context = config.forContext(environment.get(DOCKER_CONTEXT)); + return new ResolvedDockerHost(context.getDockerHost(), context.isTlsVerify(), context.getTlsPath()); + } + if (dockerHost != null && dockerHost.getContext() != null) { + DockerContext context = config.forContext(dockerHost.getContext()); + return new ResolvedDockerHost(context.getDockerHost(), context.isTlsVerify(), context.getTlsPath()); + } if (environment.get(DOCKER_HOST) != null) { return new ResolvedDockerHost(environment.get(DOCKER_HOST), isTrue(environment.get(DOCKER_TLS_VERIFY)), environment.get(DOCKER_CERT_PATH)); @@ -79,7 +96,11 @@ static ResolvedDockerHost from(Environment environment, DockerHost dockerHost) { return new ResolvedDockerHost(dockerHost.getAddress(), dockerHost.isSecure(), dockerHost.getCertificatePath()); } - return new ResolvedDockerHost(Platform.isWindows() ? WINDOWS_NAMED_PIPE_PATH : DOMAIN_SOCKET_PATH, false, null); + if (config.getContext().getDockerHost() != null) { + DockerContext context = config.getContext(); + return new ResolvedDockerHost(context.getDockerHost(), context.isTlsVerify(), context.getTlsPath()); + } + return new ResolvedDockerHost(Platform.isWindows() ? WINDOWS_NAMED_PIPE_PATH : DOMAIN_SOCKET_PATH); } private static boolean isTrue(String value) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransport.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransport.java index 4a8461fa0907..c428155142d8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -22,6 +22,7 @@ import java.io.OutputStream; import java.net.URI; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost; import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost; import org.springframework.boot.buildpack.platform.io.IOConsumer; @@ -93,7 +94,7 @@ public interface HttpTransport { * @param dockerHost the Docker host information * @return a {@link HttpTransport} instance */ - static HttpTransport create(DockerHost dockerHost) { + static HttpTransport create(DockerHostConfiguration dockerHost) { ResolvedDockerHost host = ResolvedDockerHost.from(dockerHost); HttpTransport remote = RemoteHttpClientTransport.createIfPossible(host); return (remote != null) ? remote : LocalHttpClientTransport.create(host); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/LocalHttpClientTransport.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/LocalHttpClientTransport.java index 13241f726c58..a5b1035a1956 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/LocalHttpClientTransport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/LocalHttpClientTransport.java @@ -20,23 +20,22 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; -import java.net.URISyntaxException; import java.net.UnknownHostException; import com.sun.jna.Platform; import org.apache.hc.client5.http.DnsResolver; -import org.apache.hc.client5.http.SchemePortResolver; +import org.apache.hc.client5.http.HttpRoute; import org.apache.hc.client5.http.classic.HttpClient; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager; import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.client5.http.routing.HttpRoutePlanner; import org.apache.hc.client5.http.socket.ConnectionSocketFactory; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.config.Registry; import org.apache.hc.core5.http.config.RegistryBuilder; import org.apache.hc.core5.http.protocol.HttpContext; -import org.apache.hc.core5.util.Args; import org.apache.hc.core5.util.TimeValue; import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost; @@ -51,26 +50,22 @@ */ final class LocalHttpClientTransport extends HttpClientTransport { - private static final HttpHost LOCAL_DOCKER_HOST; + private static final String DOCKER_SCHEME = "docker"; - static { - try { - LOCAL_DOCKER_HOST = HttpHost.create("docker://localhost"); - } - catch (URISyntaxException ex) { - throw new RuntimeException("Error creating local Docker host address", ex); - } - } + private static final int DEFAULT_DOCKER_PORT = 2376; - private LocalHttpClientTransport(HttpClient client) { - super(client, LOCAL_DOCKER_HOST); + private static final HttpHost LOCAL_DOCKER_HOST = new HttpHost(DOCKER_SCHEME, "localhost", DEFAULT_DOCKER_PORT); + + private LocalHttpClientTransport(HttpClient client, HttpHost host) { + super(client, host); } static LocalHttpClientTransport create(ResolvedDockerHost dockerHost) { HttpClientBuilder builder = HttpClients.custom(); builder.setConnectionManager(new LocalConnectionManager(dockerHost.getAddress())); - builder.setSchemePortResolver(new LocalSchemePortResolver()); - return new LocalHttpClientTransport(builder.build()); + builder.setRoutePlanner(new LocalRoutePlanner()); + HttpHost host = new HttpHost(DOCKER_SCHEME, dockerHost.getAddress()); + return new LocalHttpClientTransport(builder.build(), host); } /** @@ -84,7 +79,7 @@ private static class LocalConnectionManager extends BasicHttpClientConnectionMan private static Registry getRegistry(String host) { RegistryBuilder builder = RegistryBuilder.create(); - builder.register("docker", new LocalConnectionSocketFactory(host)); + builder.register(DOCKER_SCHEME, new LocalConnectionSocketFactory(host)); return builder.build(); } @@ -139,20 +134,13 @@ public Socket connectSocket(TimeValue connectTimeout, Socket socket, HttpHost ho } /** - * {@link SchemePortResolver} for local Docker. + * {@link HttpRoutePlanner} for local Docker. */ - private static class LocalSchemePortResolver implements SchemePortResolver { - - private static final int DEFAULT_DOCKER_PORT = 2376; + private static class LocalRoutePlanner implements HttpRoutePlanner { @Override - public int resolve(HttpHost host) { - Args.notNull(host, "HTTP host"); - String name = host.getSchemeName(); - if ("docker".equals(name)) { - return DEFAULT_DOCKER_PORT; - } - return -1; + public HttpRoute determineRoute(HttpHost target, HttpContext context) { + return new HttpRoute(LOCAL_DOCKER_HOST); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java index 78ee54874bb0..d8b03407a3d5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java @@ -40,7 +40,7 @@ import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi; import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi; import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost; import org.springframework.boot.buildpack.platform.docker.type.Binding; import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig; @@ -246,7 +246,8 @@ void executeWithDockerHostAndRemoteAddressExecutesPhases() throws Exception { given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); BuildRequest request = getTestRequest(); - createLifecycle(request, ResolvedDockerHost.from(new DockerHost("tcp://192.168.1.2:2376"))).execute(); + createLifecycle(request, ResolvedDockerHost.from(DockerHostConfiguration.forAddress("tcp://192.168.1.2:2376"))) + .execute(); assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-inherit-remote.json")); assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); } @@ -257,7 +258,8 @@ void executeWithDockerHostAndLocalAddressExecutesPhases() throws Exception { given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); BuildRequest request = getTestRequest(); - createLifecycle(request, ResolvedDockerHost.from(new DockerHost("/var/alt.sock"))).execute(); + createLifecycle(request, ResolvedDockerHost.from(DockerHostConfiguration.forAddress("/var/alt.sock"))) + .execute(); assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-inherit-local.json")); assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadataTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadataTests.java new file mode 100644 index 000000000000..c619eac31614 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadataTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2012-2023 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.buildpack.platform.docker.configuration; + +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Paths; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfigurationMetadata.DockerContext; +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link DockerConfigurationMetadata}. + * + * @author Scott Frederick + */ +class DockerConfigurationMetadataTests extends AbstractJsonTests { + + private final Map environment = new LinkedHashMap<>(); + + @Test + void configWithContextIsRead() throws Exception { + this.environment.put("DOCKER_CONFIG", pathToResource("with-context/config.json")); + DockerConfigurationMetadata config = DockerConfigurationMetadata.from(this.environment::get); + assertThat(config.getConfiguration().getCurrentContext()).isEqualTo("test-context"); + assertThat(config.getContext().getDockerHost()).isEqualTo("unix:///home/user/.docker/docker.sock"); + assertThat(config.getContext().isTlsVerify()).isFalse(); + assertThat(config.getContext().getTlsPath()).isNull(); + } + + @Test + void configWithoutContextIsRead() throws Exception { + this.environment.put("DOCKER_CONFIG", pathToResource("without-context/config.json")); + DockerConfigurationMetadata config = DockerConfigurationMetadata.from(this.environment::get); + assertThat(config.getConfiguration().getCurrentContext()).isNull(); + assertThat(config.getContext().getDockerHost()).isNull(); + assertThat(config.getContext().isTlsVerify()).isFalse(); + assertThat(config.getContext().getTlsPath()).isNull(); + } + + @Test + void configWithDefaultContextIsRead() throws Exception { + this.environment.put("DOCKER_CONFIG", pathToResource("with-default-context/config.json")); + DockerConfigurationMetadata config = DockerConfigurationMetadata.from(this.environment::get); + assertThat(config.getConfiguration().getCurrentContext()).isEqualTo("default"); + assertThat(config.getContext().getDockerHost()).isNull(); + assertThat(config.getContext().isTlsVerify()).isFalse(); + assertThat(config.getContext().getTlsPath()).isNull(); + } + + @Test + void configIsReadWithProvidedContext() throws Exception { + this.environment.put("DOCKER_CONFIG", pathToResource("with-default-context/config.json")); + DockerConfigurationMetadata config = DockerConfigurationMetadata.from(this.environment::get); + DockerContext context = config.forContext("test-context"); + assertThat(context.getDockerHost()).isEqualTo("unix:///home/user/.docker/docker.sock"); + assertThat(context.isTlsVerify()).isTrue(); + assertThat(context.getTlsPath()).matches("^.*/with-default-context/contexts/tls/[a-zA-z0-9]*/docker$"); + } + + @Test + void invalidContextThrowsException() throws Exception { + this.environment.put("DOCKER_CONFIG", pathToResource("with-default-context/config.json")); + assertThatIllegalArgumentException() + .isThrownBy(() -> DockerConfigurationMetadata.from(this.environment::get).forContext("invalid-context")) + .withMessageContaining("Docker context 'invalid-context' does not exist"); + } + + @Test + void configIsEmptyWhenConfigFileDoesNotExist() { + this.environment.put("DOCKER_CONFIG", "docker-config-dummy-path"); + DockerConfigurationMetadata config = DockerConfigurationMetadata.from(this.environment::get); + assertThat(config.getConfiguration().getCurrentContext()).isNull(); + assertThat(config.getContext().getDockerHost()).isNull(); + assertThat(config.getContext().isTlsVerify()).isFalse(); + } + + private String pathToResource(String resource) throws URISyntaxException { + URL url = getClass().getResource(resource); + return Paths.get(url.toURI()).getParent().toAbsolutePath().toString(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/ResolvedDockerHostTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/ResolvedDockerHostTests.java index 30a1c358304b..131299849788 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/ResolvedDockerHostTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/ResolvedDockerHostTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -17,8 +17,11 @@ package org.springframework.boot.buildpack.platform.docker.configuration; import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.LinkedHashMap; import java.util.Map; @@ -28,6 +31,8 @@ import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.io.TempDir; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; + import static org.assertj.core.api.Assertions.assertThat; /** @@ -41,7 +46,8 @@ class ResolvedDockerHostTests { @Test @DisabledOnOs(OS.WINDOWS) - void resolveWhenDockerHostIsNullReturnsLinuxDefault() { + void resolveWhenDockerHostIsNullReturnsLinuxDefault() throws Exception { + this.environment.put("DOCKER_CONFIG", pathToResource("with-default-context/config.json")); ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, null); assertThat(dockerHost.getAddress()).isEqualTo("/var/run/docker.sock"); assertThat(dockerHost.isSecure()).isFalse(); @@ -50,7 +56,8 @@ void resolveWhenDockerHostIsNullReturnsLinuxDefault() { @Test @EnabledOnOs(OS.WINDOWS) - void resolveWhenDockerHostIsNullReturnsWindowsDefault() { + void resolveWhenDockerHostIsNullReturnsWindowsDefault() throws Exception { + this.environment.put("DOCKER_CONFIG", pathToResource("with-default-context/config.json")); ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, null); assertThat(dockerHost.getAddress()).isEqualTo("//./pipe/docker_engine"); assertThat(dockerHost.isSecure()).isFalse(); @@ -59,8 +66,10 @@ void resolveWhenDockerHostIsNullReturnsWindowsDefault() { @Test @DisabledOnOs(OS.WINDOWS) - void resolveWhenDockerHostAddressIsNullReturnsLinuxDefault() { - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, new DockerHost(null)); + void resolveWhenDockerHostAddressIsNullReturnsLinuxDefault() throws Exception { + this.environment.put("DOCKER_CONFIG", pathToResource("with-default-context/config.json")); + ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, + DockerHostConfiguration.forAddress(null)); assertThat(dockerHost.getAddress()).isEqualTo("/var/run/docker.sock"); assertThat(dockerHost.isSecure()).isFalse(); assertThat(dockerHost.getCertificatePath()).isNull(); @@ -70,7 +79,7 @@ void resolveWhenDockerHostAddressIsNullReturnsLinuxDefault() { void resolveWhenDockerHostAddressIsLocalReturnsAddress(@TempDir Path tempDir) throws IOException { String socketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath().toString(); ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, - new DockerHost(socketFilePath, false, null)); + DockerHostConfiguration.forAddress(socketFilePath)); assertThat(dockerHost.isLocalFileReference()).isTrue(); assertThat(dockerHost.isRemote()).isFalse(); assertThat(dockerHost.getAddress()).isEqualTo(socketFilePath); @@ -82,7 +91,7 @@ void resolveWhenDockerHostAddressIsLocalReturnsAddress(@TempDir Path tempDir) th void resolveWhenDockerHostAddressIsLocalWithSchemeReturnsAddress(@TempDir Path tempDir) throws IOException { String socketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath().toString(); ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, - new DockerHost("unix://" + socketFilePath, false, null)); + DockerHostConfiguration.forAddress("unix://" + socketFilePath)); assertThat(dockerHost.isLocalFileReference()).isTrue(); assertThat(dockerHost.isRemote()).isFalse(); assertThat(dockerHost.getAddress()).isEqualTo(socketFilePath); @@ -93,7 +102,7 @@ void resolveWhenDockerHostAddressIsLocalWithSchemeReturnsAddress(@TempDir Path t @Test void resolveWhenDockerHostAddressIsHttpReturnsAddress() { ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, - new DockerHost("http://docker.example.com", false, null)); + DockerHostConfiguration.forAddress("http://docker.example.com")); assertThat(dockerHost.isLocalFileReference()).isFalse(); assertThat(dockerHost.isRemote()).isTrue(); assertThat(dockerHost.getAddress()).isEqualTo("http://docker.example.com"); @@ -104,7 +113,7 @@ void resolveWhenDockerHostAddressIsHttpReturnsAddress() { @Test void resolveWhenDockerHostAddressIsHttpsReturnsAddress() { ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, - new DockerHost("https://docker.example.com", true, "/cert-path")); + DockerHostConfiguration.forAddress("https://docker.example.com", true, "/cert-path")); assertThat(dockerHost.isLocalFileReference()).isFalse(); assertThat(dockerHost.isRemote()).isTrue(); assertThat(dockerHost.getAddress()).isEqualTo("https://docker.example.com"); @@ -115,7 +124,7 @@ void resolveWhenDockerHostAddressIsHttpsReturnsAddress() { @Test void resolveWhenDockerHostAddressIsTcpReturnsAddress() { ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, - new DockerHost("tcp://192.168.99.100:2376", true, "/cert-path")); + DockerHostConfiguration.forAddress("tcp://192.168.99.100:2376", true, "/cert-path")); assertThat(dockerHost.isLocalFileReference()).isFalse(); assertThat(dockerHost.isRemote()).isTrue(); assertThat(dockerHost.getAddress()).isEqualTo("tcp://192.168.99.100:2376"); @@ -128,7 +137,7 @@ void resolveWhenEnvironmentAddressIsLocalReturnsAddress(@TempDir Path tempDir) t String socketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath().toString(); this.environment.put("DOCKER_HOST", socketFilePath); ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, - new DockerHost("/unused", true, "/unused")); + DockerHostConfiguration.forAddress("/unused")); assertThat(dockerHost.isLocalFileReference()).isTrue(); assertThat(dockerHost.isRemote()).isFalse(); assertThat(dockerHost.getAddress()).isEqualTo(socketFilePath); @@ -141,7 +150,7 @@ void resolveWhenEnvironmentAddressIsLocalWithSchemeReturnsAddress(@TempDir Path String socketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath().toString(); this.environment.put("DOCKER_HOST", "unix://" + socketFilePath); ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, - new DockerHost("/unused", true, "/unused")); + DockerHostConfiguration.forAddress("/unused")); assertThat(dockerHost.isLocalFileReference()).isTrue(); assertThat(dockerHost.isRemote()).isFalse(); assertThat(dockerHost.getAddress()).isEqualTo(socketFilePath); @@ -155,7 +164,7 @@ void resolveWhenEnvironmentAddressIsTcpReturnsAddress() { this.environment.put("DOCKER_TLS_VERIFY", "1"); this.environment.put("DOCKER_CERT_PATH", "/cert-path"); ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, - new DockerHost("tcp://1.1.1.1", false, "/unused")); + DockerHostConfiguration.forAddress("tcp://1.1.1.1")); assertThat(dockerHost.isLocalFileReference()).isFalse(); assertThat(dockerHost.isRemote()).isTrue(); assertThat(dockerHost.getAddress()).isEqualTo("tcp://192.168.99.100:2376"); @@ -163,4 +172,39 @@ void resolveWhenEnvironmentAddressIsTcpReturnsAddress() { assertThat(dockerHost.getCertificatePath()).isEqualTo("/cert-path"); } + @Test + void resolveWithDockerHostContextReturnsAddress() throws Exception { + this.environment.put("DOCKER_CONFIG", pathToResource("with-default-context/config.json")); + ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, + DockerHostConfiguration.forContext("test-context")); + assertThat(dockerHost.getAddress()).isEqualTo("/home/user/.docker/docker.sock"); + assertThat(dockerHost.isSecure()).isTrue(); + assertThat(dockerHost.getCertificatePath()).isNotNull(); + } + + @Test + void resolveWithDockerConfigMetadataContextReturnsAddress() throws Exception { + this.environment.put("DOCKER_CONFIG", pathToResource("with-context/config.json")); + ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, null); + assertThat(dockerHost.getAddress()).isEqualTo("/home/user/.docker/docker.sock"); + assertThat(dockerHost.isSecure()).isFalse(); + assertThat(dockerHost.getCertificatePath()).isNull(); + } + + @Test + void resolveWhenEnvironmentHasAddressAndContextPrefersContext() throws Exception { + this.environment.put("DOCKER_CONFIG", pathToResource("with-context/config.json")); + this.environment.put("DOCKER_CONTEXT", "test-context"); + this.environment.put("DOCKER_HOST", "notused"); + ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, null); + assertThat(dockerHost.getAddress()).isEqualTo("/home/user/.docker/docker.sock"); + assertThat(dockerHost.isSecure()).isFalse(); + assertThat(dockerHost.getCertificatePath()).isNull(); + } + + private String pathToResource(String resource) throws URISyntaxException { + URL url = getClass().getResource(resource); + return Paths.get(url.toURI()).getParent().toAbsolutePath().toString(); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransportTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransportTests.java index c04cd5e719db..a383d0240ec9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransportTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -23,7 +23,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; import static org.assertj.core.api.Assertions.assertThat; @@ -37,21 +37,21 @@ class HttpTransportTests { @Test void createWhenDockerHostVariableIsAddressReturnsRemote() { - HttpTransport transport = HttpTransport.create(new DockerHost("tcp://192.168.1.0")); + HttpTransport transport = HttpTransport.create(DockerHostConfiguration.forAddress("tcp://192.168.1.0")); assertThat(transport).isInstanceOf(RemoteHttpClientTransport.class); } @Test void createWhenDockerHostVariableIsFileReturnsLocal(@TempDir Path tempDir) throws IOException { String dummySocketFilePath = Files.createTempFile(tempDir, "http-transport", null).toAbsolutePath().toString(); - HttpTransport transport = HttpTransport.create(new DockerHost(dummySocketFilePath)); + HttpTransport transport = HttpTransport.create(DockerHostConfiguration.forAddress(dummySocketFilePath)); assertThat(transport).isInstanceOf(LocalHttpClientTransport.class); } @Test void createWhenDockerHostVariableIsUnixSchemePrefixedFileReturnsLocal(@TempDir Path tempDir) throws IOException { String dummySocketFilePath = "unix://" + Files.createTempFile(tempDir, "http-transport", null).toAbsolutePath(); - HttpTransport transport = HttpTransport.create(new DockerHost(dummySocketFilePath)); + HttpTransport transport = HttpTransport.create(DockerHostConfiguration.forAddress(dummySocketFilePath)); assertThat(transport).isInstanceOf(LocalHttpClientTransport.class); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/LocalHttpClientTransportTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/LocalHttpClientTransportTests.java index 78ff1d0c71fe..81cd780c5b04 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/LocalHttpClientTransportTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/LocalHttpClientTransportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -24,7 +24,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost; import static org.assertj.core.api.Assertions.assertThat; @@ -39,24 +39,28 @@ class LocalHttpClientTransportTests { @Test void createWhenDockerHostIsFileReturnsTransport(@TempDir Path tempDir) throws IOException { String socketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath().toString(); - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(new DockerHost(socketFilePath)); + ResolvedDockerHost dockerHost = ResolvedDockerHost.from(DockerHostConfiguration.forAddress(socketFilePath)); LocalHttpClientTransport transport = LocalHttpClientTransport.create(dockerHost); assertThat(transport).isNotNull(); + assertThat(transport.getHost().toHostString()).isEqualTo(socketFilePath); } @Test void createWhenDockerHostIsFileThatDoesNotExistReturnsTransport(@TempDir Path tempDir) { String socketFilePath = Paths.get(tempDir.toString(), "dummy").toAbsolutePath().toString(); - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(new DockerHost(socketFilePath)); + ResolvedDockerHost dockerHost = ResolvedDockerHost.from(DockerHostConfiguration.forAddress(socketFilePath)); LocalHttpClientTransport transport = LocalHttpClientTransport.create(dockerHost); assertThat(transport).isNotNull(); + assertThat(transport.getHost().toHostString()).isEqualTo(socketFilePath); } @Test void createWhenDockerHostIsAddressReturnsTransport() { - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(new DockerHost("tcp://192.168.1.2:2376")); + ResolvedDockerHost dockerHost = ResolvedDockerHost + .from(DockerHostConfiguration.forAddress("tcp://192.168.1.2:2376")); LocalHttpClientTransport transport = LocalHttpClientTransport.create(dockerHost); assertThat(transport).isNotNull(); + assertThat(transport.getHost().toHostString()).isEqualTo("tcp://192.168.1.2:2376"); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/RemoteHttpClientTransportTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/RemoteHttpClientTransportTests.java index a56373709eff..529709d5cc38 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/RemoteHttpClientTransportTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/RemoteHttpClientTransportTests.java @@ -23,7 +23,7 @@ import org.apache.hc.core5.http.HttpHost; import org.junit.jupiter.api.Test; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost; import org.springframework.boot.buildpack.platform.docker.ssl.SslContextFactory; @@ -49,28 +49,31 @@ void createIfPossibleWhenDockerHostIsNotSetReturnsNull() { @Test void createIfPossibleWhenDockerHostIsDefaultReturnsNull() { - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(new DockerHost(null)); + ResolvedDockerHost dockerHost = ResolvedDockerHost.from(DockerHostConfiguration.forAddress(null)); RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(dockerHost); assertThat(transport).isNull(); } @Test void createIfPossibleWhenDockerHostIsFileReturnsNull() { - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(new DockerHost("unix:///var/run/socket.sock")); + ResolvedDockerHost dockerHost = ResolvedDockerHost + .from(DockerHostConfiguration.forAddress("unix:///var/run/socket.sock")); RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(dockerHost); assertThat(transport).isNull(); } @Test void createIfPossibleWhenDockerHostIsAddressReturnsTransport() { - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(new DockerHost("tcp://192.168.1.2:2376")); + ResolvedDockerHost dockerHost = ResolvedDockerHost + .from(DockerHostConfiguration.forAddress("tcp://192.168.1.2:2376")); RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(dockerHost); assertThat(transport).isNotNull(); } @Test void createIfPossibleWhenNoTlsVerifyUsesHttp() { - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(new DockerHost("tcp://192.168.1.2:2376")); + ResolvedDockerHost dockerHost = ResolvedDockerHost + .from(DockerHostConfiguration.forAddress("tcp://192.168.1.2:2376")); RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(dockerHost); assertThat(transport.getHost()).satisfies(hostOf("http", "192.168.1.2", 2376)); } @@ -80,14 +83,15 @@ void createIfPossibleWhenTlsVerifyUsesHttps() throws Exception { SslContextFactory sslContextFactory = mock(SslContextFactory.class); given(sslContextFactory.forDirectory("/test-cert-path")).willReturn(SSLContext.getDefault()); ResolvedDockerHost dockerHost = ResolvedDockerHost - .from(new DockerHost("tcp://192.168.1.2:2376", true, "/test-cert-path")); + .from(DockerHostConfiguration.forAddress("tcp://192.168.1.2:2376", true, "/test-cert-path")); RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(dockerHost, sslContextFactory); assertThat(transport.getHost()).satisfies(hostOf("https", "192.168.1.2", 2376)); } @Test void createIfPossibleWhenTlsVerifyWithMissingCertPathThrowsException() { - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(new DockerHost("tcp://192.168.1.2:2376", true, null)); + ResolvedDockerHost dockerHost = ResolvedDockerHost + .from(DockerHostConfiguration.forAddress("tcp://192.168.1.2:2376", true, null)); assertThatIllegalArgumentException().isThrownBy(() -> RemoteHttpClientTransport.createIfPossible(dockerHost)) .withMessageContaining("Docker host TLS verification requires trust material"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-context/config.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-context/config.json new file mode 100644 index 000000000000..7e3fa77f5bfe --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-context/config.json @@ -0,0 +1,3 @@ +{ + "currentContext": "test-context" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-context/contexts/meta/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/meta.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-context/contexts/meta/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/meta.json new file mode 100644 index 000000000000..fa4655b1a026 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-context/contexts/meta/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/meta.json @@ -0,0 +1,12 @@ +{ + "Name": "test-context", + "Metadata": { + "Description": "A context for testing" + }, + "Endpoints": { + "docker": { + "Host": "unix:///home/user/.docker/docker.sock", + "SkipTLSVerify": true + } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/config.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/config.json new file mode 100644 index 000000000000..6eaf50253da3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/config.json @@ -0,0 +1,3 @@ +{ + "currentContext": "default" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/contexts/meta/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/meta.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/contexts/meta/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/meta.json new file mode 100644 index 000000000000..f072aa2647e2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/contexts/meta/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/meta.json @@ -0,0 +1,12 @@ +{ + "Name": "test-context", + "Metadata": { + "Description": "A context for testing" + }, + "Endpoints": { + "docker": { + "Host": "unix:///home/user/.docker/docker.sock", + "SkipTLSVerify": false + } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/contexts/tls/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/docker/cert.pem b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/contexts/tls/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/docker/cert.pem new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/contexts/tls/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/docker/key.pem b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/contexts/tls/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/docker/key.pem new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/without-context/config.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/without-context/config.json new file mode 100644 index 000000000000..2c63c0851048 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/without-context/config.json @@ -0,0 +1,2 @@ +{ +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc index c5e429729335..824c79ee0049 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -13,7 +13,8 @@ The task is automatically created when the `java` or `war` plugin is applied and [[build-image.docker-daemon]] == Docker Daemon The `bootBuildImage` task requires access to a Docker daemon. -By default, it will communicate with a Docker daemon over a local connection. +The task will inspect local Docker CLI https://docs.docker.com/engine/reference/commandline/cli/#configuration-files[configuration files] to determine the current https://docs.docker.com/engine/context/working-with-contexts/[context] and use the context connection information to communicate with a Docker daemon. +If the current context can not be determined or the context does not have connection information, then the task will use a default local connection. This works with https://docs.docker.com/install/[Docker Engine] on all supported platforms without configuration. Environment variables can be set to configure the `bootBuildImage` task to use an alternative local or remote connection. @@ -22,6 +23,12 @@ The following table shows the environment variables and their values: |=== | Environment variable | Description +| DOCKER_CONFIG +| Location of Docker CLI https://docs.docker.com/engine/reference/commandline/cli/#configuration-files[configuration files] used to determine the current context (defaults to `$HOME/.docker`) + +| DOCKER_CONTEXT +| Name of a https://docs.docker.com/engine/context/working-with-contexts/[context] that should be used to retrieve host information from Docker CLI configuration files (overrides `DOCKER_HOST`) + | DOCKER_HOST | URL containing the host and port for the Docker daemon - for example `tcp://192.168.99.100:2376` @@ -38,6 +45,9 @@ The following table summarizes the available properties: |=== | Property | Description +| `context` +| Name of a https://docs.docker.com/engine/context/working-with-contexts/[context] that should be used to retrieve host information from Docker CLI https://docs.docker.com/engine/reference/commandline/cli/#configuration-files[configuration files] + | `host` | URL containing the host and port for the Docker daemon - for example `tcp://192.168.99.100:2376` diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DockerSpec.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DockerSpec.java index ce3907a4db16..ffed3ddba17c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DockerSpec.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DockerSpec.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -54,6 +54,10 @@ public DockerSpec(ObjectFactory objects) { this.publishRegistry = publishRegistry; } + @Input + @Optional + public abstract Property getContext(); + @Input @Optional public abstract Property getHost(); @@ -124,7 +128,15 @@ DockerConfiguration asDockerConfiguration() { } private DockerConfiguration customizeHost(DockerConfiguration dockerConfiguration) { + String context = getContext().getOrNull(); String host = getHost().getOrNull(); + if (context != null && host != null) { + throw new GradleException( + "Invalid Docker configuration, either context or host can be provided but not both"); + } + if (context != null) { + return dockerConfiguration.withContext(context); + } if (host != null) { return dockerConfiguration.withHost(host, getTlsVerify().get(), getCertPath().getOrNull()); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DockerSpecTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DockerSpecTests.java index 7cce2758b0ad..3252cedb2da0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DockerSpecTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DockerSpecTests.java @@ -25,7 +25,7 @@ import org.junit.jupiter.api.io.TempDir; import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; import org.springframework.boot.gradle.junit.GradleProjectBuilder; import static org.assertj.core.api.Assertions.assertThat; @@ -68,10 +68,11 @@ void asDockerConfigurationWithHostConfiguration() { this.dockerSpec.getTlsVerify().set(true); this.dockerSpec.getCertPath().set("/tmp/ca-cert"); DockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration(); - DockerHost host = dockerConfiguration.getHost(); + DockerHostConfiguration host = dockerConfiguration.getHost(); assertThat(host.getAddress()).isEqualTo("docker.example.com"); assertThat(host.isSecure()).isTrue(); assertThat(host.getCertificatePath()).isEqualTo("/tmp/ca-cert"); + assertThat(host.getContext()).isNull(); assertThat(dockerConfiguration.isBindHostToBuilder()).isFalse(); assertThat(this.dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull(); assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) @@ -85,10 +86,11 @@ void asDockerConfigurationWithHostConfiguration() { void asDockerConfigurationWithHostConfigurationNoTlsVerify() { this.dockerSpec.getHost().set("docker.example.com"); DockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration(); - DockerHost host = dockerConfiguration.getHost(); + DockerHostConfiguration host = dockerConfiguration.getHost(); assertThat(host.getAddress()).isEqualTo("docker.example.com"); assertThat(host.isSecure()).isFalse(); assertThat(host.getCertificatePath()).isNull(); + assertThat(host.getContext()).isNull(); assertThat(dockerConfiguration.isBindHostToBuilder()).isFalse(); assertThat(this.dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull(); assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) @@ -98,12 +100,38 @@ void asDockerConfigurationWithHostConfigurationNoTlsVerify() { .contains("\"serveraddress\" : \"\""); } + @Test + void asDockerConfigurationWithContextConfiguration() { + this.dockerSpec.getContext().set("test-context"); + DockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration(); + DockerHostConfiguration host = dockerConfiguration.getHost(); + assertThat(host.getContext()).isEqualTo("test-context"); + assertThat(host.getAddress()).isNull(); + assertThat(host.isSecure()).isFalse(); + assertThat(host.getCertificatePath()).isNull(); + assertThat(dockerConfiguration.isBindHostToBuilder()).isFalse(); + assertThat(this.dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull(); + assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) + .contains("\"username\" : \"\"") + .contains("\"password\" : \"\"") + .contains("\"email\" : \"\"") + .contains("\"serveraddress\" : \"\""); + } + + @Test + void asDockerConfigurationWithHostAndContextFails() { + this.dockerSpec.getContext().set("test-context"); + this.dockerSpec.getHost().set("docker.example.com"); + assertThatExceptionOfType(GradleException.class).isThrownBy(this.dockerSpec::asDockerConfiguration) + .withMessageContaining("Invalid Docker configuration"); + } + @Test void asDockerConfigurationWithBindHostToBuilder() { this.dockerSpec.getHost().set("docker.example.com"); this.dockerSpec.getBindHostToBuilder().set(true); DockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration(); - DockerHost host = dockerConfiguration.getHost(); + DockerHostConfiguration host = dockerConfiguration.getHost(); assertThat(host.getAddress()).isEqualTo("docker.example.com"); assertThat(host.isSecure()).isFalse(); assertThat(host.getCertificatePath()).isNull(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc index f9b084ce7eea..09e85abb8209 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -28,7 +28,8 @@ The `spring-boot-devtools` and `spring-boot-docker-compose` modules are automati [[build-image.docker-daemon]] == Docker Daemon The `build-image` goal requires access to a Docker daemon. -By default, it will communicate with a Docker daemon over a local connection. +The goal will inspect local Docker CLI https://docs.docker.com/engine/reference/commandline/cli/#configuration-files[configuration files] to determine the current https://docs.docker.com/engine/context/working-with-contexts/[context] and use the context connection information to communicate with a Docker daemon. +If the current context can not be determined or the context does not have connection information, then the goal will use a default local connection. This works with https://docs.docker.com/install/[Docker Engine] on all supported platforms without configuration. Environment variables can be set to configure the `build-image` goal to use an alternative local or remote connection. @@ -37,6 +38,12 @@ The following table shows the environment variables and their values: |=== | Environment variable | Description +| DOCKER_CONFIG +| Location of Docker CLI https://docs.docker.com/engine/reference/commandline/cli/#configuration-files[configuration files] used to determine the current context (defaults to `$HOME/.docker`) + +| DOCKER_CONTEXT +| Name of a https://docs.docker.com/engine/context/working-with-contexts/[context] that should be used to retrieve host information from Docker CLI configuration files (overrides `DOCKER_HOST`) + | DOCKER_HOST | URL containing the host and port for the Docker daemon - for example `tcp://192.168.99.100:2376` @@ -53,6 +60,9 @@ The following table summarizes the available parameters: |=== | Parameter | Description +| `context` +| Name of a https://docs.docker.com/engine/context/working-with-contexts/[context] that should be used to retrieve host information from Docker CLI https://docs.docker.com/engine/reference/commandline/cli/#configuration-files[configuration files] + | `host` | URL containing the host and port for the Docker daemon - for example `tcp://192.168.99.100:2376` diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Docker.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Docker.java index 78e8b5b89b98..53618609d4d7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Docker.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Docker.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -29,6 +29,8 @@ public class Docker { private String host; + private String context; + private boolean tlsVerify; private String certPath; @@ -51,6 +53,18 @@ void setHost(String host) { this.host = host; } + /** + * The Docker context to use to retrieve host configuration. + * @return the Docker context + */ + public String getContext() { + return this.context; + } + + public void setContext(String context) { + this.context = context; + } + /** * Whether the Docker daemon requires TLS communication. * @return {@code true} to enable TLS @@ -138,6 +152,13 @@ DockerConfiguration asDockerConfiguration() { } private DockerConfiguration customizeHost(DockerConfiguration dockerConfiguration) { + if (this.context != null && this.host != null) { + throw new IllegalArgumentException( + "Invalid Docker configuration, either context or host can be provided but not both"); + } + if (this.context != null) { + return dockerConfiguration.withContext(this.context); + } if (this.host != null) { return dockerConfiguration.withHost(this.host, this.tlsVerify, this.certPath); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DockerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DockerTests.java index 1fd381ee28de..f2258b915dc3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DockerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DockerTests.java @@ -21,7 +21,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -54,10 +54,11 @@ void asDockerConfigurationWithHostConfiguration() { docker.setTlsVerify(true); docker.setCertPath("/tmp/ca-cert"); DockerConfiguration dockerConfiguration = docker.asDockerConfiguration(); - DockerHost host = dockerConfiguration.getHost(); + DockerHostConfiguration host = dockerConfiguration.getHost(); assertThat(host.getAddress()).isEqualTo("docker.example.com"); assertThat(host.isSecure()).isTrue(); assertThat(host.getCertificatePath()).isEqualTo("/tmp/ca-cert"); + assertThat(host.getContext()).isNull(); assertThat(dockerConfiguration.isBindHostToBuilder()).isFalse(); assertThat(docker.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull(); assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) @@ -67,6 +68,34 @@ void asDockerConfigurationWithHostConfiguration() { .contains("\"serveraddress\" : \"\""); } + @Test + void asDockerConfigurationWithContextConfiguration() { + Docker docker = new Docker(); + docker.setContext("test-context"); + DockerConfiguration dockerConfiguration = docker.asDockerConfiguration(); + DockerHostConfiguration host = dockerConfiguration.getHost(); + assertThat(host.getContext()).isEqualTo("test-context"); + assertThat(host.getAddress()).isNull(); + assertThat(host.isSecure()).isFalse(); + assertThat(host.getCertificatePath()).isNull(); + assertThat(dockerConfiguration.isBindHostToBuilder()).isFalse(); + assertThat(docker.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull(); + assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) + .contains("\"username\" : \"\"") + .contains("\"password\" : \"\"") + .contains("\"email\" : \"\"") + .contains("\"serveraddress\" : \"\""); + } + + @Test + void asDockerConfigurationWithHostAndContextFails() { + Docker docker = new Docker(); + docker.setContext("test-context"); + docker.setHost("docker.example.com"); + assertThatIllegalArgumentException().isThrownBy(docker::asDockerConfiguration) + .withMessageContaining("Invalid Docker configuration"); + } + @Test void asDockerConfigurationWithBindHostToBuilder() { Docker docker = new Docker(); @@ -75,7 +104,7 @@ void asDockerConfigurationWithBindHostToBuilder() { docker.setCertPath("/tmp/ca-cert"); docker.setBindHostToBuilder(true); DockerConfiguration dockerConfiguration = docker.asDockerConfiguration(); - DockerHost host = dockerConfiguration.getHost(); + DockerHostConfiguration host = dockerConfiguration.getHost(); assertThat(host.getAddress()).isEqualTo("docker.example.com"); assertThat(host.isSecure()).isTrue(); assertThat(host.getCertificatePath()).isEqualTo("/tmp/ca-cert"); From 9efbe5b2eaed42bb2183157ea9ff9bf231f3338e Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 19 Jul 2023 07:44:39 +0200 Subject: [PATCH 0165/1215] Upgrade to Spring GraphQL 1.2.2 Closes gh-36191 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 8a55db136838..fd169b6c10ce 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1400,7 +1400,7 @@ bom { ] } } - library("Spring GraphQL", "1.2.2-SNAPSHOT") { + library("Spring GraphQL", "1.2.2") { group("org.springframework.graphql") { modules = [ "spring-graphql", From 0a365acf9594d1f78975b0bcd5523f38d4bd404b Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 19 Jul 2023 07:45:15 +0200 Subject: [PATCH 0166/1215] Upgrade to Spring Integration 6.2.0-M1 Closes gh-36193 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index fd169b6c10ce..aa3cce4e2adc 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1415,7 +1415,7 @@ bom { ] } } - library("Spring Integration", "6.2.0-SNAPSHOT") { + library("Spring Integration", "6.2.0-M1") { group("org.springframework.integration") { imports = [ "spring-integration-bom" From 858cadc8e26da3270c0767315cc07459983d8eaf Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 19 Jul 2023 07:46:08 +0200 Subject: [PATCH 0167/1215] Upgrade to Spring LDAP 3.2.0-M1 Closes gh-36299 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index aa3cce4e2adc..5c1a3e103360 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1430,7 +1430,7 @@ bom { ] } } - library("Spring LDAP", "3.2.0-SNAPSHOT") { + library("Spring LDAP", "3.2.0-M1") { group("org.springframework.ldap") { modules = [ "spring-ldap-core", From 2506172d91c14005d080b3a440ef588201196382 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 19 Jul 2023 07:46:39 +0200 Subject: [PATCH 0168/1215] Upgrade to Spring Session 3.2.0-M1 Closes gh-36196 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5c1a3e103360..08d12e4cbfb6 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1461,7 +1461,7 @@ bom { ] } } - library("Spring Session", "3.2.0-SNAPSHOT") { + library("Spring Session", "3.2.0-M1") { prohibit { startsWith(["Apple-", "Bean-", "Corn-", "Dragonfruit-"]) because "Spring Session switched to numeric version numbers" From abebc396c0b6f41d47be5eafe020ecdd2bba32b0 Mon Sep 17 00:00:00 2001 From: kitbolourchi <74569356+KitBolourchi@users.noreply.github.com> Date: Sat, 24 Jun 2023 15:32:57 +0100 Subject: [PATCH 0169/1215] Change B3 extraction format to single See gh-36061 --- .../tracing/CompositePropagationFactory.java | 2 +- .../tracing/BraveAutoConfigurationTests.java | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/CompositePropagationFactory.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/CompositePropagationFactory.java index 0ec3db30c2b7..2c653b362d4e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/CompositePropagationFactory.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/CompositePropagationFactory.java @@ -124,7 +124,7 @@ Propagation.Factory map(PropagationType type) { * @return the B3 propagation factory */ private Propagation.Factory b3Single() { - return B3Propagation.newFactoryBuilder().injectFormat(B3Propagation.Format.SINGLE_NO_PARENT).build(); + return B3Propagation.newFactoryBuilder().injectFormat(B3Propagation.Format.SINGLE).build(); } /** diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java index f97f4c8c8c8a..372166ed2555 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java @@ -17,7 +17,9 @@ package org.springframework.boot.actuate.autoconfigure.tracing; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Stream; import brave.Span; @@ -31,6 +33,7 @@ import brave.propagation.CurrentTraceContext.ScopeDecorator; import brave.propagation.Propagation; import brave.propagation.Propagation.Factory; +import brave.propagation.TraceContext; import brave.sampler.Sampler; import io.micrometer.tracing.brave.bridge.BraveBaggageManager; import io.micrometer.tracing.brave.bridge.BraveSpanCustomizer; @@ -152,6 +155,31 @@ void shouldSupplyB3PropagationFactoryViaProperty() { }); } + @Test + void shouldUseB3SingleWithParentWhenPropagationTypeIsB3() { + this.contextRunner + .withPropertyValues("management.tracing.propagation.type=B3", "management.tracing.sampling.probability=1.0") + .run((context) -> { + Propagation propagation = context.getBean(Factory.class).get(); + Tracer tracer = context.getBean(Tracing.class).tracer(); + Span child; + Span parent = tracer.nextSpan().name("parent"); + try (Tracer.SpanInScope ignored = tracer.withSpanInScope(parent.start())) { + child = tracer.nextSpan().name("child"); + child.start().finish(); + } + finally { + parent.finish(); + } + + Map map = new HashMap<>(); + TraceContext childContext = child.context(); + propagation.injector(this::injectToMap).inject(childContext, map); + assertThat(map).containsExactly(Map.entry("b3", "%s-%s-1-%s".formatted(childContext.traceIdString(), + childContext.spanIdString(), childContext.parentIdString()))); + }); + } + @Test void shouldNotSupplyCorrelationScopeDecoratorIfBaggageDisabled() { this.contextRunner.withPropertyValues("management.tracing.baggage.enabled=false") @@ -313,6 +341,10 @@ void compositeSpanHandlerUsesFilterPredicateAndReportersInOrder() { }); } + private void injectToMap(Map map, String key, String value) { + map.put(key, value); + } + private List getInjectors(Factory factory) { assertThat(factory).as("factory").isNotNull(); if (factory instanceof CompositePropagationFactory compositePropagationFactory) { From 0fd3d992b5c7a2266c74a2be9fec2d03b2fdb116 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 19 Jul 2023 20:00:42 +0200 Subject: [PATCH 0170/1215] Upgrade to Spring Framework 6.1.0-M3 Closes gh-36443 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7e5a78d8f27f..aa7abc2f7484 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 kotlinVersion=1.8.22 nativeBuildToolsVersion=0.9.23 -springFrameworkVersion=6.1.0-SNAPSHOT +springFrameworkVersion=6.1.0-M3 tomcatVersion=10.1.11 kotlin.stdlib.default.dependency=false From 4a4b29fcbf6ce803cd30ea21e3b7ebd96f230f48 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 19 Jul 2023 20:01:06 +0200 Subject: [PATCH 0171/1215] Upgrade to Spring Batch 5.1.0-M1 Closes gh-36189 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 08d12e4cbfb6..9b911da4835c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1379,7 +1379,7 @@ bom { ] } } - library("Spring Batch", "5.1.0-SNAPSHOT") { + library("Spring Batch", "5.1.0-M1") { group("org.springframework.batch") { imports = [ "spring-batch-bom" From 2029117999886d60b1940d516778d3e9ee56ed28 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 19 Jul 2023 20:27:41 +0100 Subject: [PATCH 0172/1215] Upgrade to Kotlin 1.9.0 Closes gh-36362 --- gradle.properties | 2 +- .../KotlinPluginActionIntegrationTests.java | 42 ++++++++----------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/gradle.properties b/gradle.properties index aa7abc2f7484..c0cea14c2595 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ org.gradle.caching=true org.gradle.parallel=true org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 -kotlinVersion=1.8.22 +kotlinVersion=1.9.0 nativeBuildToolsVersion=0.9.23 springFrameworkVersion=6.1.0-M3 tomcatVersion=10.1.11 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests.java index cfb35aa41c71..54d8807735da 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests.java @@ -23,11 +23,11 @@ import java.util.Set; import org.gradle.testkit.runner.BuildResult; -import org.gradle.util.GradleVersion; -import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.gradle.junit.GradleCompatibility; import org.springframework.boot.testsupport.gradle.testkit.GradleBuild; +import org.springframework.boot.testsupport.gradle.testkit.GradleBuildExtension; import static org.assertj.core.api.Assertions.assertThat; @@ -36,43 +36,40 @@ * * @author Andy Wilkinson */ -@GradleCompatibility +@ExtendWith(GradleBuildExtension.class) class KotlinPluginActionIntegrationTests { - GradleBuild gradleBuild; + GradleBuild gradleBuild = new GradleBuild(); - @TestTemplate + @Test void noKotlinVersionPropertyWithoutKotlinPlugin() { assertThat(this.gradleBuild.build("kotlinVersion").getOutput()).contains("Kotlin version: none"); } - @TestTemplate + @Test void kotlinVersionPropertyIsSet() { - String output = this.gradleBuild.expectDeprecationWarningsWithAtLeastVersion("8.1-rc-1") - .build("kotlinVersion", "dependencies", "--configuration", "compileClasspath") + String output = this.gradleBuild.build("kotlinVersion", "dependencies", "--configuration", "compileClasspath") .getOutput(); assertThat(output).containsPattern("Kotlin version: [0-9]\\.[0-9]\\.[0-9]+"); } - @TestTemplate + @Test void kotlinCompileTasksUseJavaParametersFlagByDefault() { - assertThat(this.gradleBuild.expectDeprecationWarningsWithAtLeastVersion("8.1-rc-1") - .build("kotlinCompileTasksJavaParameters") - .getOutput()).contains("compileKotlin java parameters: true") + assertThat(this.gradleBuild.build("kotlinCompileTasksJavaParameters").getOutput()) + .contains("compileKotlin java parameters: true") .contains("compileTestKotlin java parameters: true"); } - @TestTemplate + @Test void kotlinCompileTasksCanOverrideDefaultJavaParametersFlag() { - assertThat(this.gradleBuild.expectDeprecationWarningsWithAtLeastVersion("8.1-rc-1") - .build("kotlinCompileTasksJavaParameters") - .getOutput()).contains("compileKotlin java parameters: false") + assertThat(this.gradleBuild.build("kotlinCompileTasksJavaParameters").getOutput()) + .contains("compileKotlin java parameters: false") .contains("compileTestKotlin java parameters: false"); } - @TestTemplate + @Test void taskConfigurationIsAvoided() throws IOException { - BuildResult result = this.gradleBuild.expectDeprecationWarningsWithAtLeastVersion("8.1-rc-1").build("help"); + BuildResult result = this.gradleBuild.build("help"); String output = result.getOutput(); BufferedReader reader = new BufferedReader(new StringReader(output)); String line; @@ -82,12 +79,7 @@ void taskConfigurationIsAvoided() throws IOException { configured.add(line.substring("Configuring :".length())); } } - if (GradleVersion.version(this.gradleBuild.getGradleVersion()).compareTo(GradleVersion.version("7.3.3")) < 0) { - assertThat(configured).containsExactly("help"); - } - else { - assertThat(configured).containsExactlyInAnyOrder("help", "clean"); - } + assertThat(configured).containsExactlyInAnyOrder("help", "clean"); } } From a1db6cd63054d3ee28517b7d835bdacddd44d900 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 20 Jul 2023 08:37:40 +0100 Subject: [PATCH 0173/1215] Upgrade to MySQL 8.1.0 Closes gh-36470 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 9b911da4835c..388f9f213e59 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1018,7 +1018,7 @@ bom { ] } } - library("MySQL", "8.0.33") { + library("MySQL", "8.1.0") { group("com.mysql") { modules = [ "mysql-connector-j" { From 8100aba33230fee5a29925bf70062f24db3ef804 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 20 Jul 2023 08:37:45 +0100 Subject: [PATCH 0174/1215] Upgrade to R2DBC MSSQL 1.0.2.RELEASE Closes gh-36471 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 388f9f213e59..fe282c569c8c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1139,7 +1139,7 @@ bom { ] } } - library("R2DBC MSSQL", "1.0.1.RELEASE") { + library("R2DBC MSSQL", "1.0.2.RELEASE") { group ("io.r2dbc") { modules = [ "r2dbc-mssql" From 34d02659a829892e9d83247a8723b4d34be7f629 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 20 Jul 2023 08:37:50 +0100 Subject: [PATCH 0175/1215] Upgrade to R2DBC Pool 1.0.1.RELEASE Closes gh-36472 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index fe282c569c8c..6dd420394d8a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1153,7 +1153,7 @@ bom { ] } } - library("R2DBC Pool", "1.0.0.RELEASE") { + library("R2DBC Pool", "1.0.1.RELEASE") { group("io.r2dbc") { modules = [ "r2dbc-pool" From 7e344c0ef6d8caad58368376439dae722ebed502 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 20 Jul 2023 08:37:55 +0100 Subject: [PATCH 0176/1215] Upgrade to R2DBC Postgresql 1.0.2.RELEASE Closes gh-36473 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 6dd420394d8a..c704aaa9efd1 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1160,7 +1160,7 @@ bom { ] } } - library("R2DBC Postgresql", "1.0.1.RELEASE") { + library("R2DBC Postgresql", "1.0.2.RELEASE") { group("org.postgresql") { modules = [ "r2dbc-postgresql" From 90a38b6b0bdfb4d27b79933bbfacc38b8e73c7f3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 20 Jul 2023 08:37:55 +0100 Subject: [PATCH 0177/1215] Upgrade to Spring HATEOAS 2.2.0-M2 Closes gh-36456 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c704aaa9efd1..e2d22cde5fe9 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1408,7 +1408,7 @@ bom { ] } } - library("Spring HATEOAS", "2.2.0-M1") { + library("Spring HATEOAS", "2.2.0-M2") { group("org.springframework.hateoas") { modules = [ "spring-hateoas" From 54e99d68fabbf6a810789a74abc4af03b25158d3 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 30 Jun 2023 11:54:31 +0200 Subject: [PATCH 0178/1215] Auto-configure ObservationRegistry on ScheduledTaskRegistrar The TaskSchedulingAutoConfiguration.taskScheduler auto-configuration now no longer backs off on SchedulingConfigurer beans. Closes gh-36119 --- ...edTasksObservabilityAutoConfiguration.java | 63 ++++++++++++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + ...ksObservabilityAutoConfigurationTests.java | 64 +++++++++++++++++++ .../task/TaskSchedulingAutoConfiguration.java | 5 +- .../TaskSchedulingAutoConfigurationTests.java | 11 ---- 5 files changed, 130 insertions(+), 14 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/scheduling/ScheduledTasksObservabilityAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/scheduling/ScheduledTasksObservabilityAutoConfigurationTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/scheduling/ScheduledTasksObservabilityAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/scheduling/ScheduledTasksObservabilityAutoConfiguration.java new file mode 100644 index 000000000000..a4014d2d3eb5 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/scheduling/ScheduledTasksObservabilityAutoConfiguration.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.scheduling; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; + +/** + * {@link EnableAutoConfiguration Auto-configuration} to enable observability for + * scheduled tasks. + * + * @author Moritz Halbritter + * @since 3.2.0 + */ +@AutoConfiguration(after = ObservationAutoConfiguration.class) +@ConditionalOnBean(ObservationRegistry.class) +@ConditionalOnClass(ThreadPoolTaskScheduler.class) +public class ScheduledTasksObservabilityAutoConfiguration { + + @Bean + ObservabilitySchedulingConfigurer observabilitySchedulingConfigurer(ObservationRegistry observationRegistry) { + return new ObservabilitySchedulingConfigurer(observationRegistry); + } + + static final class ObservabilitySchedulingConfigurer implements SchedulingConfigurer { + + private final ObservationRegistry observationRegistry; + + ObservabilitySchedulingConfigurer(ObservationRegistry observationRegistry) { + this.observationRegistry = observationRegistry; + } + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + taskRegistrar.setObservationRegistry(this.observationRegistry); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index affbe7607f9e..f2b80c4ffb40 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -93,6 +93,7 @@ org.springframework.boot.actuate.autoconfigure.r2dbc.ConnectionFactoryHealthCont org.springframework.boot.actuate.autoconfigure.data.redis.RedisHealthContributorAutoConfiguration org.springframework.boot.actuate.autoconfigure.data.redis.RedisReactiveHealthContributorAutoConfiguration org.springframework.boot.actuate.autoconfigure.scheduling.ScheduledTasksEndpointAutoConfiguration +org.springframework.boot.actuate.autoconfigure.scheduling.ScheduledTasksObservabilityAutoConfiguration org.springframework.boot.actuate.autoconfigure.security.reactive.ReactiveManagementWebSecurityAutoConfiguration org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration org.springframework.boot.actuate.autoconfigure.session.SessionsEndpointAutoConfiguration diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/scheduling/ScheduledTasksObservabilityAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/scheduling/ScheduledTasksObservabilityAutoConfigurationTests.java new file mode 100644 index 000000000000..60ef5d14c5cc --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/scheduling/ScheduledTasksObservabilityAutoConfigurationTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.scheduling; + +import java.util.List; + +import io.micrometer.observation.ObservationRegistry; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.scheduling.ScheduledTasksObservabilityAutoConfiguration.ObservabilitySchedulingConfigurer; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.annotation.ImportCandidates; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ScheduledTasksObservabilityAutoConfiguration}. + * + * @author Moritz Halbritter + */ +class ScheduledTasksObservabilityAutoConfigurationTests { + + private final ApplicationContextRunner runner = new ApplicationContextRunner().withConfiguration(AutoConfigurations + .of(ObservationAutoConfiguration.class, ScheduledTasksObservabilityAutoConfiguration.class)); + + @Test + void shouldProvideObservabilitySchedulingConfigurer() { + this.runner.run((context) -> assertThat(context).hasSingleBean(ObservabilitySchedulingConfigurer.class)); + } + + @Test + void observabilitySchedulingConfigurerShouldConfigureObservationRegistry() { + ObservationRegistry observationRegistry = ObservationRegistry.create(); + ObservabilitySchedulingConfigurer configurer = new ObservabilitySchedulingConfigurer(observationRegistry); + ScheduledTaskRegistrar registrar = new ScheduledTaskRegistrar(); + configurer.configureTasks(registrar); + assertThat(registrar.getObservationRegistry()).isEqualTo(observationRegistry); + } + + @Test + void isRegisteredInAutoConfigurationsFile() { + List configurations = ImportCandidates.load(AutoConfiguration.class, null).getCandidates(); + assertThat(configurations).contains(ScheduledTasksObservabilityAutoConfiguration.class.getName()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java index a5dd93bf4f44..d7969608de36 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -31,7 +31,6 @@ import org.springframework.boot.task.TaskSchedulerCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.scheduling.config.TaskManagementConfigUtils; @@ -48,7 +47,7 @@ public class TaskSchedulingAutoConfiguration { @Bean @ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME) - @ConditionalOnMissingBean({ SchedulingConfigurer.class, TaskScheduler.class, ScheduledExecutorService.class }) + @ConditionalOnMissingBean({ TaskScheduler.class, ScheduledExecutorService.class }) public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder builder) { return builder.build(); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java index 990e8cb6dbe8..8d42ccd05171 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java @@ -122,17 +122,6 @@ void enableSchedulingWithExistingScheduledExecutorServiceBacksOff() { }); } - @Test - void enableSchedulingWithConfigurerBacksOff() { - this.contextRunner.withUserConfiguration(SchedulingConfiguration.class, SchedulingConfigurerConfiguration.class) - .run((context) -> { - assertThat(context).doesNotHaveBean(TaskScheduler.class); - TestBean bean = context.getBean(TestBean.class); - assertThat(bean.latch.await(30, TimeUnit.SECONDS)).isTrue(); - assertThat(bean.threadNames).containsExactly("test-1"); - }); - } - @Test void enableSchedulingWithLazyInitializationInvokeScheduledMethods() { List threadNames = new ArrayList<>(); From 63121dd08a7af4203b8c563b1d0433d8eb2f8e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edd=C3=BA=20Mel=C3=A9ndez?= Date: Wed, 19 Apr 2023 10:34:19 -0600 Subject: [PATCH 0179/1215] Add service connection for Testcontainers ActiveMQ See gh-35080 --- .../activemq/ActiveMQAutoConfiguration.java | 36 ++++++++ .../activemq/ActiveMQConnectionDetails.java | 35 ++++++++ ...ctiveMQConnectionFactoryConfiguration.java | 22 +++-- .../ActiveMQConnectionFactoryFactory.java | 21 ++--- .../jms/activemq/ActiveMQProperties.java | 10 +++ ...iveMQXAConnectionFactoryConfiguration.java | 13 +-- .../ActiveMQAutoConfigurationTests.java | 47 ++++++++++ .../jms/activemq/ActiveMQPropertiesTests.java | 8 +- .../src/docs/asciidoc/features/testing.adoc | 3 + .../spring-boot-testcontainers/build.gradle | 2 + ...veMQContainerConnectionDetailsFactory.java | 79 ++++++++++++++++ .../connection/activemq/package-info.java | 20 +++++ .../main/resources/META-INF/spring.factories | 1 + ...nectionDetailsFactoryIntegrationTests.java | 90 +++++++++++++++++++ .../build.gradle | 2 + .../activemq/SampleActiveMqTests.java | 17 ++-- 16 files changed, 367 insertions(+), 39 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionDetails.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/activemq/ActiveMQContainerConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/activemq/package-info.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/activemq/ActiveMQContainerConnectionDetailsFactoryIntegrationTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQAutoConfiguration.java index da642a7f8689..44c3cebe3082 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQAutoConfiguration.java @@ -27,6 +27,7 @@ import org.springframework.boot.autoconfigure.jms.JmsProperties; import org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; /** @@ -35,6 +36,7 @@ * * @author Stephane Nicoll * @author Phillip Webb + * @author Eddú Meléndez * @since 3.1.0 */ @AutoConfiguration(before = JmsAutoConfiguration.class, after = JndiConnectionFactoryAutoConfiguration.class) @@ -44,4 +46,38 @@ @Import({ ActiveMQXAConnectionFactoryConfiguration.class, ActiveMQConnectionFactoryConfiguration.class }) public class ActiveMQAutoConfiguration { + @Bean + @ConditionalOnMissingBean(ActiveMQConnectionDetails.class) + ActiveMQConnectionDetails activemqConnectionDetails(ActiveMQProperties properties) { + return new PropertiesActiveMQConnectionDetails(properties); + } + + /** + * Adapts {@link ActiveMQProperties} to {@link ActiveMQConnectionDetails}. + */ + static class PropertiesActiveMQConnectionDetails implements ActiveMQConnectionDetails { + + private final ActiveMQProperties properties; + + PropertiesActiveMQConnectionDetails(ActiveMQProperties properties) { + this.properties = properties; + } + + @Override + public String getBrokerUrl() { + return this.properties.determineBrokerUrl(); + } + + @Override + public String getUser() { + return this.properties.getUser(); + } + + @Override + public String getPassword() { + return this.properties.getPassword(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionDetails.java new file mode 100644 index 000000000000..b139215095f8 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionDetails.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2023 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.jms.activemq; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to an ActiveMQ service. + * + * @author Eddú Meléndez + * @since 3.1.0 + */ +public interface ActiveMQConnectionDetails extends ConnectionDetails { + + String getBrokerUrl(); + + String getUser(); + + String getPassword(); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionFactoryConfiguration.java index a55337e58a2c..a4d242600e51 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionFactoryConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionFactoryConfiguration.java @@ -39,6 +39,7 @@ * @author Phillip Webb * @author Andy Wilkinson * @author Aurélien Leboulanger + * @author Eddú Meléndez */ @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(ConnectionFactory.class) @@ -52,13 +53,16 @@ static class SimpleConnectionFactoryConfiguration { @Bean @ConditionalOnProperty(prefix = "spring.jms.cache", name = "enabled", havingValue = "false") ActiveMQConnectionFactory jmsConnectionFactory(ActiveMQProperties properties, - ObjectProvider factoryCustomizers) { - return createJmsConnectionFactory(properties, factoryCustomizers); + ObjectProvider factoryCustomizers, + ActiveMQConnectionDetails connectionDetails) { + return createJmsConnectionFactory(properties, factoryCustomizers, connectionDetails); } private static ActiveMQConnectionFactory createJmsConnectionFactory(ActiveMQProperties properties, - ObjectProvider factoryCustomizers) { - return new ActiveMQConnectionFactoryFactory(properties, factoryCustomizers.orderedStream().toList()) + ObjectProvider factoryCustomizers, + ActiveMQConnectionDetails connectionDetails) { + return new ActiveMQConnectionFactoryFactory(properties, factoryCustomizers.orderedStream().toList(), + connectionDetails) .createConnectionFactory(ActiveMQConnectionFactory.class); } @@ -70,10 +74,11 @@ static class CachingConnectionFactoryConfiguration { @Bean CachingConnectionFactory jmsConnectionFactory(JmsProperties jmsProperties, ActiveMQProperties properties, - ObjectProvider factoryCustomizers) { + ObjectProvider factoryCustomizers, + ActiveMQConnectionDetails connectionDetails) { JmsProperties.Cache cacheProperties = jmsProperties.getCache(); CachingConnectionFactory connectionFactory = new CachingConnectionFactory( - createJmsConnectionFactory(properties, factoryCustomizers)); + createJmsConnectionFactory(properties, factoryCustomizers, connectionDetails)); connectionFactory.setCacheConsumers(cacheProperties.isConsumers()); connectionFactory.setCacheProducers(cacheProperties.isProducers()); connectionFactory.setSessionCacheSize(cacheProperties.getSessionCacheSize()); @@ -91,9 +96,10 @@ static class PooledConnectionFactoryConfiguration { @Bean(destroyMethod = "stop") @ConditionalOnProperty(prefix = "spring.activemq.pool", name = "enabled", havingValue = "true") JmsPoolConnectionFactory jmsConnectionFactory(ActiveMQProperties properties, - ObjectProvider factoryCustomizers) { + ObjectProvider factoryCustomizers, + ActiveMQConnectionDetails connectionDetails) { ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactoryFactory(properties, - factoryCustomizers.orderedStream().toList()) + factoryCustomizers.orderedStream().toList(), connectionDetails) .createConnectionFactory(ActiveMQConnectionFactory.class); return new JmsPoolConnectionFactoryFactory(properties.getPool()) .createPooledConnectionFactory(connectionFactory); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionFactoryFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionFactoryFactory.java index b571860491f0..67768c0363ae 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionFactoryFactory.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionFactoryFactory.java @@ -32,20 +32,22 @@ * * @author Phillip Webb * @author Venil Noronha + * @author Eddú Meléndez */ class ActiveMQConnectionFactoryFactory { - private static final String DEFAULT_NETWORK_BROKER_URL = "tcp://localhost:61616"; - private final ActiveMQProperties properties; private final List factoryCustomizers; + private final ActiveMQConnectionDetails connectionDetails; + ActiveMQConnectionFactoryFactory(ActiveMQProperties properties, - List factoryCustomizers) { + List factoryCustomizers, ActiveMQConnectionDetails connectionDetails) { Assert.notNull(properties, "Properties must not be null"); this.properties = properties; this.factoryCustomizers = (factoryCustomizers != null) ? factoryCustomizers : Collections.emptyList(); + this.connectionDetails = connectionDetails; } T createConnectionFactory(Class factoryClass) { @@ -79,9 +81,9 @@ private T doCreateConnectionFactory(Class< private T createConnectionFactoryInstance(Class factoryClass) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { - String brokerUrl = determineBrokerUrl(); - String user = this.properties.getUser(); - String password = this.properties.getPassword(); + String brokerUrl = this.connectionDetails.getBrokerUrl(); + String user = this.connectionDetails.getUser(); + String password = this.connectionDetails.getPassword(); if (StringUtils.hasLength(user) && StringUtils.hasLength(password)) { return factoryClass.getConstructor(String.class, String.class, String.class) .newInstance(user, password, brokerUrl); @@ -95,11 +97,4 @@ private void customize(ActiveMQConnectionFactory connectionFactory) { } } - String determineBrokerUrl() { - if (this.properties.getBrokerUrl() != null) { - return this.properties.getBrokerUrl(); - } - return DEFAULT_NETWORK_BROKER_URL; - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQProperties.java index 48b72e88935c..2877479a08e6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQProperties.java @@ -31,11 +31,14 @@ * @author Stephane Nicoll * @author Aurélien Leboulanger * @author Venil Noronha + * @author Eddú Meléndez * @since 3.1.0 */ @ConfigurationProperties(prefix = "spring.activemq") public class ActiveMQProperties { + private static final String DEFAULT_NETWORK_BROKER_URL = "tcp://localhost:61616"; + /** * URL of the ActiveMQ broker. Auto-generated by default. */ @@ -128,6 +131,13 @@ public Packages getPackages() { return this.packages; } + String determineBrokerUrl() { + if (this.brokerUrl != null) { + return this.brokerUrl; + } + return DEFAULT_NETWORK_BROKER_URL; + } + public static class Packages { /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQXAConnectionFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQXAConnectionFactoryConfiguration.java index 4a7cbd214cea..6458c5824ae3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQXAConnectionFactoryConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQXAConnectionFactoryConfiguration.java @@ -36,6 +36,7 @@ * * @author Phillip Webb * @author Aurélien Leboulanger + * @author Eddú Meléndez */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(TransactionManager.class) @@ -46,10 +47,10 @@ class ActiveMQXAConnectionFactoryConfiguration { @Primary @Bean(name = { "jmsConnectionFactory", "xaJmsConnectionFactory" }) ConnectionFactory jmsConnectionFactory(ActiveMQProperties properties, - ObjectProvider factoryCustomizers, XAConnectionFactoryWrapper wrapper) - throws Exception { + ObjectProvider factoryCustomizers, XAConnectionFactoryWrapper wrapper, + ActiveMQConnectionDetails connectionDetails) throws Exception { ActiveMQXAConnectionFactory connectionFactory = new ActiveMQConnectionFactoryFactory(properties, - factoryCustomizers.orderedStream().toList()) + factoryCustomizers.orderedStream().toList(), connectionDetails) .createConnectionFactory(ActiveMQXAConnectionFactory.class); return wrapper.wrapConnectionFactory(connectionFactory); } @@ -58,8 +59,10 @@ ConnectionFactory jmsConnectionFactory(ActiveMQProperties properties, @ConditionalOnProperty(prefix = "spring.activemq.pool", name = "enabled", havingValue = "false", matchIfMissing = true) ActiveMQConnectionFactory nonXaJmsConnectionFactory(ActiveMQProperties properties, - ObjectProvider factoryCustomizers) { - return new ActiveMQConnectionFactoryFactory(properties, factoryCustomizers.orderedStream().toList()) + ObjectProvider factoryCustomizers, + ActiveMQConnectionDetails connectionDetails) { + return new ActiveMQConnectionFactoryFactory(properties, factoryCustomizers.orderedStream().toList(), + connectionDetails) .createConnectionFactory(ActiveMQConnectionFactory.class); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQAutoConfigurationTests.java index 0edd4270c7ee..3e1b0980d88f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQAutoConfigurationTests.java @@ -40,6 +40,7 @@ * @author Andy Wilkinson * @author Aurélien Leboulanger * @author Stephane Nicoll + * @author Eddú Meléndez */ class ActiveMQAutoConfigurationTests { @@ -233,6 +234,27 @@ void cachingConnectionFactoryNotOnTheClasspathAndCacheEnabledThenSimpleConnectio .doesNotHaveBean("jmsConnectionFactory")); } + @Test + void definesPropertiesBasedConnectionDetailsByDefault() { + this.contextRunner.run((context) -> assertThat(context) + .hasSingleBean(ActiveMQAutoConfiguration.PropertiesActiveMQConnectionDetails.class)); + } + + @Test + void testConnectionFactoryWithOverridesWhenUsingCustomConnectionDetails() { + this.contextRunner.withClassLoader(new FilteredClassLoader(CachingConnectionFactory.class)) + .withPropertyValues("spring.activemq.pool.enabled=false", "spring.jms.cache.enabled=false") + .withUserConfiguration(TestConnectionDetailsConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(ActiveMQConnectionDetails.class) + .doesNotHaveBean(ActiveMQAutoConfiguration.PropertiesActiveMQConnectionDetails.class); + ActiveMQConnectionFactory connectionFactory = context.getBean(ActiveMQConnectionFactory.class); + assertThat(connectionFactory.getBrokerURL()).isEqualTo("tcp://localhost:12345"); + assertThat(connectionFactory.getUserName()).isEqualTo("springuser"); + assertThat(connectionFactory.getPassword()).isEqualTo("spring"); + }); + } + @Configuration(proxyBeanMethods = false) static class EmptyConfiguration { @@ -261,4 +283,29 @@ ActiveMQConnectionFactoryCustomizer activeMQConnectionFactoryCustomizer() { } + @Configuration(proxyBeanMethods = false) + static class TestConnectionDetailsConfiguration { + + @Bean + ActiveMQConnectionDetails activemqConnectionDetails() { + return new ActiveMQConnectionDetails() { + @Override + public String getBrokerUrl() { + return "tcp://localhost:12345"; + } + + @Override + public String getUser() { + return "springuser"; + } + + @Override + public String getPassword() { + return "spring"; + } + }; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQPropertiesTests.java index d6b66bc12390..07d84e900e7a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQPropertiesTests.java @@ -29,6 +29,7 @@ * @author Stephane Nicoll * @author Aurélien Leboulanger * @author Venil Noronha + * @author Eddú Meléndez */ class ActiveMQPropertiesTests { @@ -38,13 +39,13 @@ class ActiveMQPropertiesTests { @Test void getBrokerUrlIsLocalhostByDefault() { - assertThat(createFactory(this.properties).determineBrokerUrl()).isEqualTo(DEFAULT_NETWORK_BROKER_URL); + assertThat(this.properties.determineBrokerUrl()).isEqualTo(DEFAULT_NETWORK_BROKER_URL); } @Test void getBrokerUrlUseExplicitBrokerUrl() { this.properties.setBrokerUrl("tcp://activemq.example.com:71717"); - assertThat(createFactory(this.properties).determineBrokerUrl()).isEqualTo("tcp://activemq.example.com:71717"); + assertThat(this.properties.determineBrokerUrl()).isEqualTo("tcp://activemq.example.com:71717"); } @Test @@ -66,7 +67,8 @@ void setTrustedPackages() { } private ActiveMQConnectionFactoryFactory createFactory(ActiveMQProperties properties) { - return new ActiveMQConnectionFactoryFactory(properties, Collections.emptyList()); + return new ActiveMQConnectionFactoryFactory(properties, Collections.emptyList(), + new ActiveMQAutoConfiguration.PropertiesActiveMQConnectionDetails(properties)); } } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc index 436deb91f7dd..57ded5512638 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc @@ -946,6 +946,9 @@ The following service connection factories are provided in the `spring-boot-test |=== | Connection Details | Matched on +| `ActiveMQConnectionDetails` +| Containers named "symptoma/activemq" + | `CassandraConnectionDetails` | Containers of type `CassandraContainer` diff --git a/spring-boot-project/spring-boot-testcontainers/build.gradle b/spring-boot-project/spring-boot-testcontainers/build.gradle index 9cfb9709582e..2d20062409a8 100644 --- a/spring-boot-project/spring-boot-testcontainers/build.gradle +++ b/spring-boot-project/spring-boot-testcontainers/build.gradle @@ -36,6 +36,7 @@ dependencies { testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) testImplementation(project(":spring-boot-project:spring-boot-test")) testImplementation("ch.qos.logback:logback-classic") + testImplementation("org.apache.activemq:activemq-client-jakarta") testImplementation("org.assertj:assertj-core") testImplementation("org.awaitility:awaitility") testImplementation("org.influxdb:influxdb-java") @@ -45,6 +46,7 @@ dependencies { testImplementation("org.mockito:mockito-core") testImplementation("org.mockito:mockito-junit-jupiter") testImplementation("org.springframework:spring-core-test") + testImplementation("org.springframework:spring-jms") testImplementation("org.springframework:spring-r2dbc") testImplementation("org.springframework.amqp:spring-rabbit") testImplementation("org.springframework.kafka:spring-kafka") diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/activemq/ActiveMQContainerConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/activemq/ActiveMQContainerConnectionDetailsFactory.java new file mode 100644 index 000000000000..a81469645a30 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/activemq/ActiveMQContainerConnectionDetailsFactory.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2023 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.testcontainers.service.connection.activemq; + +import java.util.Map; + +import org.testcontainers.containers.Container; +import org.testcontainers.containers.GenericContainer; + +import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQConnectionDetails; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; + +/** + * {@link ContainerConnectionDetailsFactory} to create {@link ActiveMQConnectionDetails} + * from a {@link ServiceConnection @ServiceConnection}-annotated {@link GenericContainer} + * using the {@code "symptoma/activemq"} image. + * + * @author Eddú Meléndez + */ +class ActiveMQContainerConnectionDetailsFactory + extends ContainerConnectionDetailsFactory> { + + ActiveMQContainerConnectionDetailsFactory() { + super("symptoma/activemq"); + } + + @Override + protected ActiveMQConnectionDetails getContainerConnectionDetails(ContainerConnectionSource> source) { + return new ActiveMQContainerConnectionDetails(source); + } + + private static final class ActiveMQContainerConnectionDetails extends ContainerConnectionDetails + implements ActiveMQConnectionDetails { + + private final String brokerUrl; + + private final Map envVars; + + private ActiveMQContainerConnectionDetails(ContainerConnectionSource> source) { + super(source); + this.brokerUrl = "tcp://" + source.getContainer().getHost() + ":" + + source.getContainer().getFirstMappedPort(); + this.envVars = source.getContainer().getEnvMap(); + } + + @Override + public String getBrokerUrl() { + return this.brokerUrl; + } + + @Override + public String getUser() { + return this.envVars.get("ACTIVEMQ_USERNAME"); + } + + @Override + public String getPassword() { + return this.envVars.get("ACTIVEMQ_PASSWORD"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/activemq/package-info.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/activemq/package-info.java new file mode 100644 index 000000000000..0981f1bf9b21 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/activemq/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Support for testcontainers ActiveMQ service connections. + */ +package org.springframework.boot.testcontainers.service.connection.activemq; diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories index 2cfe37359c38..f26cc7230f68 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories @@ -8,6 +8,7 @@ org.springframework.boot.testcontainers.service.connection.ServiceConnectionCont # Connection Details Factories org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ +org.springframework.boot.testcontainers.service.connection.activemq.ActiveMQContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.amqp.RabbitContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.cassandra.CassandraContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.couchbase.CouchbaseContainerConnectionDetailsFactory,\ diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/activemq/ActiveMQContainerConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/activemq/ActiveMQContainerConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..647b4861d086 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/activemq/ActiveMQContainerConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2023 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.testcontainers.service.connection.activemq; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration; +import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.testcontainers.ActiveMQContainer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.jms.core.JmsMessagingTemplate; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ActiveMQContainerConnectionDetailsFactory}. + * + * @author Eddú Meléndez + */ +@SpringJUnitConfig +@Testcontainers(disabledWithoutDocker = true) +class ActiveMQContainerConnectionDetailsFactoryIntegrationTests { + + @Container + @ServiceConnection + static final ActiveMQContainer activemq = new ActiveMQContainer(); + + @Autowired + private JmsMessagingTemplate jmsTemplate; + + @Autowired + private TestListener listener; + + @Test + void connectionCanBeMadeToActiveMQContainer() { + this.jmsTemplate.convertAndSend("sample.queue", "message"); + Awaitility.waitAtMost(Duration.ofMinutes(1)) + .untilAsserted(() -> assertThat(this.listener.messages).containsExactly("message")); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration({ ActiveMQAutoConfiguration.class, JmsAutoConfiguration.class }) + static class TestConfiguration { + + @Bean + TestListener testListener() { + return new TestListener(); + } + + } + + static class TestListener { + + private final List messages = new ArrayList<>(); + + @JmsListener(destination = "sample.queue") + void processMessage(String message) { + this.messages.add(message); + } + + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-activemq/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-activemq/build.gradle index 5ad092f62cb1..f250b84c075d 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-activemq/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-activemq/build.gradle @@ -8,6 +8,8 @@ description = "Spring Boot Actuator ActiveMQ smoke test" dependencies { implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-activemq")) + testImplementation("org.awaitility:awaitility") + testImplementation("org.testcontainers:testcontainers") testImplementation("org.testcontainers:junit-jupiter") testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) testImplementation(project(":spring-boot-project:spring-boot-testcontainers")) diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-activemq/src/test/java/smoketest/activemq/SampleActiveMqTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-activemq/src/test/java/smoketest/activemq/SampleActiveMqTests.java index 7637a28f8869..9d68eda78d2b 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-activemq/src/test/java/smoketest/activemq/SampleActiveMqTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-activemq/src/test/java/smoketest/activemq/SampleActiveMqTests.java @@ -16,6 +16,9 @@ package smoketest.activemq; +import java.time.Duration; + +import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.testcontainers.junit.jupiter.Container; @@ -25,9 +28,8 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.testcontainers.ActiveMQContainer; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; import static org.assertj.core.api.Assertions.assertThat; @@ -43,21 +45,16 @@ class SampleActiveMqTests { @Container + @ServiceConnection private static final ActiveMQContainer container = new ActiveMQContainer(); - @DynamicPropertySource - static void activeMqProperties(DynamicPropertyRegistry registry) { - registry.add("spring.activemq.broker-url", container::getBrokerUrl); - } - @Autowired private Producer producer; @Test - void sendSimpleMessage(CapturedOutput output) throws InterruptedException { + void sendSimpleMessage(CapturedOutput output) { this.producer.send("Test message"); - Thread.sleep(1000L); - assertThat(output).contains("Test message"); + Awaitility.waitAtMost(Duration.ofMinutes(1)).untilAsserted(() -> assertThat(output).contains("Test message")); } } From 311fa6272dd3f2ee63358f4ab09db3cddf9c2181 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 17 Jul 2023 16:51:46 +0200 Subject: [PATCH 0180/1215] Polish "Add service connection for Testcontainers ActiveMQ" This also adds support for Docker Compose. See gh-35080 --- .../activemq/ActiveMQConnectionDetails.java | 15 +++- ...DockerComposeConnectionDetailsFactory.java | 79 +++++++++++++++++++ .../activemq/ActiveMQEnvironment.java | 45 +++++++++++ .../connection/activemq/package-info.java | 20 +++++ .../main/resources/META-INF/spring.factories | 1 + ...nectionDetailsFactoryIntegrationTests.java | 45 +++++++++++ .../activemq/ActiveMQEnvironmentTests.java | 57 +++++++++++++ .../connection/activemq/activemq-compose.yaml | 8 ++ .../asciidoc/features/docker-compose.adoc | 3 + ...veMQContainerConnectionDetailsFactory.java | 19 ++--- .../build.gradle | 1 - 11 files changed, 277 insertions(+), 16 deletions(-) create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQEnvironment.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/package-info.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQEnvironmentTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/activemq/activemq-compose.yaml diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionDetails.java index b139215095f8..9c095cfda901 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionDetails.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQConnectionDetails.java @@ -22,14 +22,27 @@ * Details required to establish a connection to an ActiveMQ service. * * @author Eddú Meléndez - * @since 3.1.0 + * @author Stephane Nicoll + * @since 3.2.0 */ public interface ActiveMQConnectionDetails extends ConnectionDetails { + /** + * Broker URL to use. + * @return the url of the broker + */ String getBrokerUrl(); + /** + * Login user to authenticate to the broker. + * @return the login user to authenticate to the broker or {@code null} + */ String getUser(); + /** + * Login to authenticate against the broker. + * @return the login to authenticate against the broker or {@code null} + */ String getPassword(); } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQDockerComposeConnectionDetailsFactory.java new file mode 100644 index 000000000000..ac3809d8da21 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.activemq; + +import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails; +import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create + * {@link ActiveMQConnectionDetails} for an {@code activemq} service. + * + * @author Stephane Nicoll + */ +class ActiveMQDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + private static final int ACTIVEMQ_PORT = 61616; + + protected ActiveMQDockerComposeConnectionDetailsFactory() { + super("symptoma/activemq"); + } + + @Override + protected ActiveMQConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new ActiveMQDockerComposeConnectionDetails(source.getRunningService()); + } + + /** + * {@link RabbitConnectionDetails} backed by a {@code rabbitmq} + * {@link RunningService}. + */ + static class ActiveMQDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements ActiveMQConnectionDetails { + + private final ActiveMQEnvironment environment; + + private final String brokerUrl; + + protected ActiveMQDockerComposeConnectionDetails(RunningService service) { + super(service); + this.environment = new ActiveMQEnvironment(service.env()); + this.brokerUrl = "tcp://" + service.host() + ":" + service.ports().get(ACTIVEMQ_PORT); + } + + @Override + public String getBrokerUrl() { + return this.brokerUrl; + } + + @Override + public String getUser() { + return this.environment.getUser(); + } + + @Override + public String getPassword() { + return this.environment.getPassword(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQEnvironment.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQEnvironment.java new file mode 100644 index 000000000000..742389e80a7e --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQEnvironment.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.activemq; + +import java.util.Map; + +/** + * ActiveMQ environment details. + * + * @author Stephane Nicoll + */ +class ActiveMQEnvironment { + + private final String user; + + private final String password; + + ActiveMQEnvironment(Map env) { + this.user = env.get("ACTIVEMQ_USERNAME"); + this.password = env.get("ACTIVEMQ_PASSWORD"); + } + + String getUser() { + return this.user; + } + + String getPassword() { + return this.password; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/package-info.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/package-info.java new file mode 100644 index 000000000000..5cb2e75cf5b4 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Auto-configuration for docker compose ActiveMQ service connections. + */ +package org.springframework.boot.docker.compose.service.connection.activemq; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories index 7c6623fbe5c9..cd5dc75bb4cb 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories @@ -5,6 +5,7 @@ org.springframework.boot.docker.compose.service.connection.DockerComposeServiceC # Connection Details Factories org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ +org.springframework.boot.docker.compose.service.connection.activemq.ActiveMQDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.cassandra.CassandraDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.elasticsearch.ElasticsearchDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.flyway.JdbcAdaptingFlywayConnectionDetailsFactory,\ diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..82f26133aa9f --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.activemq; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link ActiveMQDockerComposeConnectionDetailsFactory}. + * + * @author Stephane Nicoll + */ +class ActiveMQDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { + + ActiveMQDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("activemq-compose.yaml"); + } + + @Test + void runCreatesConnectionDetails() { + ActiveMQConnectionDetails connectionDetails = run(ActiveMQConnectionDetails.class); + assertThat(connectionDetails.getBrokerUrl()).isNotNull().startsWith("tcp://"); + assertThat(connectionDetails.getUser()).isEqualTo("root"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQEnvironmentTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQEnvironmentTests.java new file mode 100644 index 000000000000..04ee5929788f --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQEnvironmentTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.activemq; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ActiveMQEnvironment}. + * + * @author Stephane Nicoll + */ +class ActiveMQEnvironmentTests { + + @Test + void getUserWhenHasNoActiveMqUser() { + ActiveMQEnvironment environment = new ActiveMQEnvironment(Collections.emptyMap()); + assertThat(environment.getUser()).isNull(); + } + + @Test + void getUserWhenHasActiveMqUser() { + ActiveMQEnvironment environment = new ActiveMQEnvironment(Map.of("ACTIVEMQ_USERNAME", "me")); + assertThat(environment.getUser()).isEqualTo("me"); + } + + @Test + void getPasswordWhenHasNoActiveMqPassword() { + ActiveMQEnvironment environment = new ActiveMQEnvironment(Collections.emptyMap()); + assertThat(environment.getPassword()).isNull(); + } + + @Test + void getPasswordWhenHasActiveMqPassword() { + ActiveMQEnvironment environment = new ActiveMQEnvironment(Map.of("ACTIVEMQ_PASSWORD", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/activemq/activemq-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/activemq/activemq-compose.yaml new file mode 100644 index 000000000000..7c005eb6dafc --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/activemq/activemq-compose.yaml @@ -0,0 +1,8 @@ +services: + activemq: + image: 'symptoma/activemq:5.18.0' + ports: + - '61616' + environment: + ACTIVEMQ_USERNAME: 'root' + ACTIVEMQ_PASSWORD: 'secret' diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc index 329bfe408e30..cd8ed4ad45f2 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc @@ -58,6 +58,9 @@ The following service connections are currently supported: |=== | Connection Details | Matched on +| `ActiveMQConnectionDetails` +| Containers named "symptoma/activemq" + | `CassandraConnectionDetails` | Containers named "cassandra" diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/activemq/ActiveMQContainerConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/activemq/ActiveMQContainerConnectionDetailsFactory.java index a81469645a30..82324da487ee 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/activemq/ActiveMQContainerConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/activemq/ActiveMQContainerConnectionDetailsFactory.java @@ -16,8 +16,6 @@ package org.springframework.boot.testcontainers.service.connection.activemq; -import java.util.Map; - import org.testcontainers.containers.Container; import org.testcontainers.containers.GenericContainer; @@ -34,7 +32,7 @@ * @author Eddú Meléndez */ class ActiveMQContainerConnectionDetailsFactory - extends ContainerConnectionDetailsFactory> { + extends ContainerConnectionDetailsFactory, ActiveMQConnectionDetails> { ActiveMQContainerConnectionDetailsFactory() { super("symptoma/activemq"); @@ -45,33 +43,26 @@ protected ActiveMQConnectionDetails getContainerConnectionDetails(ContainerConne return new ActiveMQContainerConnectionDetails(source); } - private static final class ActiveMQContainerConnectionDetails extends ContainerConnectionDetails + private static final class ActiveMQContainerConnectionDetails extends ContainerConnectionDetails> implements ActiveMQConnectionDetails { - private final String brokerUrl; - - private final Map envVars; - private ActiveMQContainerConnectionDetails(ContainerConnectionSource> source) { super(source); - this.brokerUrl = "tcp://" + source.getContainer().getHost() + ":" - + source.getContainer().getFirstMappedPort(); - this.envVars = source.getContainer().getEnvMap(); } @Override public String getBrokerUrl() { - return this.brokerUrl; + return "tcp://" + getContainer().getHost() + ":" + getContainer().getFirstMappedPort(); } @Override public String getUser() { - return this.envVars.get("ACTIVEMQ_USERNAME"); + return getContainer().getEnvMap().get("ACTIVEMQ_USERNAME"); } @Override public String getPassword() { - return this.envVars.get("ACTIVEMQ_PASSWORD"); + return getContainer().getEnvMap().get("ACTIVEMQ_PASSWORD"); } } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-activemq/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-activemq/build.gradle index f250b84c075d..963f63814ac7 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-activemq/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-activemq/build.gradle @@ -9,7 +9,6 @@ dependencies { implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-activemq")) testImplementation("org.awaitility:awaitility") - testImplementation("org.testcontainers:testcontainers") testImplementation("org.testcontainers:junit-jupiter") testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) testImplementation(project(":spring-boot-project:spring-boot-testcontainers")) From 5022e120c17ca832dc2b07e6059f7b431aeab7ca Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 20 Jul 2023 13:16:44 +0200 Subject: [PATCH 0181/1215] Configure bomr to filter GraphQL "snapshot" versions See gh-33817 --- spring-boot-project/spring-boot-dependencies/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 435e97be8a95..34856ae4a7a8 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -322,6 +322,10 @@ bom { } } library("GraphQL Java", "20.4") { + prohibit { + startsWith(["2018-", "2019-", "2020-", "2021-", "230521-"]) + because "These are snapshots that we don't want to see" + } group("com.graphql-java") { modules = [ "graphql-java" From 3affb3342e3ba6c37d62b23fd4f03fb76276c434 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 20 Jul 2023 12:25:01 +0100 Subject: [PATCH 0182/1215] Deprecate auto-configuration for InfluxDB Closes gh-35190 --- ...fluxDbHealthContributorAutoConfiguration.java | 7 ++++++- ...bHealthContributorAutoConfigurationTests.java | 2 ++ .../actuate/influx/InfluxDbHealthIndicator.java | 6 +++++- .../influx/InfluxDbHealthIndicatorTests.java | 2 ++ .../influx/InfluxDbAutoConfiguration.java | 5 +++++ .../autoconfigure/influx/InfluxDbCustomizer.java | 6 +++++- .../InfluxDbOkHttpClientBuilderProvider.java | 6 +++++- .../autoconfigure/influx/InfluxDbProperties.java | 10 +++++++++- .../influx/InfluxDbAutoConfigurationTests.java | 3 +++ .../src/docs/asciidoc/data/nosql.adoc | 16 ++++++---------- 10 files changed, 48 insertions(+), 15 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/influx/InfluxDbHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/influx/InfluxDbHealthContributorAutoConfiguration.java index 7f93279fde82..2a9b13603f90 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/influx/InfluxDbHealthContributorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/influx/InfluxDbHealthContributorAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -37,11 +37,16 @@ * * @author Eddú Meléndez * @since 2.0.0 + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of the + * new client and its own + * Spring Boot integration. */ +@SuppressWarnings("removal") @AutoConfiguration(after = InfluxDbAutoConfiguration.class) @ConditionalOnClass(InfluxDB.class) @ConditionalOnBean(InfluxDB.class) @ConditionalOnEnabledHealthIndicator("influxdb") +@Deprecated(since = "3.2.0", forRemoval = true) public class InfluxDbHealthContributorAutoConfiguration extends CompositeHealthContributorConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/influx/InfluxDbHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/influx/InfluxDbHealthContributorAutoConfigurationTests.java index 4ed923d5041f..64d2757f26dd 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/influx/InfluxDbHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/influx/InfluxDbHealthContributorAutoConfigurationTests.java @@ -32,6 +32,8 @@ * * @author Eddú Meléndez */ +@SuppressWarnings("removal") +@Deprecated(since = "3.2.0", forRemoval = true) class InfluxDbHealthContributorAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/influx/InfluxDbHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/influx/InfluxDbHealthIndicator.java index 4896ff6094e2..f58586fb925c 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/influx/InfluxDbHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/influx/InfluxDbHealthIndicator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. @@ -29,7 +29,11 @@ * * @author Eddú Meléndez * @since 2.0.0 + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of the + * new client and its own + * Spring Boot integration. */ +@Deprecated(since = "3.2.0", forRemoval = true) public class InfluxDbHealthIndicator extends AbstractHealthIndicator { private final InfluxDB influxDb; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/influx/InfluxDbHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/influx/InfluxDbHealthIndicatorTests.java index 367b1ec99113..f874582108fd 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/influx/InfluxDbHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/influx/InfluxDbHealthIndicatorTests.java @@ -36,6 +36,8 @@ * * @author Eddú Meléndez */ +@SuppressWarnings("removal") +@Deprecated(since = "3.2.0", forRemoval = true) class InfluxDbHealthIndicatorTests { @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfiguration.java index 5641d5182184..904541755a4b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfiguration.java @@ -39,11 +39,16 @@ * @author Andy Wilkinson * @author Phillip Webb * @since 2.0.0 + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of the + * new client and its own + * Spring Boot integration. */ @AutoConfiguration @ConditionalOnClass(InfluxDB.class) @EnableConfigurationProperties(InfluxDbProperties.class) @ConditionalOnProperty("spring.influx.url") +@Deprecated(since = "3.2.0", forRemoval = true) +@SuppressWarnings("removal") public class InfluxDbAutoConfiguration { @Bean diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbCustomizer.java index 9e46dd17fa3e..62f9b0df9998 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -24,8 +24,12 @@ * * @author Eddú Meléndez * @since 2.5.0 + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of the + * new client and its own + * Spring Boot integration. */ @FunctionalInterface +@Deprecated(since = "3.2.0", forRemoval = true) public interface InfluxDbCustomizer { /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbOkHttpClientBuilderProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbOkHttpClientBuilderProvider.java index 67dc383089de..14995dba425f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbOkHttpClientBuilderProvider.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbOkHttpClientBuilderProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. @@ -27,8 +27,12 @@ * * @author Stephane Nicoll * @since 2.1.0 + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of the + * new client and its own + * Spring Boot integration. */ @FunctionalInterface +@Deprecated(since = "3.2.0", forRemoval = true) public interface InfluxDbOkHttpClientBuilderProvider extends Supplier { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbProperties.java index d8a4c07d5b68..e4fd9d26b18d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.influx; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; /** * Configuration properties for InfluxDB. @@ -24,7 +25,11 @@ * @author Sergey Kuptsov * @author Stephane Nicoll * @since 2.0.0 + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of the + * new InfluxDB Java + * client and its own Spring Boot integration. */ +@Deprecated(since = "3.2.0", forRemoval = true) @ConfigurationProperties(prefix = "spring.influx") public class InfluxDbProperties { @@ -43,6 +48,7 @@ public class InfluxDbProperties { */ private String password; + @DeprecatedConfigurationProperty(reason = "the new InfluxDb Java client provides Spring Boot integration") public String getUrl() { return this.url; } @@ -51,6 +57,7 @@ public void setUrl(String url) { this.url = url; } + @DeprecatedConfigurationProperty(reason = "the new InfluxDb Java client provides Spring Boot integration") public String getUser() { return this.user; } @@ -59,6 +66,7 @@ public void setUser(String user) { this.user = user; } + @DeprecatedConfigurationProperty(reason = "the new InfluxDb Java client provides Spring Boot integration") public String getPassword() { return this.password; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfigurationTests.java index 7d37ee994cc4..fade2e3b2816 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/influx/InfluxDbAutoConfigurationTests.java @@ -42,6 +42,8 @@ * @author Andy Wilkinson * @author Phillip Webb */ +@SuppressWarnings("removal") +@Deprecated(since = "3.2.0", forRemoval = true) class InfluxDbAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() @@ -102,6 +104,7 @@ private int getReadTimeoutProperty(AssertableApplicationContext context) { static class CustomOkHttpClientBuilderProviderConfig { @Bean + @SuppressWarnings("removal") InfluxDbOkHttpClientBuilderProvider influxDbOkHttpClientBuilderProvider() { return () -> new OkHttpClient.Builder().readTimeout(40, TimeUnit.SECONDS); } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc index be74814f2651..2ffbe9da428a 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc @@ -11,7 +11,8 @@ Spring Data provides additional projects that help you access a variety of NoSQL * {spring-data-couchbase}[Couchbase] * {spring-data-ldap}[LDAP] -Spring Boot provides auto-configuration for Redis, MongoDB, Neo4j, Elasticsearch, Cassandra, Couchbase, LDAP and InfluxDB. +Spring Boot provides auto-configuration for Redis, MongoDB, Neo4j, Elasticsearch, Cassandra, Couchbase, LDAP. +Auto-configuration for InfluxDB is also provided but it is deprecated in favor of https://github.com/influxdata/influxdb-client-java[the new InfluxDB Java client] that provides its own Spring Boot integration. Additionally, {spring-boot-for-apache-geode}[Spring Boot for Apache Geode] provides {spring-boot-for-apache-geode-docs}#geode-repositories[auto-configuration for Apache Geode]. You can make use of the other projects, but you must configure them yourself. See the appropriate reference documentation at {spring-data}. @@ -637,22 +638,17 @@ If you have custom attributes, you can use configprop:spring.ldap.embedded.valid [[data.nosql.influxdb]] === InfluxDB +WARNING: Auto-configuration for InfluxDB is deprecated and scheduled for removal in Spring Boot 3.4 in favor of https://github.com/influxdata/influxdb-client-java[the new InfluxDB Java client] that provides its own Spring Boot integration. + https://www.influxdata.com/[InfluxDB] is an open-source time series database optimized for fast, high-availability storage and retrieval of time series data in fields such as operations monitoring, application metrics, Internet-of-Things sensor data, and real-time analytics. [[data.nosql.influxdb.connecting]] ==== Connecting to InfluxDB -Spring Boot auto-configures an `InfluxDB` instance, provided the `influxdb-java` client is on the classpath and the URL of the database is set, as shown in the following example: - -[source,yaml,indent=0,subs="verbatim",configprops,configblocks] ----- - spring: - influx: - url: "https://172.0.0.1:8086" ----- +Spring Boot auto-configures an `InfluxDB` instance, provided the `influxdb-java` client is on the classpath and the URL of the database is set using configprop:spring.influx.url[deprecated]. -If the connection to InfluxDB requires a user and password, you can set the `spring.influx.user` and `spring.influx.password` properties accordingly. +If the connection to InfluxDB requires a user and password, you can set the configprop:spring.influx.user[deprecated] and configprop:spring.influx.password[deprecated] properties accordingly. InfluxDB relies on OkHttp. If you need to tune the http client `InfluxDB` uses behind the scenes, you can register an `InfluxDbOkHttpClientBuilderProvider` bean. From 343c9c6f7ec03ecaa400329c44bbff6dd9e606a2 Mon Sep 17 00:00:00 2001 From: Christoph Dreis Date: Thu, 18 May 2023 17:20:08 +0200 Subject: [PATCH 0183/1215] Remove references to Atomikos and Bitronix See gh-35562 --- ...itional-spring-configuration-metadata.json | 161 ------ .../docs/asciidoc/anchor-rewrite.properties | 1 - ...itional-spring-configuration-metadata.json | 479 ------------------ 3 files changed, 641 deletions(-) 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 be821293afaa..8bfa50b648c1 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 @@ -1584,167 +1584,6 @@ "name": "spring.jpa.open-in-view", "defaultValue": true }, - { - "name": "spring.jta.bitronix.properties.allow-multiple-lrc", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.properties.asynchronous2-pc", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.properties.background-recovery-interval", - "type": "java.lang.Integer", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.properties.background-recovery-interval-seconds", - "type": "java.lang.Integer", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.properties.current-node-only-recovery", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.properties.debug-zero-resource-transaction", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.properties.default-transaction-timeout", - "type": "java.lang.Integer", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.properties.disable-jmx", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.properties.exception-analyzer", - "type": "java.lang.String", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.properties.filter-log-status", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.properties.force-batching-enabled", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.properties.forced-write-enabled", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.properties.graceful-shutdown-interval", - "type": "java.lang.String", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.properties.jndi-transaction-synchronization-registry-name", - "type": "java.lang.String", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.properties.jndi-user-transaction-name", - "type": "java.lang.String", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.properties.journal", - "type": "java.lang.String", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.properties.log-part1-filename", - "type": "java.lang.String", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.properties.log-part2-filename", - "type": "java.lang.String", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.properties.max-log-size-in-mb", - "type": "java.lang.Integer", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.properties.resource-configuration-filename", - "type": "java.lang.String", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.properties.server-id", - "type": "java.lang.String", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.properties.skip-corrupted-logs", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.properties.warn-about-zero-resource-transaction", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, { "name": "spring.jta.enabled", "type": "java.lang.Boolean", diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties index 44164b239812..fce3906da371 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties @@ -306,7 +306,6 @@ boot-features-webclient-customization=features.webclient.customization boot-features-validation=features.validation boot-features-email=features.email boot-features-jta=features.jta -boot-features-jta-atomikos=features.jta.atomikos boot-features-jta-javaee=features.jta.javaee boot-features-jta-mixed-jms=features.jta.mixing-xa-and-non-xa-connections boot-features-jta-supporting-alternative-embedded=features.jta.supporting-alternative-embedded-transaction-manager diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json index d4bd08721b86..5bc1a19d991f 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -361,485 +361,6 @@ "description": "Whether to defer DataSource initialization until after any EntityManagerFactory beans have been created and initialized.", "defaultValue": false }, - { - "name": "spring.jta.atomikos.connectionfactory.borrow-connection-timeout", - "type": "java.lang.Integer", - "description": "Timeout, in seconds, for borrowing connections from the pool.", - "defaultValue": 30 - }, - { - "name": "spring.jta.atomikos.connectionfactory.ignore-session-transacted-flag", - "type": "java.lang.Boolean", - "description": "Whether to ignore the transacted flag when creating session.", - "defaultValue": true - }, - { - "name": "spring.jta.atomikos.connectionfactory.local-transaction-mode", - "type": "java.lang.Boolean", - "description": "Whether local transactions are desired.", - "defaultValue": false - }, - { - "name": "spring.jta.atomikos.connectionfactory.maintenance-interval", - "type": "java.lang.Integer", - "description": "Time, in seconds, between runs of the pool's maintenance thread.", - "defaultValue": 60 - }, - { - "name": "spring.jta.atomikos.connectionfactory.max-idle-time", - "type": "java.lang.Integer", - "description": "Time, in seconds, after which connections are cleaned up from the pool.", - "defaultValue": 60 - }, - { - "name": "spring.jta.atomikos.connectionfactory.max-lifetime", - "type": "java.lang.Integer", - "description": "Time, in seconds, that a connection can be pooled for before being destroyed. 0 denotes no limit.", - "defaultValue": 0 - }, - { - "name": "spring.jta.atomikos.connectionfactory.max-pool-size", - "type": "java.lang.Integer", - "description": "Maximum size of the pool.", - "defaultValue": 1 - }, - { - "name": "spring.jta.atomikos.connectionfactory.min-pool-size", - "type": "java.lang.Integer", - "description": "Minimum size of the pool.", - "defaultValue": 1 - }, - { - "name": "spring.jta.atomikos.connectionfactory.reap-timeout", - "type": "java.lang.Integer", - "description": "Reap timeout, in seconds, for borrowed connections. 0 denotes no limit.", - "defaultValue": 0 - }, - { - "name": "spring.jta.atomikos.connectionfactory.unique-resource-name", - "type": "java.lang.String", - "description": "Unique name used to identify the resource during recovery.", - "defaultValue": "jmsConnectionFactory" - }, - { - "name": "spring.jta.atomikos.connectionfactory.xa-connection-factory-class-name", - "type": "java.lang.String", - "description": "Vendor-specific implementation of XAConnectionFactory." - }, - { - "name": "spring.jta.atomikos.connectionfactory.xa-properties", - "type": "java.util.Properties", - "description": "Vendor-specific XA properties." - }, - { - "name": "spring.jta.atomikos.datasource.borrow-connection-timeout", - "type": "java.lang.Integer", - "description": "Timeout, in seconds, for borrowing connections from the pool.", - "defaultValue": 30 - }, - { - "name": "spring.jta.atomikos.datasource.concurrent-connection-validation", - "type": "java.lang.Boolean", - "description": "Whether to use concurrent connection validation.", - "defaultValue": true - }, - { - "name": "spring.jta.atomikos.datasource.default-isolation-level", - "type": "java.lang.Integer", - "description": "Default isolation level of connections provided by the pool." - }, - { - "name": "spring.jta.atomikos.datasource.login-timeout", - "type": "java.lang.Integer", - "description": "Timeout, in seconds, for establishing a database connection.", - "defaultValue": 0 - }, - { - "name": "spring.jta.atomikos.datasource.maintenance-interval", - "type": "java.lang.Integer", - "description": "Time, in seconds, between runs of the pool's maintenance thread.", - "defaultValue": 60 - }, - { - "name": "spring.jta.atomikos.datasource.max-idle-time", - "type": "java.lang.Integer", - "description": "Time, in seconds, after which connections are cleaned up from the pool.", - "defaultValue": 60 - }, - { - "name": "spring.jta.atomikos.datasource.max-lifetime", - "type": "java.lang.Integer", - "description": "Time, in seconds, that a connection can be pooled for before being destroyed. 0 denotes no limit.", - "defaultValue": 0 - }, - { - "name": "spring.jta.atomikos.datasource.max-pool-size", - "type": "java.lang.Integer", - "description": "Maximum size of the pool.", - "defaultValue": 1 - }, - { - "name": "spring.jta.atomikos.datasource.min-pool-size", - "type": "java.lang.Integer", - "description": "Minimum size of the pool.", - "defaultValue": 1 - }, - { - "name": "spring.jta.atomikos.datasource.reap-timeout", - "type": "java.lang.Integer", - "description": "Reap timeout, in seconds, for borrowed connections. 0 denotes no limit.", - "defaultValue": 0 - }, - { - "name": "spring.jta.atomikos.datasource.test-query", - "type": "java.lang.String", - "description": "SQL query or statement used to validate a connection before returning it." - }, - { - "name": "spring.jta.atomikos.datasource.unique-resource-name", - "type": "java.lang.String", - "description": "Unique name used to identify the resource during recovery.", - "defaultValue": "dataSource" - }, - { - "name": "spring.jta.atomikos.datasource.xa-data-source-class-name", - "type": "java.lang.String", - "description": "Vendor-specific implementation of XAConnectionFactory." - }, - { - "name": "spring.jta.atomikos.datasource.xa-properties", - "type": "java.util.Properties", - "description": "Vendor-specific XA properties." - }, - { - "name": "spring.jta.bitronix.connectionfactory.acquire-increment", - "type": "java.lang.Integer", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.connectionfactory.acquisition-interval", - "type": "java.lang.Integer", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.connectionfactory.acquisition-timeout", - "type": "java.lang.Integer", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.connectionfactory.allow-local-transactions", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.connectionfactory.apply-transaction-timeout", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.connectionfactory.automatic-enlisting-enabled", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.connectionfactory.cache-producers-consumers", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.connectionfactory.class-name", - "type": "java.lang.String", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.connectionfactory.defer-connection-release", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.connectionfactory.disabled", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.connectionfactory.driver-properties", - "type": "java.util.Properties", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.connectionfactory.ignore-recovery-failures", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.connectionfactory.max-idle-time", - "type": "java.lang.Integer", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.connectionfactory.max-pool-size", - "type": "java.lang.Integer", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.connectionfactory.min-pool-size", - "type": "java.lang.Integer", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.connectionfactory.password", - "type": "java.lang.String", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.connectionfactory.share-transaction-connections", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.connectionfactory.test-connections", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.connectionfactory.two-pc-ordering-position", - "type": "java.lang.Integer", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.connectionfactory.unique-name", - "type": "java.lang.String", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.connectionfactory.use-tm-join", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.connectionfactory.user", - "type": "java.lang.String", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.acquire-increment", - "type": "java.lang.Integer", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.acquisition-interval", - "type": "java.lang.Integer", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.acquisition-timeout", - "type": "java.lang.Integer", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.allow-local-transactions", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.apply-transaction-timeout", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.automatic-enlisting-enabled", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.class-name", - "type": "java.lang.String", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.cursor-holdability", - "type": "java.lang.String", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.defer-connection-release", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.disabled", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.driver-properties", - "type": "java.util.Properties", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.enable-jdbc4-connection-test", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.ignore-recovery-failures", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.isolation-level", - "type": "java.lang.String", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.local-auto-commit", - "type": "java.lang.String", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.login-timeout", - "type": "java.lang.Integer", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.max-idle-time", - "type": "java.lang.Integer", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.max-pool-size", - "type": "java.lang.Integer", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.min-pool-size", - "type": "java.lang.Integer", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.prepared-statement-cache-size", - "type": "java.lang.Integer", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.share-transaction-connections", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.test-query", - "type": "java.lang.String", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.two-pc-ordering-position", - "type": "java.lang.Integer", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.unique-name", - "type": "java.lang.String", - "deprecation": { - "level": "error" - } - }, - { - "name": "spring.jta.bitronix.datasource.use-tm-join", - "type": "java.lang.Boolean", - "deprecation": { - "level": "error" - } - }, { "name": "spring.main.allow-bean-definition-overriding", "type": "java.lang.Boolean", From 12254b11ce9f14fd03cfb37ed2095abce9a27e15 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 21 Jul 2023 16:24:32 +0100 Subject: [PATCH 0184/1215] Don't run KotlinPluginActionITs on JVMs not support by Gradle 7.6 --- .../boot/gradle/plugin/KotlinPluginActionIntegrationTests.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests.java index 54d8807735da..ab32302702fc 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/KotlinPluginActionIntegrationTests.java @@ -24,6 +24,8 @@ import org.gradle.testkit.runner.BuildResult; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.testsupport.gradle.testkit.GradleBuild; @@ -36,6 +38,7 @@ * * @author Andy Wilkinson */ +@DisabledForJreRange(min = JRE.JAVA_20) @ExtendWith(GradleBuildExtension.class) class KotlinPluginActionIntegrationTests { From fe95c26a149927794e25e3c5a01572bccb5714a8 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 24 Jul 2023 12:46:49 +0200 Subject: [PATCH 0185/1215] Upgrade Java 17 version in CI image and .sdkmanrc Closes gh-36459 --- .sdkmanrc | 2 +- ci/images/get-jdk-url.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.sdkmanrc b/.sdkmanrc index 93afdeb201db..326fad3a9204 100644 --- a/.sdkmanrc +++ b/.sdkmanrc @@ -1,3 +1,3 @@ # Enable auto-env through the sdkman_auto_env config # Add key=value pairs of SDKs to use below -java=17.0.7-librca +java=17.0.8-librca diff --git a/ci/images/get-jdk-url.sh b/ci/images/get-jdk-url.sh index 6dc041e1d64e..80ca4cdb1a96 100755 --- a/ci/images/get-jdk-url.sh +++ b/ci/images/get-jdk-url.sh @@ -3,7 +3,7 @@ set -e case "$1" in java17) - echo "https://github.com/bell-sw/Liberica/releases/download/17.0.7+7/bellsoft-jdk17.0.7+7-linux-amd64.tar.gz" + echo "https://github.com/bell-sw/Liberica/releases/download/17.0.8+7/bellsoft-jdk17.0.8+7-linux-amd64.tar.gz" ;; java20) echo "https://github.com/bell-sw/Liberica/releases/download/20.0.1+10/bellsoft-jdk20.0.1+10-linux-amd64.tar.gz" From 6050fff078449a874d24950394705e471dd3fe49 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 25 Jul 2023 09:15:11 +0200 Subject: [PATCH 0186/1215] Auto-configure observability for R2DBC The new ConnectionFactoryDecorator can be used to decorate the ConnectionFactory built by the ConnectionFactoryBuilder. The new R2dbcObservationAutoConfiguration configures a ConnectionFactoryDecorator to attach a ObservationProxyExecutionListener to ConnectionFactories. This enables Micrometer Observations for R2DBC queries. Closes gh-33768 --- .../build.gradle | 1 + .../R2dbcObservationAutoConfiguration.java | 81 +++++++++++ .../r2dbc/R2dbcObservationProperties.java | 43 ++++++ ...ot.autoconfigure.AutoConfiguration.imports | 3 +- ...2dbcObservationAutoConfigurationTests.java | 131 ++++++++++++++++++ .../ConnectionFactoryConfigurations.java | 17 ++- .../docs/asciidoc/actuator/observability.adoc | 8 +- .../boot/r2dbc/ConnectionFactoryBuilder.java | 35 ++++- .../r2dbc/ConnectionFactoryDecorator.java | 38 +++++ .../r2dbc/ConnectionFactoryBuilderTests.java | 27 ++++ 10 files changed, 374 insertions(+), 10 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/r2dbc/R2dbcObservationAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/r2dbc/R2dbcObservationProperties.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/r2dbc/R2dbcObservationAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/r2dbc/ConnectionFactoryDecorator.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index a165eeee1b00..777cab24fef4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -74,6 +74,7 @@ dependencies { optional("io.projectreactor.netty:reactor-netty-http") optional("io.r2dbc:r2dbc-pool") optional("io.r2dbc:r2dbc-spi") + optional("io.r2dbc:r2dbc-proxy") optional("jakarta.jms:jakarta.jms-api") optional("jakarta.persistence:jakarta.persistence-api") optional("jakarta.servlet:jakarta.servlet-api") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/r2dbc/R2dbcObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/r2dbc/R2dbcObservationAutoConfiguration.java new file mode 100644 index 000000000000..6065ca676526 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/r2dbc/R2dbcObservationAutoConfiguration.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.r2dbc; + +import io.micrometer.observation.ObservationRegistry; +import io.r2dbc.proxy.ProxyConnectionFactory; +import io.r2dbc.proxy.observation.ObservationProxyExecutionListener; +import io.r2dbc.proxy.observation.QueryObservationConvention; +import io.r2dbc.proxy.observation.QueryParametersTagProvider; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.r2dbc.ConnectionFactoryDecorator; +import org.springframework.boot.r2dbc.OptionsCapableConnectionFactory; +import org.springframework.context.annotation.Bean; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for R2DBC observability support. + * + * @author Moritz Halbritter + * @since 3.2.0 + */ +@AutoConfiguration(after = ObservationAutoConfiguration.class) +@ConditionalOnClass({ ConnectionFactory.class, ProxyConnectionFactory.class }) +@EnableConfigurationProperties(R2dbcObservationProperties.class) +public class R2dbcObservationAutoConfiguration { + + @Bean + @ConditionalOnBean(ObservationRegistry.class) + ConnectionFactoryDecorator connectionFactoryDecorator(R2dbcObservationProperties properties, + ObservationRegistry observationRegistry, + ObjectProvider queryObservationConvention, + ObjectProvider queryParametersTagProvider) { + return (connectionFactory) -> { + ObservationProxyExecutionListener listener = new ObservationProxyExecutionListener(observationRegistry, + connectionFactory, extractUrl(connectionFactory)); + listener.setIncludeParameterValues(properties.isIncludeParameterValues()); + queryObservationConvention.ifAvailable(listener::setQueryObservationConvention); + queryParametersTagProvider.ifAvailable(listener::setQueryParametersTagProvider); + return ProxyConnectionFactory.builder(connectionFactory).listener(listener).build(); + }; + } + + private String extractUrl(ConnectionFactory connectionFactory) { + OptionsCapableConnectionFactory optionsCapableConnectionFactory = OptionsCapableConnectionFactory + .unwrapFrom(connectionFactory); + if (optionsCapableConnectionFactory == null) { + return null; + } + ConnectionFactoryOptions options = optionsCapableConnectionFactory.getOptions(); + Object host = options.getValue(ConnectionFactoryOptions.HOST); + Object port = options.getValue(ConnectionFactoryOptions.PORT); + if (host == null || !(port instanceof Integer portAsInt)) { + return null; + } + // See https://github.com/r2dbc/r2dbc-proxy/issues/135 + return "r2dbc:dummy://%s:%d/".formatted(host, portAsInt); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/r2dbc/R2dbcObservationProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/r2dbc/R2dbcObservationProperties.java new file mode 100644 index 000000000000..4eedf3e12282 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/r2dbc/R2dbcObservationProperties.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.r2dbc; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for R2DBC observability. + * + * @author Moritz Halbritter + * @since 3.2.0 + */ +@ConfigurationProperties("management.observations.r2dbc") +public class R2dbcObservationProperties { + + /** + * Whether to tag actual query parameter values. + */ + private boolean includeParameterValues; + + public boolean isIncludeParameterValues() { + return this.includeParameterValues; + } + + public void setIncludeParameterValues(boolean includeParameterValues) { + this.includeParameterValues = includeParameterValues; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index f2b80c4ffb40..eb46381bf1a0 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -90,6 +90,7 @@ org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfig org.springframework.boot.actuate.autoconfigure.observation.web.servlet.WebMvcObservationAutoConfiguration org.springframework.boot.actuate.autoconfigure.quartz.QuartzEndpointAutoConfiguration org.springframework.boot.actuate.autoconfigure.r2dbc.ConnectionFactoryHealthContributorAutoConfiguration +org.springframework.boot.actuate.autoconfigure.r2dbc.R2dbcObservationAutoConfiguration org.springframework.boot.actuate.autoconfigure.data.redis.RedisHealthContributorAutoConfiguration org.springframework.boot.actuate.autoconfigure.data.redis.RedisReactiveHealthContributorAutoConfiguration org.springframework.boot.actuate.autoconfigure.scheduling.ScheduledTasksEndpointAutoConfiguration @@ -112,4 +113,4 @@ org.springframework.boot.actuate.autoconfigure.web.exchanges.HttpExchangesEndpoi org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration org.springframework.boot.actuate.autoconfigure.web.reactive.ReactiveManagementContextAutoConfiguration org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration -org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration \ No newline at end of file +org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/r2dbc/R2dbcObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/r2dbc/R2dbcObservationAutoConfigurationTests.java new file mode 100644 index 000000000000..7d3615dc2f94 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/r2dbc/R2dbcObservationAutoConfigurationTests.java @@ -0,0 +1,131 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.r2dbc; + +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; + +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.r2dbc.spi.ConnectionFactory; +import org.awaitility.Awaitility; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.annotation.ImportCandidates; +import org.springframework.boot.r2dbc.ConnectionFactoryBuilder; +import org.springframework.boot.r2dbc.ConnectionFactoryDecorator; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link R2dbcObservationAutoConfiguration}. + * + * @author Moritz Halbritter + */ +class R2dbcObservationAutoConfigurationTests { + + private final ApplicationContextRunner runnerWithoutObservationRegistry = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(R2dbcObservationAutoConfiguration.class)); + + private final ApplicationContextRunner runner = this.runnerWithoutObservationRegistry + .withBean(ObservationRegistry.class, ObservationRegistry::create); + + @Test + void shouldBeRegisteredInAutoConfigurationImports() { + assertThat(ImportCandidates.load(AutoConfiguration.class, null).getCandidates()) + .contains(R2dbcObservationAutoConfiguration.class.getName()); + } + + @Test + void shouldSupplyConnectionFactoryDecorator() { + this.runner.run((context) -> assertThat(context).hasSingleBean(ConnectionFactoryDecorator.class)); + } + + @Test + void shouldNotSupplyBeansIfR2dbcSpiIsNotOnClasspath() { + this.runner.withClassLoader(new FilteredClassLoader("io.r2dbc.spi")) + .run((context) -> assertThat(context).doesNotHaveBean(ConnectionFactoryDecorator.class)); + } + + @Test + void shouldNotSupplyBeansIfR2dbcProxyIsNotOnClasspath() { + this.runner.withClassLoader(new FilteredClassLoader("io.r2dbc.proxy")) + .run((context) -> assertThat(context).doesNotHaveBean(ConnectionFactoryDecorator.class)); + } + + @Test + void shouldNotSupplyBeansIfObservationRegistryIsNotPresent() { + this.runnerWithoutObservationRegistry + .run((context) -> assertThat(context).doesNotHaveBean(ConnectionFactoryDecorator.class)); + } + + @Test + void decoratorShouldReportObservations() { + this.runner.run((context) -> { + CapturingObservationHandler handler = registerCapturingObservationHandler(context); + ConnectionFactoryDecorator decorator = context.getBean(ConnectionFactoryDecorator.class); + assertThat(decorator).isNotNull(); + ConnectionFactory connectionFactory = ConnectionFactoryBuilder + .withUrl("r2dbc:h2:mem:///" + UUID.randomUUID()) + .build(); + ConnectionFactory decorated = decorator.decorate(connectionFactory); + Mono.from(decorated.create()) + .flatMap((c) -> Mono.from(c.createStatement("SELECT 1;").execute()) + .flatMap((ignore) -> Mono.from(c.close()))) + .block(); + assertThat(handler.awaitContext().getName()).as("context.getName()").isEqualTo("r2dbc.query"); + }); + } + + private static CapturingObservationHandler registerCapturingObservationHandler( + AssertableApplicationContext context) { + ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); + assertThat(observationRegistry).isNotNull(); + CapturingObservationHandler handler = new CapturingObservationHandler(); + observationRegistry.observationConfig().observationHandler(handler); + return handler; + } + + private static class CapturingObservationHandler implements ObservationHandler { + + private final AtomicReference context = new AtomicReference<>(); + + @Override + public boolean supportsContext(Context context) { + return true; + } + + @Override + public void onStart(Context context) { + this.context.set(context); + } + + Context awaitContext() { + return Awaitility.await().untilAtomic(this.context, Matchers.notNullValue()); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations.java index 987b80fee7fc..6807a349f77b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations.java @@ -33,6 +33,7 @@ import org.springframework.boot.context.properties.bind.BindResult; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.r2dbc.ConnectionFactoryDecorator; import org.springframework.boot.r2dbc.EmbeddedDatabaseConnection; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Condition; @@ -54,12 +55,14 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Moritz Halbritter */ abstract class ConnectionFactoryConfigurations { protected static ConnectionFactory createConnectionFactory(R2dbcProperties properties, R2dbcConnectionDetails connectionDetails, ClassLoader classLoader, - List optionsCustomizers) { + List optionsCustomizers, + List decorators) { try { return org.springframework.boot.r2dbc.ConnectionFactoryBuilder .withOptions(new ConnectionFactoryOptionsInitializer().initialize(properties, connectionDetails, @@ -69,6 +72,7 @@ protected static ConnectionFactory createConnectionFactory(R2dbcProperties prope optionsCustomizer.customize(options); } }) + .decorators(decorators) .build(); } catch (IllegalStateException ex) { @@ -93,10 +97,11 @@ static class PooledConnectionFactoryConfiguration { @Bean(destroyMethod = "dispose") ConnectionPool connectionFactory(R2dbcProperties properties, ObjectProvider connectionDetails, ResourceLoader resourceLoader, - ObjectProvider customizers) { + ObjectProvider customizers, + ObjectProvider decorators) { ConnectionFactory connectionFactory = createConnectionFactory(properties, connectionDetails.getIfAvailable(), resourceLoader.getClassLoader(), - customizers.orderedStream().toList()); + customizers.orderedStream().toList(), decorators.orderedStream().toList()); R2dbcProperties.Pool pool = properties.getPool(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); ConnectionPoolConfiguration.Builder builder = ConnectionPoolConfiguration.builder(connectionFactory); @@ -126,9 +131,11 @@ static class GenericConfiguration { @Bean ConnectionFactory connectionFactory(R2dbcProperties properties, ObjectProvider connectionDetails, ResourceLoader resourceLoader, - ObjectProvider customizers) { + ObjectProvider customizers, + ObjectProvider decorators) { return createConnectionFactory(properties, connectionDetails.getIfAvailable(), - resourceLoader.getClassLoader(), customizers.orderedStream().toList()); + resourceLoader.getClassLoader(), customizers.orderedStream().toList(), + decorators.orderedStream().toList()); } } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc index e8c900d161e9..29c42af3743c 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc @@ -16,10 +16,12 @@ You can additionally register any number of `ObservationRegistryCustomizer` bean For more details please see the https://micrometer.io/docs/observation[Micrometer Observation documentation]. -TIP: Observability for JDBC and R2DBC can be configured using separate projects. -For JDBC, the https://github.com/jdbc-observations/datasource-micrometer[Datasource Micrometer project] provides a Spring Boot starter which automatically creates observations when JDBC operations are invoked. +TIP: Observability for JDBC can be configured using a separate project. +The https://github.com/jdbc-observations/datasource-micrometer[Datasource Micrometer project] provides a Spring Boot starter which automatically creates observations when JDBC operations are invoked. Read more about it https://jdbc-observations.github.io/datasource-micrometer/docs/current/docs/html/[in the reference documentation]. -For R2DBC, the https://github.com/spring-projects-experimental/r2dbc-micrometer-spring-boot[Spring Boot Auto Configuration for R2DBC Observation] creates observations for R2DBC query invocations. + +TIP: Observability for R2DBC is built into Spring Boot. +To enable it, add the `io.r2dbc:r2dbc-proxy` dependency to your project. [[actuator.observability.common-key-values]] === Common Key-Values diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/r2dbc/ConnectionFactoryBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/r2dbc/ConnectionFactoryBuilder.java index e69eca5d33bd..0e7880d7a2ea 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/r2dbc/ConnectionFactoryBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/r2dbc/ConnectionFactoryBuilder.java @@ -17,6 +17,8 @@ package org.springframework.boot.r2dbc; import java.time.Duration; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; import java.util.function.Consumer; import java.util.function.Function; @@ -43,6 +45,7 @@ * @author Tadaya Tsuyukubo * @author Stephane Nicoll * @author Andy Wilkinson + * @author Moritz Halbritter * @since 2.5.0 */ public final class ConnectionFactoryBuilder { @@ -62,6 +65,8 @@ public final class ConnectionFactoryBuilder { private final Builder optionsBuilder; + private final List decorators = new ArrayList<>(); + private ConnectionFactoryBuilder(Builder optionsBuilder) { this.optionsBuilder = optionsBuilder; } @@ -168,13 +173,41 @@ public ConnectionFactoryBuilder database(String database) { return configure((options) -> options.option(ConnectionFactoryOptions.DATABASE, database)); } + /** + * Add a {@link ConnectionFactoryDecorator decorator}. + * @param decorator the decorator to add + * @return this for method chaining + * @since 3.2.0 + */ + public ConnectionFactoryBuilder decorator(ConnectionFactoryDecorator decorator) { + this.decorators.add(decorator); + return this; + } + + /** + * Add {@link ConnectionFactoryDecorator decorators}. + * @param decorators the decorators to add + * @return this for method chaining + * @since 3.2.0 + */ + public ConnectionFactoryBuilder decorators(Iterable decorators) { + for (ConnectionFactoryDecorator decorator : decorators) { + this.decorators.add(decorator); + } + return this; + } + /** * Build a {@link ConnectionFactory} based on the state of this builder. * @return a connection factory */ public ConnectionFactory build() { ConnectionFactoryOptions options = buildOptions(); - return optionsCapableWrapper.buildAndWrap(options); + ConnectionFactory connectionFactory = optionsCapableWrapper.buildAndWrap(options); + for (ConnectionFactoryDecorator decorator : this.decorators) { + connectionFactory = decorator.decorate(connectionFactory); + } + return connectionFactory; } /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/r2dbc/ConnectionFactoryDecorator.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/r2dbc/ConnectionFactoryDecorator.java new file mode 100644 index 000000000000..f4885ec6c632 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/r2dbc/ConnectionFactoryDecorator.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2023 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.r2dbc; + +import io.r2dbc.spi.ConnectionFactory; + +/** + * Decorator for {@link ConnectionFactory connection factories}. + * + * @author Moritz Halbritter + * @since 3.2.0 + * @see ConnectionFactoryBuilder + */ +@FunctionalInterface +public interface ConnectionFactoryDecorator { + + /** + * Decorates the given {@link ConnectionFactory}. + * @param delegate the connection factory which should be decorated + * @return the decorated connection factory + */ + ConnectionFactory decorate(ConnectionFactory delegate); + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/r2dbc/ConnectionFactoryBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/r2dbc/ConnectionFactoryBuilderTests.java index e9e5e3988da0..43eedcdaf6e7 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/r2dbc/ConnectionFactoryBuilderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/r2dbc/ConnectionFactoryBuilderTests.java @@ -26,7 +26,9 @@ import io.r2dbc.pool.ConnectionPool; import io.r2dbc.pool.ConnectionPoolConfiguration; import io.r2dbc.pool.PoolingConnectionFactoryProvider; +import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.Option; import io.r2dbc.spi.ValidationDepth; @@ -34,6 +36,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.reactivestreams.Publisher; import org.springframework.boot.r2dbc.ConnectionFactoryBuilder.PoolingAwareOptionsCapableWrapper; import org.springframework.core.ResolvableType; @@ -50,6 +53,7 @@ * @author Mark Paluch * @author Tadaya Tsuyukubo * @author Stephane Nicoll + * @author Moritz Halbritter */ class ConnectionFactoryBuilderTests { @@ -235,6 +239,15 @@ void stringlyTypedOptionIsMappedWhenCreatingPoolConfiguration(Option option) { assertThat(configuration).extracting(expectedOption.property).isEqualTo(expectedOption.value); } + @Test + void shouldApplyDecorators() { + String url = "r2dbc:pool:h2:mem:///" + UUID.randomUUID(); + ConnectionFactory connectionFactory = ConnectionFactoryBuilder.withUrl(url) + .decorator((ignored) -> new MyConnectionFactory()) + .build(); + assertThat(connectionFactory).isInstanceOf(MyConnectionFactory.class); + } + private static Iterable primitivePoolingConnectionProviderOptions() { return extractPoolingConnectionProviderOptions((field) -> { ResolvableType type = ResolvableType.forField(field); @@ -320,4 +333,18 @@ static ExpectedOption get(Option option) { } + private static class MyConnectionFactory implements ConnectionFactory { + + @Override + public Publisher create() { + return null; + } + + @Override + public ConnectionFactoryMetadata getMetadata() { + return null; + } + + } + } From 14d2675aab99a32e874980d87af6bb9aae940af7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 25 Jul 2023 15:32:41 +0100 Subject: [PATCH 0187/1215] Add `@ConditionalOnCheckpointRestore` Closes gh-36536 --- .../ConditionalOnCheckpointRestore.java | 40 +++++++++++++ .../ConditionalOnCheckpointRestoreTests.java | 60 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnCheckpointRestore.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnCheckpointRestoreTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnCheckpointRestore.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnCheckpointRestore.java new file mode 100644 index 000000000000..2505d4930ffc --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnCheckpointRestore.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2023 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.condition; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Conditional; + +/** + * {@link Conditional @Conditional} that only matches when coordinated restore at + * checkpoint is to be used. + * + * @author Andy Wilkinson + * @since 3.2.0 + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ConditionalOnClass(name = "org.crac.Resource") +public @interface ConditionalOnCheckpointRestore { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnCheckpointRestoreTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnCheckpointRestoreTests.java new file mode 100644 index 000000000000..7e50b6423e69 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnCheckpointRestoreTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2023 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.condition; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.classpath.ClassPathOverrides; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConditionalOnCheckpointRestore @ConditionalOnCheckpointRestore}. + * + * @author Andy Wilkinson + */ +class ConditionalOnCheckpointRestoreTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(BasicConfiguration.class); + + @Test + void whenCracIsUnavailableThenConditionDoesNotMatch() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean("someBean")); + } + + @Test + @ClassPathOverrides("org.crac:crac:1.3.0") + void whenCracIsAvailableThenConditionMatches() { + this.contextRunner.run((context) -> assertThat(context).hasBean("someBean")); + } + + @Configuration(proxyBeanMethods = false) + static class BasicConfiguration { + + @Bean + @ConditionalOnCheckpointRestore + String someBean() { + return "someBean"; + } + + } + +} From 9240f971fb8382995949145a12e374b50bb56f08 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 13 Jul 2023 13:39:36 +0200 Subject: [PATCH 0188/1215] Make HikariDataSource participate in checkpoint-restore See gh-36422 --- .../spring-boot-autoconfigure/build.gradle | 1 + .../jdbc/DataSourceConfiguration.java | 6 + .../autoconfigure/jdbc/HikariLifecycle.java | 225 ++++++++++++++++++ .../jdbc/DataSourceJmxConfigurationTests.java | 25 +- .../jdbc/HikariLifecycleTests.java | 76 ++++++ 5 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/HikariLifecycle.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariLifecycleTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index a30895a521d8..4dba7255b004 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -67,6 +67,7 @@ dependencies { optional("org.apache.tomcat:tomcat-jdbc") optional("org.apiguardian:apiguardian-api") optional("org.apache.groovy:groovy-templates") + optional("org.crac:crac:1.3.0") optional("org.eclipse.angus:angus-mail") optional("com.github.ben-manes.caffeine:caffeine") optional("com.github.mxab.thymeleaf.extras:thymeleaf-extras-data-attribute") diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java index 8dd321ee7cb4..c01d840850ed 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java @@ -122,6 +122,12 @@ HikariDataSource dataSource(DataSourceProperties properties, JdbcConnectionDetai return dataSource; } + @Bean + @ConditionalOnClass(name = "org.crac.Resource") + HikariLifecycle dataSourceLifecycle(HikariDataSource hikariDataSource) { + return new HikariLifecycle(hikariDataSource); + } + } /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/HikariLifecycle.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/HikariLifecycle.java new file mode 100644 index 000000000000..611844ce9d6b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/HikariLifecycle.java @@ -0,0 +1,225 @@ +/* + * Copyright 2023 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.jdbc; + +import java.lang.reflect.Field; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; + +import javax.sql.DataSource; + +import com.zaxxer.hikari.HikariConfigMXBean; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.HikariPoolMXBean; +import com.zaxxer.hikari.pool.HikariPool; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.jdbc.DataSourceUnwrapper; +import org.springframework.context.Lifecycle; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * {@link Lifecycle} management for a {@link HikariDataSource} taking care of + * {@link Lifecycle#start() starting}/{@link Lifecycle#stop() stopping} the + * {@link javax.sql.DataSource} by {@link HikariDataSource#isAllowPoolSuspension() + * eventually} suspending/resuming the underlying {@link HikariPool connection pool} and + * {@link HikariPoolMXBean#softEvictConnections() evicting} open & idle connections. + * + * @author Christoph Strobl + */ +class HikariLifecycle implements Lifecycle { + + private final HikariDataSource dataSource; + + private final LifecycleExecutor lifecycleExecutor; + + HikariLifecycle(HikariDataSource dataSource) { + + this.dataSource = dataSource; + this.lifecycleExecutor = new LifecycleExecutor(dataSource); + } + + HikariDataSource getManagedInstance() { + return this.dataSource; + } + + @Override + public void start() { + + if (this.dataSource.isRunning()) { + return; + } + + if (this.dataSource.isClosed()) { + throw new IllegalStateException("DataSource has been closed and cannot be restarted"); + } + + this.lifecycleExecutor.resume(); + } + + @Override + public void stop() { + if (this.dataSource.isRunning()) { + this.lifecycleExecutor.pause(); + } + } + + @Override + public boolean isRunning() { + return this.dataSource.isRunning(); + } + + /** + * Component to help suspend/resume a {@link HikariDataSource} by taking the pool + * suspension flag into account. Will perform best effort to make sure connections + * reported as closed buy the {@link HikariPoolMXBean} have actually been closed by + * the {@link java.util.concurrent.Executor} that is in charge of closing them. + * + * @author Christoph Strobl + */ + private static class LifecycleExecutor { + + private static final Log logger = LogFactory.getLog(LifecycleExecutor.class); + + private static final Field CLOSE_CONNECTION_EXECUTOR; + + private final HikariDataSource dataSource; + + private final Function hasOpenConnections; + + static { + + Field closeConnectionExecutor = ReflectionUtils.findField(HikariPool.class, "closeConnectionExecutor"); + Assert.notNull(closeConnectionExecutor, "Unable to locate closeConnectionExecutor for HikariPool"); + Assert.isAssignable(ThreadPoolExecutor.class, closeConnectionExecutor.getType(), + "Expected ThreadPoolExecutor for closeConnectionExecutor but found %s" + .formatted(closeConnectionExecutor.getType())); + + ReflectionUtils.makeAccessible(closeConnectionExecutor); + + CLOSE_CONNECTION_EXECUTOR = closeConnectionExecutor; + } + + LifecycleExecutor(HikariDataSource hikariDataSource) { + + this.dataSource = getUltimateTargetObject(hikariDataSource); + this.hasOpenConnections = new Function<>() { + + @Override + public Boolean apply(HikariPool pool) { + + ThreadPoolExecutor closeConnectionExecutor = (ThreadPoolExecutor) ReflectionUtils + .getField(CLOSE_CONNECTION_EXECUTOR, pool); + if (closeConnectionExecutor == null) { + throw new IllegalStateException("CloseConnectionExecutor was null"); + } + return closeConnectionExecutor.getActiveCount() > 0; + } + }; + } + + /** + * Pause the {@link HikariDataSource} and try to suspend obtaining new connections + * from the pool if possible. Will wait for connection to be closed. Default + * timeout is set to {@link HikariDataSource#getConnectionTimeout()} + 250 ms. + */ + void pause() { + pause(Duration.ofMillis(this.dataSource.getConnectionTimeout() + 250)); + } + + /** + * Pause the {@link HikariDataSource} and try to suspend obtaining new connections + * from the pool if possible. Wait at most the given {@literal shutdownTimeout} + * for connections to be closed. + * @param shutdownTimeout max timeout to wait for connections to be closed. + */ + void pause(Duration shutdownTimeout) { + + if (this.dataSource.isAllowPoolSuspension()) { + logger.info("Suspending Hikari pool"); + this.dataSource.getHikariPoolMXBean().suspendPool(); + } + closeConnections(shutdownTimeout); + } + + /** + * Resume the {@link HikariDataSource} by lifting the pool suspension if set. + */ + void resume() { + + if (this.dataSource.isAllowPoolSuspension()) { + logger.info("Resuming Hikari pool"); + this.dataSource.getHikariPoolMXBean().resumePool(); + } + } + + void closeConnections(Duration shutdownTimeout) { + + logger.info("Evicting Hikari connections"); + this.dataSource.getHikariPoolMXBean().softEvictConnections(); + + logger.debug("Waiting for Hikari connections to be closed"); + CompletableFuture allConnectionsClosed = CompletableFuture.runAsync(this::waitForConnectionsToClose); + try { + allConnectionsClosed.get(shutdownTimeout.toMillis(), TimeUnit.MILLISECONDS); + logger.debug("Hikari connections closed"); + } + catch (InterruptedException ex) { + logger.error("Interrupted while waiting for connections to be closed", ex); + Thread.currentThread().interrupt(); + } + catch (TimeoutException ex) { + logger.error("Hikari connections could not be closed within %s".formatted(shutdownTimeout), ex); + } + catch (ExecutionException ex) { + throw new RuntimeException("Failed to close Hikari connections", ex); + } + } + + private void waitForConnectionsToClose() { + + if (!(this.dataSource.getHikariPoolMXBean() instanceof HikariPool pool)) { + throw new IllegalStateException( + "Expected HikariPool instance but was %s".formatted(this.dataSource.getHikariPoolMXBean())); + } + + while (this.hasOpenConnections.apply(pool)) { + try { + TimeUnit.MILLISECONDS.sleep(50); + } + catch (InterruptedException ex) { + logger.error("Interrupted while waiting for datasource connections to be closed", ex); + Thread.currentThread().interrupt(); + } + } + } + + @SuppressWarnings("unchecked") + private static HikariDataSource getUltimateTargetObject(DataSource candidate) { + return DataSourceUnwrapper.unwrap(candidate, HikariConfigMXBean.class, HikariDataSource.class); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceJmxConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceJmxConfigurationTests.java index b8c365a8f54f..7bf81e7fe193 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceJmxConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceJmxConfigurationTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.jdbc; import java.lang.management.ManagementFactory; +import java.sql.Connection; import java.util.Set; import java.util.UUID; @@ -35,6 +36,7 @@ import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -107,7 +109,8 @@ void hikariAutoConfiguredUsesJmxFlag() { assertThat(hikariDataSource.isRegisterMbeans()).isTrue(); // Ensure that the pool has been initialized, triggering MBean // registration - hikariDataSource.getConnection().close(); + Connection connection = hikariDataSource.getConnection(); + hikariDataSource.evictConnection(connection); // Hikari can still register mBeans validateHikariMBeansRegistration(ManagementFactory.getPlatformMBeanServer(), poolName, true); }); @@ -132,6 +135,21 @@ void hikariProxiedCanUseRegisterMBeans() { }); } + @Test + void hikariAutoConfigRegistersLifecycleBean() { + + this.contextRunner.withPropertyValues("spring.datasource.type=" + HikariDataSource.class.getName()) + .run((context) -> assertThat(context).hasSingleBean(HikariLifecycle.class)); + } + + @Test + void hikariAutoConfigConditionallyRegistersLifecycleBean() { + + this.contextRunner.withPropertyValues("spring.datasource.type=" + HikariDataSource.class.getName()) + .withClassLoader(new FilteredClassLoader("org.crac")) + .run((context) -> assertThat(context).doesNotHaveBean(HikariLifecycle.class)); + } + private void validateHikariMBeansRegistration(MBeanServer mBeanServer, String poolName, boolean expected) throws MalformedObjectNameException { assertThat(mBeanServer.isRegistered(new ObjectName("com.zaxxer.hikari:type=Pool (" + poolName + ")"))) @@ -196,7 +214,10 @@ static class DataSourceBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessAfterInitialization(Object bean, String beanName) { if (bean instanceof javax.sql.DataSource) { - return new ProxyFactory(bean).getProxy(); + + ProxyFactory pf = new ProxyFactory(bean); + pf.setProxyTargetClass(true); + return pf.getProxy(); } return bean; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariLifecycleTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariLifecycleTests.java new file mode 100644 index 000000000000..4fea72556240 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariLifecycleTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2023 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.jdbc; + +import java.util.UUID; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link HikariLifecycle}. + * + * @author Christoph Strobl + */ +class HikariLifecycleTests { + + @Test + void stopStartHikariDataSource() { + + HikariLifecycle hikariLifecycle = createLifecycle(); + + assertThat(hikariLifecycle.isRunning()).isTrue(); + + hikariLifecycle.stop(); + + assertThat(hikariLifecycle.getManagedInstance().isRunning()).isFalse(); + assertThat(hikariLifecycle.getManagedInstance().isClosed()).isFalse(); + assertThat(hikariLifecycle.isRunning()).isFalse(); + assertThat(hikariLifecycle.getManagedInstance().getHikariPoolMXBean().getTotalConnections()).isZero(); + + hikariLifecycle.start(); + + assertThat(hikariLifecycle.getManagedInstance().isRunning()).isTrue(); + assertThat(hikariLifecycle.getManagedInstance().isClosed()).isFalse(); + assertThat(hikariLifecycle.isRunning()).isTrue(); + } + + @Test + void cannotStartClosedDataSource() { + + HikariLifecycle hikariLifecycle = createLifecycle(); + hikariLifecycle.getManagedInstance().close(); + + assertThatExceptionOfType(RuntimeException.class).isThrownBy(hikariLifecycle::start); + } + + HikariLifecycle createLifecycle() { + + HikariConfig config = new HikariConfig(); + config.setAllowPoolSuspension(true); + config.setJdbcUrl("jdbc:hsqldb:mem:test-" + UUID.randomUUID()); + config.setPoolName("lifecycle-tests"); + + HikariDataSource source = new HikariDataSource(config); + return new HikariLifecycle(source); + } + +} From b476d368dbce9fd6dd8810f0f408ac30bb156aef Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 25 Jul 2023 16:27:31 +0100 Subject: [PATCH 0189/1215] Polish "Make HikariDataSource participate in checkpoint-restore" See gh-36422 --- .../spring-boot-autoconfigure/build.gradle | 1 - .../jdbc/DataSourceConfiguration.java | 8 +- .../autoconfigure/jdbc/HikariLifecycle.java | 225 ------------------ .../jdbc/DataSourceJmxConfigurationTests.java | 25 +- .../HikariDataSourceConfigurationTests.java | 15 ++ .../jdbc/HikariLifecycleTests.java | 76 ------ .../HikariCheckpointRestoreLifecycle.java | 149 ++++++++++++ ...HikariCheckpointRestoreLifecycleTests.java | 85 +++++++ 8 files changed, 256 insertions(+), 328 deletions(-) delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/HikariLifecycle.java delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariLifecycleTests.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/HikariCheckpointRestoreLifecycle.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/HikariCheckpointRestoreLifecycleTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index 4dba7255b004..a30895a521d8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -67,7 +67,6 @@ dependencies { optional("org.apache.tomcat:tomcat-jdbc") optional("org.apiguardian:apiguardian-api") optional("org.apache.groovy:groovy-templates") - optional("org.crac:crac:1.3.0") optional("org.eclipse.angus:angus-mail") optional("com.github.ben-manes.caffeine:caffeine") optional("com.github.mxab.thymeleaf.extras:thymeleaf-extras-data-attribute") diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java index c01d840850ed..d0a1098e2e1a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java @@ -25,12 +25,14 @@ import oracle.ucp.jdbc.PoolDataSourceImpl; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnCheckpointRestore; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jdbc.DatabaseDriver; +import org.springframework.boot.jdbc.HikariCheckpointRestoreLifecycle; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.StringUtils; @@ -123,9 +125,9 @@ HikariDataSource dataSource(DataSourceProperties properties, JdbcConnectionDetai } @Bean - @ConditionalOnClass(name = "org.crac.Resource") - HikariLifecycle dataSourceLifecycle(HikariDataSource hikariDataSource) { - return new HikariLifecycle(hikariDataSource); + @ConditionalOnCheckpointRestore + HikariCheckpointRestoreLifecycle hikariCheckpointRestoreLifecycle(HikariDataSource hikariDataSource) { + return new HikariCheckpointRestoreLifecycle(hikariDataSource); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/HikariLifecycle.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/HikariLifecycle.java deleted file mode 100644 index 611844ce9d6b..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/HikariLifecycle.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright 2023 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.jdbc; - -import java.lang.reflect.Field; -import java.time.Duration; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.function.Function; - -import javax.sql.DataSource; - -import com.zaxxer.hikari.HikariConfigMXBean; -import com.zaxxer.hikari.HikariDataSource; -import com.zaxxer.hikari.HikariPoolMXBean; -import com.zaxxer.hikari.pool.HikariPool; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.boot.jdbc.DataSourceUnwrapper; -import org.springframework.context.Lifecycle; -import org.springframework.util.Assert; -import org.springframework.util.ReflectionUtils; - -/** - * {@link Lifecycle} management for a {@link HikariDataSource} taking care of - * {@link Lifecycle#start() starting}/{@link Lifecycle#stop() stopping} the - * {@link javax.sql.DataSource} by {@link HikariDataSource#isAllowPoolSuspension() - * eventually} suspending/resuming the underlying {@link HikariPool connection pool} and - * {@link HikariPoolMXBean#softEvictConnections() evicting} open & idle connections. - * - * @author Christoph Strobl - */ -class HikariLifecycle implements Lifecycle { - - private final HikariDataSource dataSource; - - private final LifecycleExecutor lifecycleExecutor; - - HikariLifecycle(HikariDataSource dataSource) { - - this.dataSource = dataSource; - this.lifecycleExecutor = new LifecycleExecutor(dataSource); - } - - HikariDataSource getManagedInstance() { - return this.dataSource; - } - - @Override - public void start() { - - if (this.dataSource.isRunning()) { - return; - } - - if (this.dataSource.isClosed()) { - throw new IllegalStateException("DataSource has been closed and cannot be restarted"); - } - - this.lifecycleExecutor.resume(); - } - - @Override - public void stop() { - if (this.dataSource.isRunning()) { - this.lifecycleExecutor.pause(); - } - } - - @Override - public boolean isRunning() { - return this.dataSource.isRunning(); - } - - /** - * Component to help suspend/resume a {@link HikariDataSource} by taking the pool - * suspension flag into account. Will perform best effort to make sure connections - * reported as closed buy the {@link HikariPoolMXBean} have actually been closed by - * the {@link java.util.concurrent.Executor} that is in charge of closing them. - * - * @author Christoph Strobl - */ - private static class LifecycleExecutor { - - private static final Log logger = LogFactory.getLog(LifecycleExecutor.class); - - private static final Field CLOSE_CONNECTION_EXECUTOR; - - private final HikariDataSource dataSource; - - private final Function hasOpenConnections; - - static { - - Field closeConnectionExecutor = ReflectionUtils.findField(HikariPool.class, "closeConnectionExecutor"); - Assert.notNull(closeConnectionExecutor, "Unable to locate closeConnectionExecutor for HikariPool"); - Assert.isAssignable(ThreadPoolExecutor.class, closeConnectionExecutor.getType(), - "Expected ThreadPoolExecutor for closeConnectionExecutor but found %s" - .formatted(closeConnectionExecutor.getType())); - - ReflectionUtils.makeAccessible(closeConnectionExecutor); - - CLOSE_CONNECTION_EXECUTOR = closeConnectionExecutor; - } - - LifecycleExecutor(HikariDataSource hikariDataSource) { - - this.dataSource = getUltimateTargetObject(hikariDataSource); - this.hasOpenConnections = new Function<>() { - - @Override - public Boolean apply(HikariPool pool) { - - ThreadPoolExecutor closeConnectionExecutor = (ThreadPoolExecutor) ReflectionUtils - .getField(CLOSE_CONNECTION_EXECUTOR, pool); - if (closeConnectionExecutor == null) { - throw new IllegalStateException("CloseConnectionExecutor was null"); - } - return closeConnectionExecutor.getActiveCount() > 0; - } - }; - } - - /** - * Pause the {@link HikariDataSource} and try to suspend obtaining new connections - * from the pool if possible. Will wait for connection to be closed. Default - * timeout is set to {@link HikariDataSource#getConnectionTimeout()} + 250 ms. - */ - void pause() { - pause(Duration.ofMillis(this.dataSource.getConnectionTimeout() + 250)); - } - - /** - * Pause the {@link HikariDataSource} and try to suspend obtaining new connections - * from the pool if possible. Wait at most the given {@literal shutdownTimeout} - * for connections to be closed. - * @param shutdownTimeout max timeout to wait for connections to be closed. - */ - void pause(Duration shutdownTimeout) { - - if (this.dataSource.isAllowPoolSuspension()) { - logger.info("Suspending Hikari pool"); - this.dataSource.getHikariPoolMXBean().suspendPool(); - } - closeConnections(shutdownTimeout); - } - - /** - * Resume the {@link HikariDataSource} by lifting the pool suspension if set. - */ - void resume() { - - if (this.dataSource.isAllowPoolSuspension()) { - logger.info("Resuming Hikari pool"); - this.dataSource.getHikariPoolMXBean().resumePool(); - } - } - - void closeConnections(Duration shutdownTimeout) { - - logger.info("Evicting Hikari connections"); - this.dataSource.getHikariPoolMXBean().softEvictConnections(); - - logger.debug("Waiting for Hikari connections to be closed"); - CompletableFuture allConnectionsClosed = CompletableFuture.runAsync(this::waitForConnectionsToClose); - try { - allConnectionsClosed.get(shutdownTimeout.toMillis(), TimeUnit.MILLISECONDS); - logger.debug("Hikari connections closed"); - } - catch (InterruptedException ex) { - logger.error("Interrupted while waiting for connections to be closed", ex); - Thread.currentThread().interrupt(); - } - catch (TimeoutException ex) { - logger.error("Hikari connections could not be closed within %s".formatted(shutdownTimeout), ex); - } - catch (ExecutionException ex) { - throw new RuntimeException("Failed to close Hikari connections", ex); - } - } - - private void waitForConnectionsToClose() { - - if (!(this.dataSource.getHikariPoolMXBean() instanceof HikariPool pool)) { - throw new IllegalStateException( - "Expected HikariPool instance but was %s".formatted(this.dataSource.getHikariPoolMXBean())); - } - - while (this.hasOpenConnections.apply(pool)) { - try { - TimeUnit.MILLISECONDS.sleep(50); - } - catch (InterruptedException ex) { - logger.error("Interrupted while waiting for datasource connections to be closed", ex); - Thread.currentThread().interrupt(); - } - } - } - - @SuppressWarnings("unchecked") - private static HikariDataSource getUltimateTargetObject(DataSource candidate) { - return DataSourceUnwrapper.unwrap(candidate, HikariConfigMXBean.class, HikariDataSource.class); - } - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceJmxConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceJmxConfigurationTests.java index 7bf81e7fe193..b8c365a8f54f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceJmxConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceJmxConfigurationTests.java @@ -17,7 +17,6 @@ package org.springframework.boot.autoconfigure.jdbc; import java.lang.management.ManagementFactory; -import java.sql.Connection; import java.util.Set; import java.util.UUID; @@ -36,7 +35,6 @@ import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; -import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -109,8 +107,7 @@ void hikariAutoConfiguredUsesJmxFlag() { assertThat(hikariDataSource.isRegisterMbeans()).isTrue(); // Ensure that the pool has been initialized, triggering MBean // registration - Connection connection = hikariDataSource.getConnection(); - hikariDataSource.evictConnection(connection); + hikariDataSource.getConnection().close(); // Hikari can still register mBeans validateHikariMBeansRegistration(ManagementFactory.getPlatformMBeanServer(), poolName, true); }); @@ -135,21 +132,6 @@ void hikariProxiedCanUseRegisterMBeans() { }); } - @Test - void hikariAutoConfigRegistersLifecycleBean() { - - this.contextRunner.withPropertyValues("spring.datasource.type=" + HikariDataSource.class.getName()) - .run((context) -> assertThat(context).hasSingleBean(HikariLifecycle.class)); - } - - @Test - void hikariAutoConfigConditionallyRegistersLifecycleBean() { - - this.contextRunner.withPropertyValues("spring.datasource.type=" + HikariDataSource.class.getName()) - .withClassLoader(new FilteredClassLoader("org.crac")) - .run((context) -> assertThat(context).doesNotHaveBean(HikariLifecycle.class)); - } - private void validateHikariMBeansRegistration(MBeanServer mBeanServer, String poolName, boolean expected) throws MalformedObjectNameException { assertThat(mBeanServer.isRegistered(new ObjectName("com.zaxxer.hikari:type=Pool (" + poolName + ")"))) @@ -214,10 +196,7 @@ static class DataSourceBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessAfterInitialization(Object bean, String beanName) { if (bean instanceof javax.sql.DataSource) { - - ProxyFactory pf = new ProxyFactory(bean); - pf.setProxyTargetClass(true); - return pf.getProxy(); + return new ProxyFactory(bean).getProxy(); } return bean; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java index 653d59cb8f48..0793585ad383 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java @@ -23,7 +23,9 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.jdbc.HikariCheckpointRestoreLifecycle; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.classpath.ClassPathOverrides; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -122,6 +124,19 @@ void usesCustomConnectionDetailsWhenDefined() { }); } + @Test + @ClassPathOverrides("org.crac:crac:1.3.0") + void whenCheckpointRestoreIsAvailableHikariAutoConfigRegistersLifecycleBean() { + this.contextRunner.withPropertyValues("spring.datasource.type=" + HikariDataSource.class.getName()) + .run((context) -> assertThat(context).hasSingleBean(HikariCheckpointRestoreLifecycle.class)); + } + + @Test + void whenCheckpointRestoreIsNotAvailableHikariAutoConfigDoesNotRegisterLifecycleBean() { + this.contextRunner.withPropertyValues("spring.datasource.type=" + HikariDataSource.class.getName()) + .run((context) -> assertThat(context).doesNotHaveBean(HikariCheckpointRestoreLifecycle.class)); + } + @Configuration(proxyBeanMethods = false) static class ConnectionDetailsConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariLifecycleTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariLifecycleTests.java deleted file mode 100644 index 4fea72556240..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariLifecycleTests.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2023 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.jdbc; - -import java.util.UUID; - -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -/** - * Tests for {@link HikariLifecycle}. - * - * @author Christoph Strobl - */ -class HikariLifecycleTests { - - @Test - void stopStartHikariDataSource() { - - HikariLifecycle hikariLifecycle = createLifecycle(); - - assertThat(hikariLifecycle.isRunning()).isTrue(); - - hikariLifecycle.stop(); - - assertThat(hikariLifecycle.getManagedInstance().isRunning()).isFalse(); - assertThat(hikariLifecycle.getManagedInstance().isClosed()).isFalse(); - assertThat(hikariLifecycle.isRunning()).isFalse(); - assertThat(hikariLifecycle.getManagedInstance().getHikariPoolMXBean().getTotalConnections()).isZero(); - - hikariLifecycle.start(); - - assertThat(hikariLifecycle.getManagedInstance().isRunning()).isTrue(); - assertThat(hikariLifecycle.getManagedInstance().isClosed()).isFalse(); - assertThat(hikariLifecycle.isRunning()).isTrue(); - } - - @Test - void cannotStartClosedDataSource() { - - HikariLifecycle hikariLifecycle = createLifecycle(); - hikariLifecycle.getManagedInstance().close(); - - assertThatExceptionOfType(RuntimeException.class).isThrownBy(hikariLifecycle::start); - } - - HikariLifecycle createLifecycle() { - - HikariConfig config = new HikariConfig(); - config.setAllowPoolSuspension(true); - config.setJdbcUrl("jdbc:hsqldb:mem:test-" + UUID.randomUUID()); - config.setPoolName("lifecycle-tests"); - - HikariDataSource source = new HikariDataSource(config); - return new HikariLifecycle(source); - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/HikariCheckpointRestoreLifecycle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/HikariCheckpointRestoreLifecycle.java new file mode 100644 index 000000000000..4f458e9f5af5 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/HikariCheckpointRestoreLifecycle.java @@ -0,0 +1,149 @@ +/* + * Copyright 2012-2023 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.jdbc; + +import java.lang.reflect.Field; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; + +import com.zaxxer.hikari.HikariConfigMXBean; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.HikariPoolMXBean; +import com.zaxxer.hikari.pool.HikariPool; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.Lifecycle; +import org.springframework.core.log.LogMessage; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * {@link Lifecycle} for a {@link HikariDataSource} allowing it to participate in + * checkpoint-restore. When {@link #stop() stopped}, and the data source + * {@link HikariDataSource#isAllowPoolSuspension() allows it}, its pool is suspended, + * blocking any attempts to borrow connections. Open and idle connections are then + * evicted. When subsequently {@link #start() started}, the pool is + * {@link HikariPoolMXBean#resumePool() resumed} if necessary. + * + * @author Christoph Strobl + * @author Andy Wilkinson + * @since 3.2.0 + */ +public class HikariCheckpointRestoreLifecycle implements Lifecycle { + + private static final Log logger = LogFactory.getLog(HikariCheckpointRestoreLifecycle.class); + + private static final Field CLOSE_CONNECTION_EXECUTOR; + + static { + Field closeConnectionExecutor = ReflectionUtils.findField(HikariPool.class, "closeConnectionExecutor"); + Assert.notNull(closeConnectionExecutor, "Unable to locate closeConnectionExecutor for HikariPool"); + Assert.isAssignable(ThreadPoolExecutor.class, closeConnectionExecutor.getType(), + "Expected ThreadPoolExecutor for closeConnectionExecutor but found %s" + .formatted(closeConnectionExecutor.getType())); + ReflectionUtils.makeAccessible(closeConnectionExecutor); + CLOSE_CONNECTION_EXECUTOR = closeConnectionExecutor; + } + + private final Function hasOpenConnections; + + private final HikariDataSource dataSource; + + /** + * Creates a new {@code HikariCheckpointRestoreLifecycle} that will allow the given + * {@code dataSource} to participate in checkpoint-restore. + * @param dataSource the checkpoint-restore participant + */ + public HikariCheckpointRestoreLifecycle(HikariDataSource dataSource) { + this.dataSource = DataSourceUnwrapper.unwrap(dataSource, HikariConfigMXBean.class, HikariDataSource.class); + this.hasOpenConnections = (pool) -> { + ThreadPoolExecutor closeConnectionExecutor = (ThreadPoolExecutor) ReflectionUtils + .getField(CLOSE_CONNECTION_EXECUTOR, pool); + Assert.notNull(closeConnectionExecutor, "CloseConnectionExecutor was null"); + return closeConnectionExecutor.getActiveCount() > 0; + }; + } + + @Override + public void start() { + if (this.dataSource.isRunning()) { + return; + } + Assert.state(!this.dataSource.isClosed(), "DataSource has been closed and cannot be restarted"); + if (this.dataSource.isAllowPoolSuspension()) { + logger.info("Resuming Hikari pool"); + this.dataSource.getHikariPoolMXBean().resumePool(); + } + } + + @Override + public void stop() { + if (!this.dataSource.isRunning()) { + return; + } + if (this.dataSource.isAllowPoolSuspension()) { + logger.info("Suspending Hikari pool"); + this.dataSource.getHikariPoolMXBean().suspendPool(); + } + closeConnections(Duration.ofMillis(this.dataSource.getConnectionTimeout() + 250)); + } + + private void closeConnections(Duration shutdownTimeout) { + logger.info("Evicting Hikari connections"); + this.dataSource.getHikariPoolMXBean().softEvictConnections(); + logger.debug("Waiting for Hikari connections to be closed"); + CompletableFuture allConnectionsClosed = CompletableFuture.runAsync(this::waitForConnectionsToClose); + try { + allConnectionsClosed.get(shutdownTimeout.toMillis(), TimeUnit.MILLISECONDS); + logger.debug("Hikari connections closed"); + } + catch (InterruptedException ex) { + logger.warn("Interrupted while waiting for connections to be closed", ex); + Thread.currentThread().interrupt(); + } + catch (TimeoutException ex) { + logger.warn(LogMessage.format("Hikari connections could not be closed within %s", shutdownTimeout), ex); + } + catch (ExecutionException ex) { + throw new RuntimeException("Failed to close Hikari connections", ex); + } + } + + private void waitForConnectionsToClose() { + while (this.hasOpenConnections.apply((HikariPool) this.dataSource.getHikariPoolMXBean())) { + try { + TimeUnit.MILLISECONDS.sleep(50); + } + catch (InterruptedException ex) { + logger.error("Interrupted while waiting for datasource connections to be closed", ex); + Thread.currentThread().interrupt(); + } + } + } + + @Override + public boolean isRunning() { + return this.dataSource.isRunning(); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/HikariCheckpointRestoreLifecycleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/HikariCheckpointRestoreLifecycleTests.java new file mode 100644 index 000000000000..05811c5d593d --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/HikariCheckpointRestoreLifecycleTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-2023 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.jdbc; + +import java.util.UUID; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; + +/** + * Tests for {@link HikariCheckpointRestoreLifecycle}. + * + * @author Christoph Strobl + * @author Andy Wilkinson + */ +class HikariCheckpointRestoreLifecycleTests { + + private final HikariCheckpointRestoreLifecycle lifecycle; + + private final HikariDataSource dataSource; + + HikariCheckpointRestoreLifecycleTests() { + HikariConfig config = new HikariConfig(); + config.setAllowPoolSuspension(true); + config.setJdbcUrl("jdbc:hsqldb:mem:test-" + UUID.randomUUID()); + config.setPoolName("lifecycle-tests"); + this.dataSource = new HikariDataSource(config); + this.lifecycle = new HikariCheckpointRestoreLifecycle(this.dataSource); + } + + @Test + void startedWhenStartedShouldSucceed() { + assertThat(this.lifecycle.isRunning()).isTrue(); + this.lifecycle.start(); + assertThat(this.lifecycle.isRunning()).isTrue(); + } + + @Test + void stopWhenStoppedShouldSucceed() { + assertThat(this.lifecycle.isRunning()).isTrue(); + this.lifecycle.stop(); + assertThat(this.dataSource.isRunning()).isFalse(); + assertThatNoException().isThrownBy(this.lifecycle::stop); + } + + @Test + void whenStoppedAndStartedDataSourceShouldPauseAndResume() { + assertThat(this.lifecycle.isRunning()).isTrue(); + this.lifecycle.stop(); + assertThat(this.dataSource.isRunning()).isFalse(); + assertThat(this.dataSource.isClosed()).isFalse(); + assertThat(this.lifecycle.isRunning()).isFalse(); + assertThat(this.dataSource.getHikariPoolMXBean().getTotalConnections()).isZero(); + this.lifecycle.start(); + assertThat(this.dataSource.isRunning()).isTrue(); + assertThat(this.dataSource.isClosed()).isFalse(); + assertThat(this.lifecycle.isRunning()).isTrue(); + } + + @Test + void whenDataSourceIsClosedThenStartShouldThrow() { + this.dataSource.close(); + assertThatExceptionOfType(RuntimeException.class).isThrownBy(this.lifecycle::start); + } + +} From e7b9984f4814420c6432097d5c1745ad31265ee2 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 25 Jul 2023 20:47:06 +0200 Subject: [PATCH 0190/1215] Upgrade to Spring AMQP 3.0.7 Closes gh-36573 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 34856ae4a7a8..cfcbb99835bf 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1369,7 +1369,7 @@ bom { ] } } - library("Spring AMQP", "3.0.6") { + library("Spring AMQP", "3.0.7") { group("org.springframework.amqp") { imports = [ "spring-amqp-bom" From 96c9915f127a85a1048004596e9d4b16e942f4cc Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Tue, 25 Jul 2023 22:14:50 +0900 Subject: [PATCH 0191/1215] Polish See gh-36565 --- ...CassandraDataAutoConfigurationIntegrationTests.java | 2 +- .../src/docs/asciidoc/actuator/observability.adoc | 10 +++++----- .../cassandra/DataCassandraTestIntegrationTests.java | 2 +- ...CassandraTestWithIncludeFilterIntegrationTests.java | 2 +- .../boot/test/mock/mockito/MockitoPostProcessor.java | 1 - .../boot/web/embedded/tomcat/TomcatWebServer.java | 1 + .../SampleCassandraApplicationReactiveSslTests.java | 2 +- .../cassandra/SampleCassandraApplicationSslTests.java | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfigurationIntegrationTests.java index aacd79fe8d6d..cd65a45a6421 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/cassandra/CassandraDataAutoConfigurationIntegrationTests.java @@ -79,7 +79,7 @@ static class KeyspaceTestConfiguration { CqlSession cqlSession(CqlSessionBuilder cqlSessionBuilder) { try (CqlSession session = cqlSessionBuilder.build()) { session.execute("CREATE KEYSPACE IF NOT EXISTS boot_test" - + " WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };"); + + " WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };"); } return cqlSessionBuilder.withKeyspace("boot_test").build(); } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc index 29c42af3743c..742e83f92e89 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc @@ -26,7 +26,7 @@ To enable it, add the `io.r2dbc:r2dbc-proxy` dependency to your project. [[actuator.observability.common-key-values]] === Common Key-Values Common key-values are generally used for dimensional drill-down on the operating environment, such as host, instance, region, stack, and others. -Commons key-values are applied to all observations as low cardinality key-values and can be configured, as the following example shows: +Common key-values are applied to all observations as low cardinality key-values and can be configured, as the following example shows: [source,yaml,indent=0,subs="verbatim",configprops,configblocks] ---- @@ -50,15 +50,15 @@ If you'd like to prevent some observations from being reported, you can use the observations: enable: denied: - prefix: false + prefix: false another: - denied: - prefix: false + denied: + prefix: false ---- The preceding example will prevent all observations with a name starting with `denied.prefix` or `another.denied.prefix`. -TIP: If you want to prevent Spring Security from reporting observations, set the property configprop:management.observations.enable.spring.security[] to `false`. +TIP: If you want to prevent Spring Security from reporting observations, set the property `management.observations.enable.spring.security` to `false`. If you need greater control over the prevention of observations, you can register beans of type `ObservationPredicate`. Observations are only reported if all the `ObservationPredicate` beans return `true` for that observation. diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTestIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTestIntegrationTests.java index c4cbf3cb6113..398c43fc661d 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTestIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTestIntegrationTests.java @@ -91,7 +91,7 @@ static class KeyspaceTestConfiguration { CqlSession cqlSession(CqlSessionBuilder cqlSessionBuilder) { try (CqlSession session = cqlSessionBuilder.build()) { session.execute("CREATE KEYSPACE IF NOT EXISTS boot_test" - + " WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };"); + + " WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };"); } return cqlSessionBuilder.withKeyspace("boot_test").build(); } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTestWithIncludeFilterIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTestWithIncludeFilterIntegrationTests.java index 05741e8a91de..7766fa22b3ff 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTestWithIncludeFilterIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/data/cassandra/DataCassandraTestWithIncludeFilterIntegrationTests.java @@ -77,7 +77,7 @@ static class KeyspaceTestConfiguration { CqlSession cqlSession(CqlSessionBuilder cqlSessionBuilder) { try (CqlSession session = cqlSessionBuilder.build()) { session.execute("CREATE KEYSPACE IF NOT EXISTS boot_test" - + " WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };"); + + " WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };"); } return cqlSessionBuilder.withKeyspace("boot_test").build(); } diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java index dfc97c91eaf1..6ccc03f142cd 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java @@ -263,7 +263,6 @@ private Set getExistingBeans(ConfigurableListableBeanFactory beanFactory } beans.removeIf(this::isScopedTarget); return beans; - } private boolean isScopedTarget(String beanName) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java index 86f3a0ff2c46..9290aeada33d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java @@ -344,6 +344,7 @@ public void stop() throws WebServerException { } } + @Override public void destroy() throws WebServerException { try { stopTomcat(); diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-cassandra/src/test/java/smoketest/data/cassandra/SampleCassandraApplicationReactiveSslTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-cassandra/src/test/java/smoketest/data/cassandra/SampleCassandraApplicationReactiveSslTests.java index 2362aa4e55d7..c8bfd7bdadb9 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-cassandra/src/test/java/smoketest/data/cassandra/SampleCassandraApplicationReactiveSslTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-cassandra/src/test/java/smoketest/data/cassandra/SampleCassandraApplicationReactiveSslTests.java @@ -80,7 +80,7 @@ static class KeyspaceTestConfiguration { CqlSession cqlSession(CqlSessionBuilder cqlSessionBuilder) { try (CqlSession session = cqlSessionBuilder.build()) { session.execute("CREATE KEYSPACE IF NOT EXISTS boot_test" - + " WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };"); + + " WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };"); } return cqlSessionBuilder.withKeyspace("boot_test").build(); } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-cassandra/src/test/java/smoketest/data/cassandra/SampleCassandraApplicationSslTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-cassandra/src/test/java/smoketest/data/cassandra/SampleCassandraApplicationSslTests.java index 46dd49437a7f..48080633ba60 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-cassandra/src/test/java/smoketest/data/cassandra/SampleCassandraApplicationSslTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-cassandra/src/test/java/smoketest/data/cassandra/SampleCassandraApplicationSslTests.java @@ -80,7 +80,7 @@ static class KeyspaceTestConfiguration { CqlSession cqlSession(CqlSessionBuilder cqlSessionBuilder) { try (CqlSession session = cqlSessionBuilder.build()) { session.execute("CREATE KEYSPACE IF NOT EXISTS boot_test" - + " WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };"); + + " WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };"); } return cqlSessionBuilder.withKeyspace("boot_test").build(); } From b1ac64c7e2f585675273d09fd53db56ff015e465 Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Mon, 24 Jul 2023 16:03:19 +0800 Subject: [PATCH 0192/1215] Harmonize Stream.collect() usage use Stream.toList() or collect(Collectors.toSet()) where possible. See gh-36509 --- .../bom/bomr/StandardLibraryUpdateResolver.java | 13 +++---------- .../sonatype/SonatypeServiceTests.java | 5 ++--- .../org/springframework/boot/util/Instantiator.java | 6 +----- 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/StandardLibraryUpdateResolver.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/StandardLibraryUpdateResolver.java index bcfb1406c5f5..62c8aa9e86ca 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/StandardLibraryUpdateResolver.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/StandardLibraryUpdateResolver.java @@ -19,12 +19,10 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.SortedSet; -import java.util.stream.Collectors; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.slf4j.Logger; @@ -98,19 +96,14 @@ private List determineResolvedVersionOptions(Library library) { getLaterVersionsForModule(group.getId(), plugin, libraryVersion)); } } - List allVersions = moduleVersions.values() + return moduleVersions.values() .stream() .flatMap(SortedSet::stream) .distinct() .filter((dependencyVersion) -> isPermitted(dependencyVersion, library.getProhibitedVersions())) - .toList(); - if (allVersions.isEmpty()) { - return Collections.emptyList(); - } - return allVersions.stream() - .map((version) -> new VersionOption.ResolvedVersionOption(version, + .map((version) -> (VersionOption) new VersionOption.ResolvedVersionOption(version, getMissingModules(moduleVersions, version))) - .collect(Collectors.toList()); + .toList(); } private boolean isPermitted(DependencyVersion dependencyVersion, List prohibitedVersions) { diff --git a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/sonatype/SonatypeServiceTests.java b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/sonatype/SonatypeServiceTests.java index 6c9133c7c677..ef949ac8b0d4 100644 --- a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/sonatype/SonatypeServiceTests.java +++ b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/sonatype/SonatypeServiceTests.java @@ -21,7 +21,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; -import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.stream.Collectors; @@ -105,7 +104,7 @@ void publishWithSuccessfulClose() throws IOException { .filter((artifact) -> !artifact.startsWith("build-info.json")) .map((artifact) -> requestTo( "/service/local/staging/deployByRepositoryId/example-6789/" + artifact.toString())) - .collect(Collectors.toCollection(HashSet::new)); + .collect(Collectors.toSet()); AnyOfRequestMatcher uploadRequestsMatcher = anyOf(uploads); assertThat(uploadRequestsMatcher.candidates).hasSize(150); this.server.expect(ExpectedCount.times(150), uploadRequestsMatcher).andExpect(method(HttpMethod.PUT)) @@ -157,7 +156,7 @@ void publishWithCloseFailureDueToRuleViolations() throws IOException { .filter((artifact) -> !"build-info.json".equals(artifact.toString())) .map((artifact) -> requestTo( "/service/local/staging/deployByRepositoryId/example-6789/" + artifact.toString())) - .collect(Collectors.toCollection(HashSet::new)); + .collect(Collectors.toSet()); AnyOfRequestMatcher uploadRequestsMatcher = anyOf(uploads); assertThat(uploadRequestsMatcher.candidates).hasSize(150); this.server.expect(ExpectedCount.times(150), uploadRequestsMatcher).andExpect(method(HttpMethod.PUT)) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/util/Instantiator.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/util/Instantiator.java index af23e1111559..05eb183cc679 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/util/Instantiator.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/util/Instantiator.java @@ -17,7 +17,6 @@ package org.springframework.boot.util; import java.lang.reflect.Constructor; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -28,7 +27,6 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; -import java.util.stream.Collectors; import java.util.stream.Stream; import org.springframework.core.annotation.AnnotationAwareOrderComparator; @@ -139,9 +137,7 @@ public List instantiateTypes(Collection> types) { } private List instantiate(Stream typeSuppliers) { - List instances = typeSuppliers.map(this::instantiate).collect(Collectors.toCollection(ArrayList::new)); - AnnotationAwareOrderComparator.sort(instances); - return Collections.unmodifiableList(instances); + return typeSuppliers.map(this::instantiate).sorted(AnnotationAwareOrderComparator.INSTANCE).toList(); } private T instantiate(TypeSupplier typeSupplier) { From 8b716a2f6cb6336168f2bde6940d83a5e1031a78 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 24 Jul 2023 16:00:29 -0400 Subject: [PATCH 0193/1215] Add RabbitMQ container forceStop property See gh-36539 --- ...bitListenerContainerFactoryConfigurer.java | 1 + .../autoconfigure/amqp/RabbitProperties.java | 14 +++++++++ .../amqp/RabbitAutoConfigurationTests.java | 30 +++++++++++++++++-- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractRabbitListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractRabbitListenerContainerFactoryConfigurer.java index feab224f2ca7..154ac99ceae0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractRabbitListenerContainerFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractRabbitListenerContainerFactoryConfigurer.java @@ -118,6 +118,7 @@ protected void configure(T factory, ConnectionFactory connectionFactory, } factory.setMissingQueuesFatal(configuration.isMissingQueuesFatal()); factory.setDeBatchingEnabled(configuration.isDeBatchingEnabled()); + factory.setForceStop(configuration.isForceStop()); ListenerRetry retryConfig = configuration.getRetry(); if (retryConfig.isEnabled()) { RetryInterceptorBuilder builder = (retryConfig.isStateless()) ? RetryInterceptorBuilder.stateless() diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java index 60e076ca56a3..4e95d7346bf9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java @@ -734,6 +734,12 @@ public abstract static class AmqpContainer extends BaseContainer { */ private boolean deBatchingEnabled = true; + /** + * Whether the container (when stopped) should stop immediately after processing + * the current message or stop after processing all pre-fetched messages. + */ + private boolean forceStop; + /** * Optional properties for a retry interceptor. */ @@ -781,6 +787,14 @@ public void setDeBatchingEnabled(boolean deBatchingEnabled) { this.deBatchingEnabled = deBatchingEnabled; } + public boolean isForceStop() { + return this.forceStop; + } + + public void setForceStop(boolean forceStop) { + this.forceStop = forceStop; + } + public ListenerRetry getRetry() { return this.retry; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java index a53b482e60ca..363a4153b8d7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java @@ -519,7 +519,8 @@ void testSimpleRabbitListenerContainerFactoryWithCustomSettings() { "spring.rabbitmq.listener.simple.defaultRequeueRejected:false", "spring.rabbitmq.listener.simple.idleEventInterval:5", "spring.rabbitmq.listener.simple.batchSize:20", - "spring.rabbitmq.listener.simple.missingQueuesFatal:false") + "spring.rabbitmq.listener.simple.missingQueuesFatal:false", + "spring.rabbitmq.listener.simple.force-stop:true") .run((context) -> { SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory = context .getBean("rabbitListenerContainerFactory", SimpleRabbitListenerContainerFactory.class); @@ -531,6 +532,17 @@ void testSimpleRabbitListenerContainerFactoryWithCustomSettings() { }); } + @Test + void testSimpleRabbitListenerContainerFactoryWithDefaultForceStop() { + this.contextRunner + .withUserConfiguration(MessageConvertersConfiguration.class, MessageRecoverersConfiguration.class) + .run((context) -> { + SimpleRabbitListenerContainerFactory containerFactory = context + .getBean("rabbitListenerContainerFactory", SimpleRabbitListenerContainerFactory.class); + assertThat(containerFactory).hasFieldOrPropertyWithValue("forceStop", false); + }); + } + @Test void testDirectRabbitListenerContainerFactoryWithCustomSettings() { this.contextRunner @@ -547,7 +559,8 @@ void testDirectRabbitListenerContainerFactoryWithCustomSettings() { "spring.rabbitmq.listener.direct.prefetch:40", "spring.rabbitmq.listener.direct.defaultRequeueRejected:false", "spring.rabbitmq.listener.direct.idleEventInterval:5", - "spring.rabbitmq.listener.direct.missingQueuesFatal:true") + "spring.rabbitmq.listener.direct.missingQueuesFatal:true", + "spring.rabbitmq.listener.direct.force-stop:true") .run((context) -> { DirectRabbitListenerContainerFactory rabbitListenerContainerFactory = context .getBean("rabbitListenerContainerFactory", DirectRabbitListenerContainerFactory.class); @@ -557,6 +570,18 @@ void testDirectRabbitListenerContainerFactoryWithCustomSettings() { }); } + @Test + void testDirectRabbitListenerContainerFactoryWithDefaultForceStop() { + this.contextRunner + .withUserConfiguration(MessageConvertersConfiguration.class, MessageRecoverersConfiguration.class) + .withPropertyValues("spring.rabbitmq.listener.type:direct") + .run((context) -> { + DirectRabbitListenerContainerFactory containerFactory = context + .getBean("rabbitListenerContainerFactory", DirectRabbitListenerContainerFactory.class); + assertThat(containerFactory).hasFieldOrPropertyWithValue("forceStop", false); + }); + } + @Test void testSimpleRabbitListenerContainerFactoryRetryWithCustomizer() { this.contextRunner.withUserConfiguration(RabbitRetryTemplateCustomizerConfiguration.class) @@ -662,6 +687,7 @@ private void checkCommonProps(AssertableApplicationContext context, context.getBean("myMessageConverter")); assertThat(containerFactory).hasFieldOrPropertyWithValue("defaultRequeueRejected", Boolean.FALSE); assertThat(containerFactory).hasFieldOrPropertyWithValue("idleEventInterval", 5L); + assertThat(containerFactory).hasFieldOrPropertyWithValue("forceStop", true); Advice[] adviceChain = containerFactory.getAdviceChain(); assertThat(adviceChain).isNotNull(); assertThat(adviceChain).hasSize(1); From 54066791f99206f7234d744c95a6b199cb8c8daf Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 26 Jul 2023 11:20:36 +0200 Subject: [PATCH 0194/1215] Polish "Add RabbitMQ container forceStop property" See gh-36539 --- .../boot/autoconfigure/amqp/RabbitPropertiesTests.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitPropertiesTests.java index 998f5a88d9c6..61e89c08222a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitPropertiesTests.java @@ -322,6 +322,7 @@ void simpleContainerUseConsistentDefaultValues() { assertThat(container).hasFieldOrPropertyWithValue("missingQueuesFatal", simple.isMissingQueuesFatal()); assertThat(container).hasFieldOrPropertyWithValue("deBatchingEnabled", simple.isDeBatchingEnabled()); assertThat(container).hasFieldOrPropertyWithValue("consumerBatchEnabled", simple.isConsumerBatchEnabled()); + assertThat(container).hasFieldOrPropertyWithValue("forceStop", simple.isForceStop()); } @Test @@ -332,6 +333,7 @@ void directContainerUseConsistentDefaultValues() { assertThat(direct.isAutoStartup()).isEqualTo(container.isAutoStartup()); assertThat(container).hasFieldOrPropertyWithValue("missingQueuesFatal", direct.isMissingQueuesFatal()); assertThat(container).hasFieldOrPropertyWithValue("deBatchingEnabled", direct.isDeBatchingEnabled()); + assertThat(container).hasFieldOrPropertyWithValue("forceStop", direct.isForceStop()); } @Test From f840141652a4ecf6455ebd3805b6262ddd06ebf8 Mon Sep 17 00:00:00 2001 From: Leo Li <269739606@qq.com> Date: Wed, 26 Jan 2022 10:56:50 +0800 Subject: [PATCH 0195/1215] Allow custom RSocket WebsocketServerSpecs to be defined See gh-29567 --- .../rsocket/RSocketProperties.java | 68 +++++++++++++++++++ .../RSocketServerAutoConfiguration.java | 2 +- .../RSocketWebSocketNettyRouteProvider.java | 14 +++- 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketProperties.java index f1921f23885c..c43895c47abf 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketProperties.java @@ -73,6 +73,8 @@ public static class Server { @NestedConfigurationProperty private Ssl ssl; + private Spec spec = new Spec(); + public Integer getPort() { return this.port; } @@ -121,6 +123,72 @@ public void setSsl(Ssl ssl) { this.ssl = ssl; } + public Spec getSpec() { + return this.spec; + } + + public void setSpec(Spec spec) { + this.spec = spec; + } + + public static class Spec { + + /** + * Sub-protocol to use in websocket handshake signature. + */ + private String protocols; + + /** + * Specifies a custom maximum allowable frame payload length. 65536 by + * default. + */ + private int maxFramePayloadLength = 65536; + + /** + * Flag whether to proxy websocket ping frames or respond to them. + */ + private boolean handlePing; + + /** + * Flag whether the websocket compression extension is enabled if the client + * request presents websocket extensions headers. + */ + private boolean compress; + + public String getProtocols() { + return this.protocols; + } + + public void setProtocols(String protocols) { + this.protocols = protocols; + } + + public int getMaxFramePayloadLength() { + return this.maxFramePayloadLength; + } + + public void setMaxFramePayloadLength(int maxFramePayloadLength) { + this.maxFramePayloadLength = maxFramePayloadLength; + } + + public boolean isHandlePing() { + return this.handlePing; + } + + public void setHandlePing(boolean handlePing) { + this.handlePing = handlePing; + } + + public boolean isCompress() { + return this.compress; + } + + public void setCompress(boolean compress) { + this.compress = compress; + } + + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfiguration.java index 96bbbc454ba2..6c5394d59044 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfiguration.java @@ -73,7 +73,7 @@ static class WebFluxServerConfiguration { RSocketWebSocketNettyRouteProvider rSocketWebsocketRouteProvider(RSocketProperties properties, RSocketMessageHandler messageHandler, ObjectProvider customizers) { return new RSocketWebSocketNettyRouteProvider(properties.getServer().getMappingPath(), - messageHandler.responder(), customizers.orderedStream()); + properties.getServer().getSpec(), messageHandler.responder(), customizers.orderedStream()); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketWebSocketNettyRouteProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketWebSocketNettyRouteProvider.java index 5cb8d7f374f5..9d9ddc91dd56 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketWebSocketNettyRouteProvider.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketWebSocketNettyRouteProvider.java @@ -24,7 +24,9 @@ import io.rsocket.transport.ServerTransport; import io.rsocket.transport.netty.server.WebsocketRouteTransport; import reactor.netty.http.server.HttpServerRoutes; +import reactor.netty.http.server.WebsocketServerSpec; +import org.springframework.boot.autoconfigure.rsocket.RSocketProperties.Server.Spec; import org.springframework.boot.rsocket.server.RSocketServerCustomizer; import org.springframework.boot.web.embedded.netty.NettyRouteProvider; @@ -32,6 +34,7 @@ * {@link NettyRouteProvider} that configures an RSocket Websocket endpoint. * * @author Brian Clozel + * @author Leo Li */ class RSocketWebSocketNettyRouteProvider implements NettyRouteProvider { @@ -41,11 +44,14 @@ class RSocketWebSocketNettyRouteProvider implements NettyRouteProvider { private final List customizers; - RSocketWebSocketNettyRouteProvider(String mappingPath, SocketAcceptor socketAcceptor, + private final Spec spec; + + RSocketWebSocketNettyRouteProvider(String mappingPath, Spec spec, SocketAcceptor socketAcceptor, Stream customizers) { this.mappingPath = mappingPath; this.socketAcceptor = socketAcceptor; this.customizers = customizers.toList(); + this.spec = spec; } @Override @@ -53,7 +59,11 @@ public HttpServerRoutes apply(HttpServerRoutes httpServerRoutes) { RSocketServer server = RSocketServer.create(this.socketAcceptor); this.customizers.forEach((customizer) -> customizer.customize(server)); ServerTransport.ConnectionAcceptor connectionAcceptor = server.asConnectionAcceptor(); - return httpServerRoutes.ws(this.mappingPath, WebsocketRouteTransport.newHandler(connectionAcceptor)); + WebsocketServerSpec.Builder build = (this.spec.getProtocols() == null) ? WebsocketServerSpec.builder() + : WebsocketServerSpec.builder().protocols(this.spec.getProtocols()); + return httpServerRoutes.ws(this.mappingPath, WebsocketRouteTransport.newHandler(connectionAcceptor), + build.maxFramePayloadLength(this.spec.getMaxFramePayloadLength()).handlePing(this.spec.isHandlePing()) + .compress(this.spec.isCompress()).build()); } } From b0438b0f034b1f501a26eba17dea8176e6fb6548 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 26 Jul 2023 13:42:17 +0200 Subject: [PATCH 0196/1215] Polish "Allow custom RSocket WebsocketServerSpecs to be defined" See gh-29567 --- .../rsocket/RSocketProperties.java | 22 ++++------ .../RSocketServerAutoConfiguration.java | 18 +++++++- .../RSocketWebSocketNettyRouteProvider.java | 24 ++++++----- .../rsocket/RSocketPropertiesTests.java | 43 +++++++++++++++++++ 4 files changed, 82 insertions(+), 25 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketPropertiesTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketProperties.java index c43895c47abf..dd16caad7198 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. @@ -73,7 +73,7 @@ public static class Server { @NestedConfigurationProperty private Ssl ssl; - private Spec spec = new Spec(); + private final Spec spec = new Spec(); public Integer getPort() { return this.port; @@ -127,10 +127,6 @@ public Spec getSpec() { return this.spec; } - public void setSpec(Spec spec) { - this.spec = spec; - } - public static class Spec { /** @@ -139,19 +135,17 @@ public static class Spec { private String protocols; /** - * Specifies a custom maximum allowable frame payload length. 65536 by - * default. + * Maximum allowable frame payload length. */ - private int maxFramePayloadLength = 65536; + private DataSize maxFramePayloadLength = DataSize.ofBytes(65536); /** - * Flag whether to proxy websocket ping frames or respond to them. + * Whether to proxy websocket ping frames or respond to them. */ private boolean handlePing; /** - * Flag whether the websocket compression extension is enabled if the client - * request presents websocket extensions headers. + * Whether the websocket compression extension is enabled. */ private boolean compress; @@ -163,11 +157,11 @@ public void setProtocols(String protocols) { this.protocols = protocols; } - public int getMaxFramePayloadLength() { + public DataSize getMaxFramePayloadLength() { return this.maxFramePayloadLength; } - public void setMaxFramePayloadLength(int maxFramePayloadLength) { + public void setMaxFramePayloadLength(DataSize maxFramePayloadLength) { this.maxFramePayloadLength = maxFramePayloadLength; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfiguration.java index 6c5394d59044..e329ef099e37 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfiguration.java @@ -16,10 +16,13 @@ package org.springframework.boot.autoconfigure.rsocket; +import java.util.function.Consumer; + import io.rsocket.core.RSocketServer; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.transport.netty.server.TcpServerTransport; import reactor.netty.http.server.HttpServer; +import reactor.netty.http.server.WebsocketServerSpec.Builder; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -31,6 +34,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.reactor.netty.ReactorNettyConfigurations; +import org.springframework.boot.autoconfigure.rsocket.RSocketProperties.Server.Spec; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.rsocket.context.RSocketServerBootstrap; @@ -46,6 +50,7 @@ import org.springframework.http.client.reactive.ReactorResourceFactory; import org.springframework.messaging.rsocket.RSocketStrategies; import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler; +import org.springframework.util.unit.DataSize; /** * {@link EnableAutoConfiguration Auto-configuration} for RSocket servers. In the case of @@ -73,7 +78,18 @@ static class WebFluxServerConfiguration { RSocketWebSocketNettyRouteProvider rSocketWebsocketRouteProvider(RSocketProperties properties, RSocketMessageHandler messageHandler, ObjectProvider customizers) { return new RSocketWebSocketNettyRouteProvider(properties.getServer().getMappingPath(), - properties.getServer().getSpec(), messageHandler.responder(), customizers.orderedStream()); + messageHandler.responder(), customizeWebsocketServerSpec(properties.getServer().getSpec()), + customizers.orderedStream()); + } + + private Consumer customizeWebsocketServerSpec(Spec spec) { + return (builder) -> { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(spec.getProtocols()).to(builder::protocols); + map.from(spec.getMaxFramePayloadLength()).asInt(DataSize::toBytes).to(builder::maxFramePayloadLength); + map.from(spec.isHandlePing()).to(builder::handlePing); + map.from(spec.isCompress()).to(builder::compress); + }; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketWebSocketNettyRouteProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketWebSocketNettyRouteProvider.java index 9d9ddc91dd56..f70f9d41d546 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketWebSocketNettyRouteProvider.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketWebSocketNettyRouteProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.rsocket; import java.util.List; +import java.util.function.Consumer; import java.util.stream.Stream; import io.rsocket.SocketAcceptor; @@ -25,8 +26,8 @@ import io.rsocket.transport.netty.server.WebsocketRouteTransport; import reactor.netty.http.server.HttpServerRoutes; import reactor.netty.http.server.WebsocketServerSpec; +import reactor.netty.http.server.WebsocketServerSpec.Builder; -import org.springframework.boot.autoconfigure.rsocket.RSocketProperties.Server.Spec; import org.springframework.boot.rsocket.server.RSocketServerCustomizer; import org.springframework.boot.web.embedded.netty.NettyRouteProvider; @@ -44,14 +45,14 @@ class RSocketWebSocketNettyRouteProvider implements NettyRouteProvider { private final List customizers; - private final Spec spec; + private final Consumer serverSpecCustomizer; - RSocketWebSocketNettyRouteProvider(String mappingPath, Spec spec, SocketAcceptor socketAcceptor, - Stream customizers) { + RSocketWebSocketNettyRouteProvider(String mappingPath, SocketAcceptor socketAcceptor, + Consumer serverSpecCustomizer, Stream customizers) { this.mappingPath = mappingPath; this.socketAcceptor = socketAcceptor; + this.serverSpecCustomizer = serverSpecCustomizer; this.customizers = customizers.toList(); - this.spec = spec; } @Override @@ -59,11 +60,14 @@ public HttpServerRoutes apply(HttpServerRoutes httpServerRoutes) { RSocketServer server = RSocketServer.create(this.socketAcceptor); this.customizers.forEach((customizer) -> customizer.customize(server)); ServerTransport.ConnectionAcceptor connectionAcceptor = server.asConnectionAcceptor(); - WebsocketServerSpec.Builder build = (this.spec.getProtocols() == null) ? WebsocketServerSpec.builder() - : WebsocketServerSpec.builder().protocols(this.spec.getProtocols()); return httpServerRoutes.ws(this.mappingPath, WebsocketRouteTransport.newHandler(connectionAcceptor), - build.maxFramePayloadLength(this.spec.getMaxFramePayloadLength()).handlePing(this.spec.isHandlePing()) - .compress(this.spec.isCompress()).build()); + createWebsocketServerSpec()); + } + + private WebsocketServerSpec createWebsocketServerSpec() { + WebsocketServerSpec.Builder builder = WebsocketServerSpec.builder(); + this.serverSpecCustomizer.accept(builder); + return builder.build(); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketPropertiesTests.java new file mode 100644 index 000000000000..eefd1b7212de --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketPropertiesTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2023 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.rsocket; + +import org.junit.jupiter.api.Test; +import reactor.netty.http.server.WebsocketServerSpec; + +import org.springframework.boot.autoconfigure.rsocket.RSocketProperties.Server.Spec; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link RSocketProperties}. + * + * @author Stephane Nicoll + */ +class RSocketPropertiesTests { + + @Test + void defaultServerSpecValuesAreConsistent() { + WebsocketServerSpec spec = WebsocketServerSpec.builder().build(); + Spec properties = new RSocketProperties().getServer().getSpec(); + assertThat(properties.getProtocols()).isEqualTo(spec.protocols()); + assertThat(properties.getMaxFramePayloadLength().toBytes()).isEqualTo(spec.maxFramePayloadLength()); + assertThat(properties.isHandlePing()).isEqualTo(spec.handlePing()); + assertThat(properties.isCompress()).isEqualTo(spec.compress()); + } + +} From 49ae8c0998f7990642085a848b45ad42b12f73c2 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 26 Jul 2023 11:56:54 +0200 Subject: [PATCH 0197/1215] Auto-configure Kafka's threadNameSupplier Closes gh-36344 --- ...fkaListenerContainerFactoryConfigurer.java | 13 ++++ .../KafkaAnnotationDrivenConfiguration.java | 10 ++- ...stenerContainerFactoryConfigurerTests.java | 63 +++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurerTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java index 8ea525a151b5..e842936da04f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.kafka; import java.time.Duration; +import java.util.function.Function; import org.springframework.boot.autoconfigure.kafka.KafkaProperties.Listener; import org.springframework.boot.context.properties.PropertyMapper; @@ -28,6 +29,7 @@ import org.springframework.kafka.listener.CommonErrorHandler; import org.springframework.kafka.listener.ConsumerAwareRebalanceListener; import org.springframework.kafka.listener.ContainerProperties; +import org.springframework.kafka.listener.MessageListenerContainer; import org.springframework.kafka.listener.RecordInterceptor; import org.springframework.kafka.listener.adapter.RecordFilterStrategy; import org.springframework.kafka.support.converter.BatchMessageConverter; @@ -66,6 +68,8 @@ public class ConcurrentKafkaListenerContainerFactoryConfigurer { private BatchInterceptor batchInterceptor; + private Function threadNameSupplier; + /** * Set the {@link KafkaProperties} to use. * @param properties the properties @@ -156,6 +160,14 @@ void setBatchInterceptor(BatchInterceptor batchInterceptor) { this.batchInterceptor = batchInterceptor; } + /** + * Set the thread name supplier to use. + * @param threadNameSupplier the thread name supplier to use + */ + void setThreadNameSupplier(Function threadNameSupplier) { + this.threadNameSupplier = threadNameSupplier; + } + /** * Configure the specified Kafka listener container factory. The factory can be * further tuned and default settings can be overridden. @@ -186,6 +198,7 @@ private void configureListenerFactory(ConcurrentKafkaListenerContainerFactory batchInterceptor; + private final Function threadNameSupplier; + KafkaAnnotationDrivenConfiguration(KafkaProperties properties, ObjectProvider recordMessageConverter, ObjectProvider> recordFilterStrategy, @@ -83,7 +88,8 @@ class KafkaAnnotationDrivenConfiguration { ObjectProvider commonErrorHandler, ObjectProvider> afterRollbackProcessor, ObjectProvider> recordInterceptor, - ObjectProvider> batchInterceptor) { + ObjectProvider> batchInterceptor, + ObjectProvider> threadNameSupplier) { this.properties = properties; this.recordMessageConverter = recordMessageConverter.getIfUnique(); this.recordFilterStrategy = recordFilterStrategy.getIfUnique(); @@ -96,6 +102,7 @@ class KafkaAnnotationDrivenConfiguration { this.afterRollbackProcessor = afterRollbackProcessor.getIfUnique(); this.recordInterceptor = recordInterceptor.getIfUnique(); this.batchInterceptor = batchInterceptor.getIfUnique(); + this.threadNameSupplier = threadNameSupplier.getIfUnique(); } @Bean @@ -113,6 +120,7 @@ ConcurrentKafkaListenerContainerFactoryConfigurer kafkaListenerContainerFactoryC configurer.setAfterRollbackProcessor(this.afterRollbackProcessor); configurer.setRecordInterceptor(this.recordInterceptor); configurer.setBatchInterceptor(this.batchInterceptor); + configurer.setThreadNameSupplier(this.threadNameSupplier); return configurer; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurerTests.java new file mode 100644 index 000000000000..2bac853b2e2b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurerTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2023 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.kafka; + +import java.util.function.Function; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.core.ConsumerFactory; +import org.springframework.kafka.listener.MessageListenerContainer; + +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +/** + * Tests for {@link ConcurrentKafkaListenerContainerFactoryConfigurer}. + * + * @author Moritz Halbritter + */ +class ConcurrentKafkaListenerContainerFactoryConfigurerTests { + + private ConcurrentKafkaListenerContainerFactoryConfigurer configurer; + + private ConcurrentKafkaListenerContainerFactory factory; + + private ConsumerFactory consumerFactory; + + @BeforeEach + @SuppressWarnings("unchecked") + void setUp() { + this.configurer = new ConcurrentKafkaListenerContainerFactoryConfigurer(); + this.configurer.setKafkaProperties(new KafkaProperties()); + this.factory = spy(new ConcurrentKafkaListenerContainerFactory<>()); + this.consumerFactory = mock(ConsumerFactory.class); + + } + + @Test + void shouldApplyThreadNameSupplier() { + Function function = (container) -> "thread-1"; + this.configurer.setThreadNameSupplier(function); + this.configurer.configure(this.factory, this.consumerFactory); + then(this.factory).should().setThreadNameSupplier(function); + } + +} From 9cb57637947b79d9e5fb812eb4e62851c49054de Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 26 Jul 2023 13:45:10 +0200 Subject: [PATCH 0198/1215] Add property to set changeConsumerThreadName for Kafka Closes gh-36343 --- ...entKafkaListenerContainerFactoryConfigurer.java | 1 + .../boot/autoconfigure/kafka/KafkaProperties.java | 14 ++++++++++++++ ...fkaListenerContainerFactoryConfigurerTests.java | 12 +++++++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java index e842936da04f..edd8f4a37c43 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java @@ -199,6 +199,7 @@ private void configureListenerFactory(ConcurrentKafkaListenerContainerFactory consumerFactory; + private KafkaProperties properties; + @BeforeEach @SuppressWarnings("unchecked") void setUp() { this.configurer = new ConcurrentKafkaListenerContainerFactoryConfigurer(); - this.configurer.setKafkaProperties(new KafkaProperties()); + this.properties = new KafkaProperties(); + this.configurer.setKafkaProperties(this.properties); this.factory = spy(new ConcurrentKafkaListenerContainerFactory<>()); this.consumerFactory = mock(ConsumerFactory.class); @@ -60,4 +63,11 @@ void shouldApplyThreadNameSupplier() { then(this.factory).should().setThreadNameSupplier(function); } + @Test + void shouldApplyChangeConsumerThreadName() { + this.properties.getListener().setChangeConsumerThreadName(true); + this.configurer.configure(this.factory, this.consumerFactory); + then(this.factory).should().setChangeConsumerThreadName(true); + } + } From b0615dd311ae89de5108149f4baacf7b25b1f0fe Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 25 Jul 2023 10:46:54 +0200 Subject: [PATCH 0199/1215] Split OpenTelemetry auto-configuration The OpenTelemetry bean is now configured in the OpenTelemetryAutoConfiguration. This method also applies SdkLoggerProvider and SdkMeterProvider. Additionally, the OpenTelemetry Resource is now a bean. Resource attributes can now be configured through properties The resourceAttributes in OtlpProperties have been deprecated in favor of the new one in OpenTelemetryProperties. Closes gh-36544 Closes gh-36545 --- .../OtlpMetricsExportAutoConfiguration.java | 14 +- .../metrics/export/otlp/OtlpProperties.java | 2 + .../otlp/OtlpPropertiesConfigAdapter.java | 11 +- .../OpenTelemetryAutoConfiguration.java | 77 +++++++++ .../OpenTelemetryProperties.java | 58 +++++++ .../opentelemetry/package-info.java | 20 +++ .../OpenTelemetryAutoConfiguration.java | 30 +--- ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../OtlpPropertiesConfigAdapterTests.java | 68 +++++--- .../OpenTelemetryAutoConfigurationTests.java | 157 ++++++++++++++++++ .../BaggagePropagationIntegrationTests.java | 21 ++- .../OpenTelemetryAutoConfigurationTests.java | 14 +- ...OtlpAutoConfigurationIntegrationTests.java | 9 +- .../docs/asciidoc/actuator/observability.adoc | 11 ++ 14 files changed, 417 insertions(+), 76 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryProperties.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/package-info.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java index 29e89c29e50a..5ee64e539ee4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -24,6 +24,7 @@ import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport; import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryProperties; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -36,6 +37,7 @@ * {@link EnableAutoConfiguration Auto-configuration} for exporting metrics to OTLP. * * @author Eddú Meléndez + * @author Moritz Halbritter * @since 3.0.0 */ @AutoConfiguration( @@ -44,19 +46,23 @@ @ConditionalOnBean(Clock.class) @ConditionalOnClass(OtlpMeterRegistry.class) @ConditionalOnEnabledMetricsExport("otlp") -@EnableConfigurationProperties(OtlpProperties.class) +@EnableConfigurationProperties({ OtlpProperties.class, OpenTelemetryProperties.class }) public class OtlpMetricsExportAutoConfiguration { private final OtlpProperties properties; - public OtlpMetricsExportAutoConfiguration(OtlpProperties properties) { + private final OpenTelemetryProperties openTelemetryProperties; + + public OtlpMetricsExportAutoConfiguration(OtlpProperties properties, + OpenTelemetryProperties openTelemetryProperties) { this.properties = properties; + this.openTelemetryProperties = openTelemetryProperties; } @Bean @ConditionalOnMissingBean public OtlpConfig otlpConfig() { - return new OtlpPropertiesConfigAdapter(this.properties); + return new OtlpPropertiesConfigAdapter(this.properties, this.openTelemetryProperties); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpProperties.java index b39faa557713..ba3cc1145b19 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpProperties.java @@ -23,6 +23,7 @@ import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.StepRegistryProperties; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; /** * {@link ConfigurationProperties @ConfigurationProperties} for configuring OTLP metrics @@ -77,6 +78,7 @@ public void setAggregationTemporality(AggregationTemporality aggregationTemporal this.aggregationTemporality = aggregationTemporality; } + @DeprecatedConfigurationProperty(replacement = "management.opentelemetry.resource-attributes") public Map getResourceAttributes() { return this.resourceAttributes; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java index e21455e80f44..2d225499cbb0 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java @@ -23,17 +23,23 @@ import io.micrometer.registry.otlp.OtlpConfig; import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.StepRegistryPropertiesConfigAdapter; +import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryProperties; +import org.springframework.util.CollectionUtils; /** * Adapter to convert {@link OtlpProperties} to an {@link OtlpConfig}. * * @author Eddú Meléndez * @author Jonatan Ivanov + * @author Moritz Halbritter */ class OtlpPropertiesConfigAdapter extends StepRegistryPropertiesConfigAdapter implements OtlpConfig { - OtlpPropertiesConfigAdapter(OtlpProperties properties) { + private final OpenTelemetryProperties openTelemetryProperties; + + OtlpPropertiesConfigAdapter(OtlpProperties properties, OpenTelemetryProperties openTelemetryProperties) { super(properties); + this.openTelemetryProperties = openTelemetryProperties; } @Override @@ -53,6 +59,9 @@ public AggregationTemporality aggregationTemporality() { @Override public Map resourceAttributes() { + if (!CollectionUtils.isEmpty(this.openTelemetryProperties.getResourceAttributes())) { + return this.openTelemetryProperties.getResourceAttributes(); + } return get(OtlpProperties::getResourceAttributes, OtlpConfig.super::resourceAttributes); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java new file mode 100644 index 000000000000..92bb18e374ad --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.opentelemetry; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.OpenTelemetrySdkBuilder; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for OpenTelemetry. + * + * @author Moritz Halbritter + * @since 3.2.0 + */ +@AutoConfiguration +@ConditionalOnClass(OpenTelemetrySdk.class) +@EnableConfigurationProperties(OpenTelemetryProperties.class) +public class OpenTelemetryAutoConfiguration { + + /** + * Default value for application name if {@code spring.application.name} is not set. + */ + private static final String DEFAULT_APPLICATION_NAME = "application"; + + @Bean + @ConditionalOnMissingBean(OpenTelemetry.class) + OpenTelemetrySdk openTelemetry(ObjectProvider tracerProvider, + ObjectProvider propagators, ObjectProvider loggerProvider, + ObjectProvider meterProvider) { + OpenTelemetrySdkBuilder builder = OpenTelemetrySdk.builder(); + tracerProvider.ifAvailable(builder::setTracerProvider); + propagators.ifAvailable(builder::setPropagators); + loggerProvider.ifAvailable(builder::setLoggerProvider); + meterProvider.ifAvailable(builder::setMeterProvider); + return builder.build(); + } + + @Bean + @ConditionalOnMissingBean + Resource openTelemetryResource(Environment environment, OpenTelemetryProperties properties) { + String applicationName = environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME); + return Resource.getDefault() + .merge(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, applicationName))) + .merge(properties.toResource()); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryProperties.java new file mode 100644 index 000000000000..4b2b68da0cdb --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryProperties.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.opentelemetry; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.ResourceBuilder; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for OpenTelemetry. + * + * @author Moritz Halbritter + * @since 3.2.0 + */ +@ConfigurationProperties(prefix = "management.opentelemetry") +public class OpenTelemetryProperties { + + /** + * Resource attributes. + */ + private Map resourceAttributes = new HashMap<>(); + + public Map getResourceAttributes() { + return this.resourceAttributes; + } + + public void setResourceAttributes(Map resourceAttributes) { + this.resourceAttributes = resourceAttributes; + } + + Resource toResource() { + ResourceBuilder builder = Resource.builder(); + for (Entry entry : this.resourceAttributes.entrySet()) { + builder.put(entry.getKey(), entry.getValue()); + } + return builder.build(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/package-info.java new file mode 100644 index 000000000000..c1aab18823c6 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Auto-configuration for OpenTelemetry. + */ +package org.springframework.boot.actuate.autoconfigure.opentelemetry; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java index adb79ab09b2d..92e75afc0761 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java @@ -36,13 +36,11 @@ import io.micrometer.tracing.otel.bridge.Slf4JEventListener; import io.micrometer.tracing.otel.propagation.BaggageTextMapPropagator; import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.ContextStorage; import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.context.propagation.TextMapPropagator; -import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; @@ -51,7 +49,6 @@ import io.opentelemetry.sdk.trace.export.BatchSpanProcessorBuilder; import io.opentelemetry.sdk.trace.export.SpanExporter; import io.opentelemetry.sdk.trace.samplers.Sampler; -import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.SpringBootVersion; @@ -63,26 +60,20 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.env.Environment; /** - * {@link EnableAutoConfiguration Auto-configuration} for OpenTelemetry. + * {@link EnableAutoConfiguration Auto-configuration} for OpenTelemetry tracing. * * @author Moritz Halbritter * @author Marcin Grzejszczak * @author Yanming Zhou * @since 3.0.0 */ -@AutoConfiguration(before = MicrometerTracingAutoConfiguration.class) +@AutoConfiguration(value = "openTelemetryTracingAutoConfiguration", before = MicrometerTracingAutoConfiguration.class) @ConditionalOnClass({ OtelTracer.class, SdkTracerProvider.class, OpenTelemetry.class }) @EnableConfigurationProperties(TracingProperties.class) public class OpenTelemetryAutoConfiguration { - /** - * Default value for application name if {@code spring.application.name} is not set. - */ - private static final String DEFAULT_APPLICATION_NAME = "application"; - private final TracingProperties tracingProperties; OpenTelemetryAutoConfiguration(TracingProperties tracingProperties) { @@ -91,22 +82,9 @@ public class OpenTelemetryAutoConfiguration { @Bean @ConditionalOnMissingBean - OpenTelemetry openTelemetry(SdkTracerProvider sdkTracerProvider, ContextPropagators contextPropagators) { - return OpenTelemetrySdk.builder() - .setTracerProvider(sdkTracerProvider) - .setPropagators(contextPropagators) - .build(); - } - - @Bean - @ConditionalOnMissingBean - SdkTracerProvider otelSdkTracerProvider(Environment environment, SpanProcessors spanProcessors, Sampler sampler, + SdkTracerProvider otelSdkTracerProvider(Resource resource, SpanProcessors spanProcessors, Sampler sampler, ObjectProvider customizers) { - String applicationName = environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME); - Resource springResource = Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, applicationName)); - SdkTracerProviderBuilder builder = SdkTracerProvider.builder() - .setSampler(sampler) - .setResource(Resource.getDefault().merge(springResource)); + SdkTracerProviderBuilder builder = SdkTracerProvider.builder().setSampler(sampler).setResource(resource); spanProcessors.forEach(builder::addSpanProcessor); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder.build(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index eb46381bf1a0..79c100062557 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -88,6 +88,7 @@ org.springframework.boot.actuate.autoconfigure.data.mongo.MongoReactiveHealthCon org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jHealthContributorAutoConfiguration org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration org.springframework.boot.actuate.autoconfigure.observation.web.servlet.WebMvcObservationAutoConfiguration +org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration org.springframework.boot.actuate.autoconfigure.quartz.QuartzEndpointAutoConfiguration org.springframework.boot.actuate.autoconfigure.r2dbc.ConnectionFactoryHealthContributorAutoConfiguration org.springframework.boot.actuate.autoconfigure.r2dbc.R2dbcObservationAutoConfiguration diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java index 87f527dcd197..5d6cbea3ffa3 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java @@ -16,69 +16,93 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.otlp; +import java.util.Collections; import java.util.Map; import java.util.concurrent.TimeUnit; import io.micrometer.registry.otlp.AggregationTemporality; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryProperties; + import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; /** * Tests for {@link OtlpPropertiesConfigAdapter}. * * @author Eddú Meléndez + * @author Moritz Halbritter */ class OtlpPropertiesConfigAdapterTests { + private OtlpProperties properties; + + private OpenTelemetryProperties openTelemetryProperties; + + @BeforeEach + void setUp() { + this.properties = new OtlpProperties(); + this.openTelemetryProperties = new OpenTelemetryProperties(); + } + @Test void whenPropertiesUrlIsSetAdapterUrlReturnsIt() { - OtlpProperties properties = new OtlpProperties(); - properties.setUrl("http://another-url:4318/v1/metrics"); - assertThat(new OtlpPropertiesConfigAdapter(properties).url()).isEqualTo("http://another-url:4318/v1/metrics"); + this.properties.setUrl("http://another-url:4318/v1/metrics"); + assertThat(createAdapter().url()).isEqualTo("http://another-url:4318/v1/metrics"); } @Test void whenPropertiesAggregationTemporalityIsNotSetAdapterAggregationTemporalityReturnsCumulative() { - OtlpProperties properties = new OtlpProperties(); - assertThat(new OtlpPropertiesConfigAdapter(properties).aggregationTemporality()) - .isSameAs(AggregationTemporality.CUMULATIVE); + assertThat(createAdapter().aggregationTemporality()).isSameAs(AggregationTemporality.CUMULATIVE); } @Test void whenPropertiesAggregationTemporalityIsSetAdapterAggregationTemporalityReturnsIt() { - OtlpProperties properties = new OtlpProperties(); - properties.setAggregationTemporality(AggregationTemporality.DELTA); - assertThat(new OtlpPropertiesConfigAdapter(properties).aggregationTemporality()) - .isSameAs(AggregationTemporality.DELTA); + this.properties.setAggregationTemporality(AggregationTemporality.DELTA); + assertThat(createAdapter().aggregationTemporality()).isSameAs(AggregationTemporality.DELTA); } @Test void whenPropertiesResourceAttributesIsSetAdapterResourceAttributesReturnsIt() { - OtlpProperties properties = new OtlpProperties(); - properties.setResourceAttributes(Map.of("service.name", "boot-service")); - assertThat(new OtlpPropertiesConfigAdapter(properties).resourceAttributes()).containsEntry("service.name", - "boot-service"); + this.properties.setResourceAttributes(Map.of("service.name", "boot-service")); + assertThat(createAdapter().resourceAttributes()).containsEntry("service.name", "boot-service"); } @Test void whenPropertiesHeadersIsSetAdapterHeadersReturnsIt() { - OtlpProperties properties = new OtlpProperties(); - properties.setHeaders(Map.of("header", "value")); - assertThat(new OtlpPropertiesConfigAdapter(properties).headers()).containsEntry("header", "value"); + this.properties.setHeaders(Map.of("header", "value")); + assertThat(createAdapter().headers()).containsEntry("header", "value"); } @Test void whenPropertiesBaseTimeUnitIsNotSetAdapterBaseTimeUnitReturnsMillis() { - OtlpProperties properties = new OtlpProperties(); - assertThat(new OtlpPropertiesConfigAdapter(properties).baseTimeUnit()).isSameAs(TimeUnit.MILLISECONDS); + assertThat(createAdapter().baseTimeUnit()).isSameAs(TimeUnit.MILLISECONDS); } @Test void whenPropertiesBaseTimeUnitIsSetAdapterBaseTimeUnitReturnsIt() { - OtlpProperties properties = new OtlpProperties(); - properties.setBaseTimeUnit(TimeUnit.SECONDS); - assertThat(new OtlpPropertiesConfigAdapter(properties).baseTimeUnit()).isSameAs(TimeUnit.SECONDS); + this.properties.setBaseTimeUnit(TimeUnit.SECONDS); + assertThat(createAdapter().baseTimeUnit()).isSameAs(TimeUnit.SECONDS); + } + + @Test + void openTelemetryPropertiesShouldOverrideOtlpPropertiesIfNotEmpty() { + this.properties.setResourceAttributes(Map.of("a", "alpha")); + this.openTelemetryProperties.setResourceAttributes(Map.of("b", "beta")); + assertThat(createAdapter().resourceAttributes()).containsExactly(entry("b", "beta")); + } + + @Test + void openTelemetryPropertiesShouldNotOverrideOtlpPropertiesIfEmpty() { + this.properties.setResourceAttributes(Map.of("a", "alpha")); + this.openTelemetryProperties.setResourceAttributes(Collections.emptyMap()); + assertThat(createAdapter().resourceAttributes()).containsExactly(entry("a", "alpha")); + } + + private OtlpPropertiesConfigAdapter createAdapter() { + return new OtlpPropertiesConfigAdapter(this.properties, this.openTelemetryProperties); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java new file mode 100644 index 000000000000..e6fbbcf5dd3c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java @@ -0,0 +1,157 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.opentelemetry; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.annotation.ImportCandidates; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link OpenTelemetryAutoConfiguration}. + * + * @author Moritz Halbritter + */ +class OpenTelemetryAutoConfigurationTests { + + private final ApplicationContextRunner runner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)); + + @Test + void isRegisteredInAutoConfigurationImports() { + assertThat(ImportCandidates.load(AutoConfiguration.class, null).getCandidates()) + .contains(OpenTelemetryAutoConfiguration.class.getName()); + } + + @Test + void shouldProvideBeans() { + this.runner.run((context) -> { + assertThat(context).hasSingleBean(OpenTelemetrySdk.class); + assertThat(context).hasSingleBean(Resource.class); + }); + } + + @Test + void shouldBackOffIfOpenTelemetryIsNotOnClasspath() { + this.runner.withClassLoader(new FilteredClassLoader("io.opentelemetry")).run((context) -> { + assertThat(context).doesNotHaveBean(OpenTelemetrySdk.class); + assertThat(context).doesNotHaveBean(Resource.class); + }); + } + + @Test + void backsOffOnUserSuppliedBeans() { + this.runner.withUserConfiguration(UserConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(OpenTelemetry.class); + assertThat(context).hasBean("customOpenTelemetry"); + assertThat(context).hasSingleBean(Resource.class); + assertThat(context).hasBean("customResource"); + }); + } + + @Test + void shouldApplySpringApplicationNameToResource() { + this.runner.withPropertyValues("spring.application.name=my-application").run((context) -> { + Resource resource = context.getBean(Resource.class); + assertThat(resource.getAttributes().asMap()) + .contains(entry(ResourceAttributes.SERVICE_NAME, "my-application")); + }); + } + + @Test + void shouldFallbackToDefaultApplicationNameIfSpringApplicationNameIsNotSet() { + this.runner.run((context) -> { + Resource resource = context.getBean(Resource.class); + assertThat(resource.getAttributes().asMap()) + .contains(entry(ResourceAttributes.SERVICE_NAME, "application")); + }); + } + + @Test + void shouldApplyResourceAttributesFromProperties() { + this.runner.withPropertyValues("management.opentelemetry.resource-attributes.region=us-west").run((context) -> { + Resource resource = context.getBean(Resource.class); + assertThat(resource.getAttributes().asMap()).contains(entry(AttributeKey.stringKey("region"), "us-west")); + }); + } + + @Test + void shouldRegisterSdkTracerProviderIfAvailable() { + this.runner.withBean(SdkTracerProvider.class, () -> SdkTracerProvider.builder().build()).run((context) -> { + OpenTelemetry openTelemetry = context.getBean(OpenTelemetry.class); + assertThat(openTelemetry.getTracerProvider()).isNotNull(); + }); + } + + @Test + void shouldRegisterContextPropagatorsIfAvailable() { + this.runner.withBean(ContextPropagators.class, ContextPropagators::noop).run((context) -> { + OpenTelemetry openTelemetry = context.getBean(OpenTelemetry.class); + assertThat(openTelemetry.getPropagators()).isNotNull(); + }); + } + + @Test + void shouldRegisterSdkLoggerProviderIfAvailable() { + this.runner.withBean(SdkLoggerProvider.class, () -> SdkLoggerProvider.builder().build()).run((context) -> { + OpenTelemetry openTelemetry = context.getBean(OpenTelemetry.class); + assertThat(openTelemetry.getLogsBridge()).isNotNull(); + }); + } + + @Test + void shouldRegisterSdkMeterProviderIfAvailable() { + this.runner.withBean(SdkMeterProvider.class, () -> SdkMeterProvider.builder().build()).run((context) -> { + OpenTelemetry openTelemetry = context.getBean(OpenTelemetry.class); + assertThat(openTelemetry.getMeterProvider()).isNotNull(); + }); + } + + @Configuration(proxyBeanMethods = false) + private static class UserConfiguration { + + @Bean + OpenTelemetry customOpenTelemetry() { + return mock(OpenTelemetry.class); + } + + @Bean + Resource customResource() { + return Resource.getDefault(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggagePropagationIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggagePropagationIntegrationTests.java index 6a243e92284e..77c997cd196c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggagePropagationIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggagePropagationIntegrationTests.java @@ -29,6 +29,7 @@ import org.junit.jupiter.params.provider.EnumSource; import org.slf4j.MDC; +import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.ApplicationContext; @@ -151,8 +152,9 @@ public ApplicationContextRunner get() { OTEL_DEFAULT { @Override public ApplicationContextRunner get() { - return new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)) + return new ApplicationContextRunner().withConfiguration(AutoConfigurations.of( + OpenTelemetryAutoConfiguration.class, + org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration.class)) .withPropertyValues("management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp", "management.tracing.baggage.correlation.fields=country-code,bp"); } @@ -172,8 +174,9 @@ public ApplicationContextRunner get() { OTEL_W3C { @Override public ApplicationContextRunner get() { - return new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)) + return new ApplicationContextRunner().withConfiguration(AutoConfigurations.of( + OpenTelemetryAutoConfiguration.class, + org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration.class)) .withPropertyValues("management.tracing.propagation.type=W3C", "management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp", "management.tracing.baggage.correlation.fields=country-code,bp"); @@ -205,8 +208,9 @@ public ApplicationContextRunner get() { OTEL_B3 { @Override public ApplicationContextRunner get() { - return new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)) + return new ApplicationContextRunner().withConfiguration(AutoConfigurations.of( + OpenTelemetryAutoConfiguration.class, + org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration.class)) .withPropertyValues("management.tracing.propagation.type=B3", "management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp", "management.tracing.baggage.correlation.fields=country-code,bp"); @@ -216,8 +220,9 @@ public ApplicationContextRunner get() { OTEL_B3_MULTI { @Override public ApplicationContextRunner get() { - return new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)) + return new ApplicationContextRunner().withConfiguration(AutoConfigurations.of( + OpenTelemetryAutoConfiguration.class, + org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration.class)) .withPropertyValues("management.tracing.propagation.type=B3_MULTI", "management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp", "management.tracing.baggage.correlation.fields=country-code,bp"); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java index f7c94ddffe7c..6ff9bda7d2bd 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java @@ -35,7 +35,6 @@ import io.micrometer.tracing.otel.bridge.Slf4JBaggageEventListener; import io.micrometer.tracing.otel.bridge.Slf4JEventListener; import io.micrometer.tracing.otel.propagation.BaggageTextMapPropagator; -import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.MeterProvider; @@ -83,7 +82,9 @@ class OpenTelemetryAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of( + org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration.class, + OpenTelemetryAutoConfiguration.class)); @Test void shouldSupplyBeans() { @@ -91,7 +92,6 @@ void shouldSupplyBeans() { assertThat(context).hasSingleBean(OtelTracer.class); assertThat(context).hasSingleBean(EventPublisher.class); assertThat(context).hasSingleBean(OtelCurrentTraceContext.class); - assertThat(context).hasSingleBean(OpenTelemetry.class); assertThat(context).hasSingleBean(SdkTracerProvider.class); assertThat(context).hasSingleBean(ContextPropagators.class); assertThat(context).hasSingleBean(Sampler.class); @@ -123,7 +123,6 @@ void shouldNotSupplyBeansIfDependencyIsMissing(String packageName) { assertThat(context).doesNotHaveBean(OtelTracer.class); assertThat(context).doesNotHaveBean(EventPublisher.class); assertThat(context).doesNotHaveBean(OtelCurrentTraceContext.class); - assertThat(context).doesNotHaveBean(OpenTelemetry.class); assertThat(context).doesNotHaveBean(SdkTracerProvider.class); assertThat(context).doesNotHaveBean(ContextPropagators.class); assertThat(context).doesNotHaveBean(Sampler.class); @@ -148,8 +147,6 @@ void shouldBackOffOnCustomBeans() { assertThat(context).hasSingleBean(EventPublisher.class); assertThat(context).hasBean("customOtelCurrentTraceContext"); assertThat(context).hasSingleBean(OtelCurrentTraceContext.class); - assertThat(context).hasBean("customOpenTelemetry"); - assertThat(context).hasSingleBean(OpenTelemetry.class); assertThat(context).hasBean("customSdkTracerProvider"); assertThat(context).hasSingleBean(SdkTracerProvider.class); assertThat(context).hasBean("customContextPropagators"); @@ -369,11 +366,6 @@ OtelCurrentTraceContext customOtelCurrentTraceContext() { return mock(OtelCurrentTraceContext.class); } - @Bean - OpenTelemetry customOpenTelemetry() { - return mock(OpenTelemetry.class); - } - @Bean SdkTracerProvider customSdkTracerProvider() { return SdkTracerProvider.builder().build(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationIntegrationTests.java index 47fbcc824a3c..0fcc77811f95 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationIntegrationTests.java @@ -34,8 +34,8 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -50,9 +50,10 @@ class OtlpAutoConfigurationIntegrationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("management.tracing.sampling.probability=1.0") - .withConfiguration( - AutoConfigurations.of(ObservationAutoConfiguration.class, MicrometerTracingAutoConfiguration.class, - OpenTelemetryAutoConfiguration.class, OtlpAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class, + MicrometerTracingAutoConfiguration.class, OpenTelemetryAutoConfiguration.class, + org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration.class, + OtlpAutoConfiguration.class)); private final MockWebServer mockWebServer = new MockWebServer(); diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc index 742e83f92e89..53c79996f7a4 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc @@ -67,4 +67,15 @@ include::code:MyObservationPredicate[] The preceding example will prevent all observations whose name contains "denied". +[[actuator.observability.opentelemetry]] +=== OpenTelemetry Support +Spring Boot's actuator module includes basic support for https://opentelemetry.io/[OpenTelemetry]. + +It provides a bean of type `OpenTelemetry`, and if there are beans of type `SdkTracerProvider`, `ContextPropagators`, `SdkLoggerProvider` or `SdkMeterProvider` in the application context, they automatically get registered. +Additionally, it provides a `Resource` bean. +The attributes of the `Resource` can be configured via the configprop:management.opentelemetry.resource-attributes[] configuration property. + +NOTE: Spring Boot does not provide auto-configuration for OpenTelemetry metrics or logging. +OpenTelemetry tracing is only auto-configured when used together with <>. + The next sections will provide more details about logging, metrics and traces. From 39f6b85039d91a905197da86903469bac8ca03de Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 27 Jul 2023 09:14:04 +0200 Subject: [PATCH 0200/1215] Polish --- .../boot/autoconfigure/task/TaskExecutorConfigurations.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java index f80bbaf493b5..1b9ebade9666 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java @@ -50,7 +50,7 @@ SimpleAsyncTaskExecutor applicationTaskExecutor(TaskExecutionProperties properti ObjectProvider taskDecorator) { SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(properties.getThreadNamePrefix()); executor.setVirtualThreads(true); - executor.setTaskDecorator(taskDecorator.getIfUnique()); + taskDecorator.ifUnique(executor::setTaskDecorator); return executor; } From 3cc9a3bb32ef9e8b67364477c939fbc60dfac9b9 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 27 Jul 2023 11:27:29 +0200 Subject: [PATCH 0201/1215] Remove duplicate applicationTaskExecutor bean method See gh-35710 --- .../task/TaskExecutionAutoConfiguration.java | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java index 0ee647f712c0..c0530797c4f8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -16,8 +16,6 @@ package org.springframework.boot.autoconfigure.task; -import java.util.concurrent.Executor; - import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -29,10 +27,8 @@ import org.springframework.boot.task.TaskExecutorCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.Lazy; import org.springframework.core.task.TaskDecorator; import org.springframework.core.task.TaskExecutor; -import org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; /** @@ -75,12 +71,4 @@ public TaskExecutorBuilder taskExecutorBuilder(TaskExecutionProperties propertie return builder; } - @Lazy - @Bean(name = { APPLICATION_TASK_EXECUTOR_BEAN_NAME, - AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME }) - @ConditionalOnMissingBean(Executor.class) - public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) { - return builder.build(); - } - } From eeb1e1fc35267f5a200284a8390dccc10fc8b355 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 27 Jul 2023 16:30:59 +0200 Subject: [PATCH 0202/1215] Add VirtualThreads bean and auto-configuration This bean is only in the context if virtual threads are enabled. It can be used to get access to the virtual thread executor. --- .../autoconfigure/thread/VirtualThreads.java | 50 ++++++++++++++++ .../VirtualThreadsAutoConfiguration.java | 39 ++++++++++++ .../autoconfigure/thread/package-info.java | 20 +++++++ ...ot.autoconfigure.AutoConfiguration.imports | 3 +- .../VirtualThreadsAutoConfigurationTests.java | 60 +++++++++++++++++++ .../thread/VirtualThreadsTests.java | 47 +++++++++++++++ 6 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/VirtualThreads.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/package-info.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/VirtualThreads.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/VirtualThreads.java new file mode 100644 index 000000000000..db4fd60b661f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/VirtualThreads.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2023 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.thread; + +import java.lang.reflect.Method; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * Virtual thread support. + * + * @author Moritz Halbritter + * @since 3.2.0 + */ +public class VirtualThreads { + + private final Executor executor; + + VirtualThreads() { + Method method = ReflectionUtils.findMethod(Executors.class, "newVirtualThreadPerTaskExecutor"); + Assert.notNull(method, "Executors.newVirtualThreadPerTaskExecutor() method is missing"); + this.executor = (Executor) ReflectionUtils.invokeMethod(method, null); + } + + /** + * Returns the virtual thread executor. + * @return the virtual thread executor + */ + public Executor getExecutor() { + return this.executor; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsAutoConfiguration.java new file mode 100644 index 000000000000..b2936490e44b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsAutoConfiguration.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2023 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.thread; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnVirtualThreads; +import org.springframework.context.annotation.Bean; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for virtual threads. + * + * @author Moritz Halbritter + * @since 3.2.0 + */ +@AutoConfiguration +@ConditionalOnVirtualThreads +public class VirtualThreadsAutoConfiguration { + + @Bean + VirtualThreads virtualThreads() { + return new VirtualThreads(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/package-info.java new file mode 100644 index 000000000000..61c141a651aa --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Classes related to threads. + */ +package org.springframework.boot.autoconfigure.thread; 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 5997fb8ea2db..46be5e5216ee 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 @@ -119,6 +119,7 @@ org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfigurati org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration +org.springframework.boot.autoconfigure.thread.VirtualThreadsAutoConfiguration org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration @@ -144,4 +145,4 @@ org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoC org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration -org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration \ No newline at end of file +org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsAutoConfigurationTests.java new file mode 100644 index 000000000000..12d80789f9c4 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsAutoConfigurationTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2023 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.thread; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.annotation.ImportCandidates; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link VirtualThreadsAutoConfiguration}. + * + * @author Moritz Halbritter + */ +class VirtualThreadsAutoConfigurationTests { + + private final ApplicationContextRunner runner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(VirtualThreadsAutoConfiguration.class)); + + @Test + void shouldBeRegisteredInAutoConfigurationImports() { + assertThat(ImportCandidates.load(AutoConfiguration.class, null).getCandidates()) + .contains(VirtualThreadsAutoConfiguration.class.getName()); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void shouldSupplyBeans() { + this.runner.withPropertyValues("spring.threads.virtual.enabled=true") + .run((context) -> assertThat(context).hasSingleBean(VirtualThreads.class)); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void shouldNotSupplyBeansIfVirtualThreadsAreNotEnabled() { + this.runner.withPropertyValues("spring.threads.virtual.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(VirtualThreads.class)); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsTests.java new file mode 100644 index 000000000000..9f2bf4c40d98 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2023 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.thread; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link VirtualThreads}. + * + * @author Moritz Halbritter + */ +class VirtualThreadsTests { + + @Test + @EnabledForJreRange(max = JRE.JAVA_20) + void shouldThrowExceptionBelowJava21() { + assertThatThrownBy(VirtualThreads::new).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Executors.newVirtualThreadPerTaskExecutor() method is missing"); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void shouldReturnExecutorOnJava21AndUp() { + VirtualThreads virtualThreads = new VirtualThreads(); + assertThat(virtualThreads.getExecutor()).isNotNull(); + } + +} From f85a7258a6094f4b076bc97885c99de043bf73cb Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 27 Jul 2023 16:31:46 +0200 Subject: [PATCH 0203/1215] Polish SampleAmqpSimpleApplication --- .../java/smoketest/amqp/SampleAmqpSimpleApplication.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/SampleAmqpSimpleApplication.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/SampleAmqpSimpleApplication.java index 410385cb72e6..356ce74c9d7f 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/SampleAmqpSimpleApplication.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/main/java/smoketest/amqp/SampleAmqpSimpleApplication.java @@ -16,7 +16,8 @@ package smoketest.amqp; -import java.util.Date; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.annotation.RabbitHandler; @@ -31,6 +32,8 @@ @RabbitListener(queues = "foo") public class SampleAmqpSimpleApplication { + private static final Log logger = LogFactory.getLog(SampleAmqpSimpleApplication.class); + @Bean public Sender mySender() { return new Sender(); @@ -43,7 +46,7 @@ public Queue fooQueue() { @RabbitHandler public void process(@Payload String foo) { - System.out.println(new Date() + ": " + foo); + logger.info(foo); } @Bean From b1a3dad16cead58676b1eeac25cdb0f6d005d5ae Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 27 Jul 2023 16:32:30 +0200 Subject: [PATCH 0204/1215] Configure virtual threads on the RabbitMQ listener Closes gh-36387 --- ...RabbitListenerContainerFactoryConfigurer.java | 14 ++++++++++++++ .../RabbitAnnotationDrivenConfiguration.java | 9 ++++++++- .../amqp/RabbitAutoConfigurationTests.java | 16 ++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractRabbitListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractRabbitListenerContainerFactoryConfigurer.java index 154ac99ceae0..781029791b22 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractRabbitListenerContainerFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractRabbitListenerContainerFactoryConfigurer.java @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.amqp; import java.util.List; +import java.util.concurrent.Executor; import org.springframework.amqp.rabbit.config.AbstractRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.config.RetryInterceptorBuilder; @@ -47,6 +48,8 @@ public abstract class AbstractRabbitListenerContainerFactoryConfigurer r this.retryTemplateCustomizers = retryTemplateCustomizers; } + /** + * Set the task executor to use. + * @param taskExecutor the task executor + */ + public void setTaskExecutor(Executor taskExecutor) { + this.taskExecutor = taskExecutor; + } + protected final RabbitProperties getRabbitProperties() { return this.rabbitProperties; } @@ -119,6 +130,9 @@ protected void configure(T factory, ConnectionFactory connectionFactory, factory.setMissingQueuesFatal(configuration.isMissingQueuesFatal()); factory.setDeBatchingEnabled(configuration.isDeBatchingEnabled()); factory.setForceStop(configuration.isForceStop()); + if (this.taskExecutor != null) { + factory.setTaskExecutor(this.taskExecutor); + } ListenerRetry retryConfig = configuration.getRetry(); if (retryConfig.isEnabled()) { RetryInterceptorBuilder builder = (retryConfig.isStateless()) ? RetryInterceptorBuilder.stateless() diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAnnotationDrivenConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAnnotationDrivenConfiguration.java index 51decebff512..2a81759aeaf2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAnnotationDrivenConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAnnotationDrivenConfiguration.java @@ -30,6 +30,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.thread.VirtualThreads; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -51,13 +52,17 @@ class RabbitAnnotationDrivenConfiguration { private final RabbitProperties properties; + private final ObjectProvider virtualThreads; + RabbitAnnotationDrivenConfiguration(ObjectProvider messageConverter, ObjectProvider messageRecoverer, - ObjectProvider retryTemplateCustomizers, RabbitProperties properties) { + ObjectProvider retryTemplateCustomizers, RabbitProperties properties, + ObjectProvider virtualThreads) { this.messageConverter = messageConverter; this.messageRecoverer = messageRecoverer; this.retryTemplateCustomizers = retryTemplateCustomizers; this.properties = properties; + this.virtualThreads = virtualThreads; } @Bean @@ -68,6 +73,7 @@ SimpleRabbitListenerContainerFactoryConfigurer simpleRabbitListenerContainerFact configurer.setMessageConverter(this.messageConverter.getIfUnique()); configurer.setMessageRecoverer(this.messageRecoverer.getIfUnique()); configurer.setRetryTemplateCustomizers(this.retryTemplateCustomizers.orderedStream().toList()); + this.virtualThreads.ifAvailable((virtualThreads) -> configurer.setTaskExecutor(virtualThreads.getExecutor())); return configurer; } @@ -92,6 +98,7 @@ DirectRabbitListenerContainerFactoryConfigurer directRabbitListenerContainerFact configurer.setMessageConverter(this.messageConverter.getIfUnique()); configurer.setMessageRecoverer(this.messageRecoverer.getIfUnique()); configurer.setRetryTemplateCustomizers(this.retryTemplateCustomizers.orderedStream().toList()); + this.virtualThreads.ifAvailable((virtualThreads) -> configurer.setTaskExecutor(virtualThreads.getExecutor())); return configurer; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java index 363a4153b8d7..afe3271b4899 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java @@ -30,6 +30,8 @@ import com.rabbitmq.client.impl.DefaultCredentialsProvider; import org.aopalliance.aop.Advice; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InOrder; @@ -58,6 +60,7 @@ import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.thread.VirtualThreadsAutoConfiguration; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.system.CapturedOutput; @@ -532,6 +535,19 @@ void testSimpleRabbitListenerContainerFactoryWithCustomSettings() { }); } + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void shouldConfigureVirtualThreads() { + this.contextRunner.withConfiguration(AutoConfigurations.of(VirtualThreadsAutoConfiguration.class)) + .withPropertyValues("spring.threads.virtual.enabled=true") + .run((context) -> { + SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory = context + .getBean("rabbitListenerContainerFactory", SimpleRabbitListenerContainerFactory.class); + Object executor = ReflectionTestUtils.getField(rabbitListenerContainerFactory, "taskExecutor"); + assertThat(executor).as("rabbitListenerContainerFactory.taskExecutor").isNotNull(); + }); + } + @Test void testSimpleRabbitListenerContainerFactoryWithDefaultForceStop() { this.contextRunner From 96796cc9b508498443751b6c2f10a926a301d49f Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 27 Jul 2023 16:33:33 +0200 Subject: [PATCH 0205/1215] Disable 'Use Lombok Getter' IntelliJ inspection --- .idea/inspectionProfiles/Project_Default.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 1ec9e6600dca..7f1766dd1b68 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,6 +1,7 @@ \ No newline at end of file From f6eaedea3577ab44304da17e8d63f62d9be1317c Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Thu, 27 Jul 2023 12:44:30 +0800 Subject: [PATCH 0206/1215] Reinstate use of configprop macro See gh-36604 --- .../src/docs/asciidoc/actuator/observability.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc index 53c79996f7a4..9d8cd3b0efad 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc @@ -58,7 +58,7 @@ If you'd like to prevent some observations from being reported, you can use the The preceding example will prevent all observations with a name starting with `denied.prefix` or `another.denied.prefix`. -TIP: If you want to prevent Spring Security from reporting observations, set the property `management.observations.enable.spring.security` to `false`. +TIP: If you want to prevent Spring Security from reporting observations, set the property configprop:management.observations.enable.spring.security[] to `false`. If you need greater control over the prevention of observations, you can register beans of type `ObservationPredicate`. Observations are only reported if all the `ObservationPredicate` beans return `true` for that observation. From bf488192222d668927df0344c29a203e66af8ec8 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 28 Jul 2023 14:05:17 +0200 Subject: [PATCH 0207/1215] Implement @ConditionalOnThreading Closes gh-36624 --- .../condition/ConditionalOnThreading.java | 46 ++++++++++ .../condition/OnThreadingCondition.java | 51 +++++++++++ .../boot/autoconfigure/thread/Threading.java | 59 ++++++++++++ .../ConditionalOnThreadingTests.java | 91 +++++++++++++++++++ 4 files changed, 247 insertions(+) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnThreading.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnThreadingCondition.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/Threading.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnThreadingTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnThreading.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnThreading.java new file mode 100644 index 000000000000..18da6ceddbda --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnThreading.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2023 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.condition; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.thread.Threading; +import org.springframework.context.annotation.Conditional; + +/** + * {@link Conditional @Conditional} that matches when the specified threading is active. + * + * @author Moritz Halbritter + * @since 3.2.0 + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Conditional(OnThreadingCondition.class) +public @interface ConditionalOnThreading { + + /** + * The {@link Threading threading} that must be active. + * @return the expected threading + */ + Threading value(); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnThreadingCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnThreadingCondition.java new file mode 100644 index 000000000000..7856a63431a6 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnThreadingCondition.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2023 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.condition; + +import java.util.Map; + +import org.springframework.boot.autoconfigure.thread.Threading; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * {@link Condition} that checks for a required {@link Threading}. + * + * @author Moritz Halbritter + * @see ConditionalOnThreading + */ +class OnThreadingCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + Map attributes = metadata.getAnnotationAttributes(ConditionalOnThreading.class.getName()); + Threading threading = (Threading) attributes.get("value"); + return getMatchOutcome(context.getEnvironment(), threading); + } + + private ConditionOutcome getMatchOutcome(Environment environment, Threading threading) { + String name = threading.name(); + ConditionMessage.Builder message = ConditionMessage.forCondition(ConditionalOnThreading.class); + if (threading.isActive(environment)) { + return ConditionOutcome.match(message.foundExactly(name)); + } + return ConditionOutcome.noMatch(message.didNotFind(name).atAll()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/Threading.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/Threading.java new file mode 100644 index 000000000000..1f32fa117005 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/Threading.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2023 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.thread; + +import org.springframework.boot.system.JavaVersion; +import org.springframework.core.env.Environment; + +/** + * Threading of the application. + * + * @author Moritz Halbritter + * @since 3.2.0 + */ +public enum Threading { + + /** + * Platform threads. Active if virtual threads are not active. + */ + PLATFORM { + @Override + public boolean isActive(Environment environment) { + return !VIRTUAL.isActive(environment); + } + }, + /** + * Virtual threads. Active if {@code spring.threads.virtual.enabled} is {@code true} + * and running on Java 21 or later. + */ + VIRTUAL { + @Override + public boolean isActive(Environment environment) { + boolean virtualThreadsEnabled = environment.getProperty("spring.threads.virtual.enabled", boolean.class, + false); + return virtualThreadsEnabled && JavaVersion.getJavaVersion().isEqualOrNewerThan(JavaVersion.TWENTY_ONE); + } + }; + + /** + * Determines whether the threading is active. + * @param environment the environment + * @return whether the threading is active + */ + public abstract boolean isActive(Environment environment); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnThreadingTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnThreadingTests.java new file mode 100644 index 000000000000..a455b22f0f91 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnThreadingTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2023 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.condition; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; + +import org.springframework.boot.autoconfigure.thread.Threading; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConditionalOnThreading}. + * + * @author Moritz Halbritter + */ +class ConditionalOnThreadingTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(BasicConfiguration.class); + + @Test + @EnabledForJreRange(max = JRE.JAVA_20) + void platformThreadsOnJdkBelow21IfVirtualThreadsPropertyIsEnabled() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true") + .run((context) -> assertThat(context.getBean(ThreadType.class)).isEqualTo(ThreadType.PLATFORM)); + } + + @Test + @EnabledForJreRange(max = JRE.JAVA_20) + void platformThreadsOnJdkBelow21IfVirtualThreadsPropertyIsDisabled() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=false") + .run((context) -> assertThat(context.getBean(ThreadType.class)).isEqualTo(ThreadType.PLATFORM)); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void virtualThreadsOnJdk21IfVirtualThreadsPropertyIsEnabled() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true") + .run((context) -> assertThat(context.getBean(ThreadType.class)).isEqualTo(ThreadType.VIRTUAL)); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void platformThreadsOnJdk21IfVirtualThreadsPropertyIsDisabled() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=false") + .run((context) -> assertThat(context.getBean(ThreadType.class)).isEqualTo(ThreadType.PLATFORM)); + } + + private enum ThreadType { + + PLATFORM, VIRTUAL + + } + + @Configuration(proxyBeanMethods = false) + static class BasicConfiguration { + + @Bean + @ConditionalOnThreading(Threading.VIRTUAL) + ThreadType virtual() { + return ThreadType.VIRTUAL; + } + + @Bean + @ConditionalOnThreading(Threading.PLATFORM) + ThreadType platform() { + return ThreadType.PLATFORM; + } + + } + +} From 9e212875c3653b5b6e58fc4c7106b95bd332178c Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 28 Jul 2023 14:13:43 +0200 Subject: [PATCH 0208/1215] Remove VirtualThreads bean Reverts eeb1e1fc35267f5a200284a8390dccc10fc8b355 See gh-36615 See gh-36387 --- .../RabbitAnnotationDrivenConfiguration.java | 61 +++++++++++++------ .../autoconfigure/thread/VirtualThreads.java | 50 --------------- .../VirtualThreadsAutoConfiguration.java | 39 ------------ ...ot.autoconfigure.AutoConfiguration.imports | 1 - .../amqp/RabbitAutoConfigurationTests.java | 17 +++--- .../VirtualThreadsAutoConfigurationTests.java | 60 ------------------ .../thread/VirtualThreadsTests.java | 47 -------------- 7 files changed, 51 insertions(+), 224 deletions(-) delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/VirtualThreads.java delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsAutoConfiguration.java delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsAutoConfigurationTests.java delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAnnotationDrivenConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAnnotationDrivenConfiguration.java index 2a81759aeaf2..0a5331e144b0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAnnotationDrivenConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAnnotationDrivenConfiguration.java @@ -30,15 +30,18 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.thread.VirtualThreads; +import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading; +import org.springframework.boot.autoconfigure.thread.Threading; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.VirtualThreadTaskExecutor; /** * Configuration for Spring AMQP annotation driven endpoints. * * @author Stephane Nicoll * @author Josh Thornhill + * @author Moritz Halbritter */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(EnableRabbit.class) @@ -52,28 +55,28 @@ class RabbitAnnotationDrivenConfiguration { private final RabbitProperties properties; - private final ObjectProvider virtualThreads; - RabbitAnnotationDrivenConfiguration(ObjectProvider messageConverter, ObjectProvider messageRecoverer, - ObjectProvider retryTemplateCustomizers, RabbitProperties properties, - ObjectProvider virtualThreads) { + ObjectProvider retryTemplateCustomizers, RabbitProperties properties) { this.messageConverter = messageConverter; this.messageRecoverer = messageRecoverer; this.retryTemplateCustomizers = retryTemplateCustomizers; this.properties = properties; - this.virtualThreads = virtualThreads; } @Bean @ConditionalOnMissingBean + @ConditionalOnThreading(Threading.PLATFORM) SimpleRabbitListenerContainerFactoryConfigurer simpleRabbitListenerContainerFactoryConfigurer() { - SimpleRabbitListenerContainerFactoryConfigurer configurer = new SimpleRabbitListenerContainerFactoryConfigurer( - this.properties); - configurer.setMessageConverter(this.messageConverter.getIfUnique()); - configurer.setMessageRecoverer(this.messageRecoverer.getIfUnique()); - configurer.setRetryTemplateCustomizers(this.retryTemplateCustomizers.orderedStream().toList()); - this.virtualThreads.ifAvailable((virtualThreads) -> configurer.setTaskExecutor(virtualThreads.getExecutor())); + return simpleListenerConfigurer(); + } + + @Bean(name = "simpleRabbitListenerContainerFactoryConfigurer") + @ConditionalOnMissingBean + @ConditionalOnThreading(Threading.VIRTUAL) + SimpleRabbitListenerContainerFactoryConfigurer simpleRabbitListenerContainerFactoryConfigurerVirtualThreads() { + SimpleRabbitListenerContainerFactoryConfigurer configurer = simpleListenerConfigurer(); + configurer.setTaskExecutor(new VirtualThreadTaskExecutor()); return configurer; } @@ -92,13 +95,17 @@ SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory( @Bean @ConditionalOnMissingBean + @ConditionalOnThreading(Threading.PLATFORM) DirectRabbitListenerContainerFactoryConfigurer directRabbitListenerContainerFactoryConfigurer() { - DirectRabbitListenerContainerFactoryConfigurer configurer = new DirectRabbitListenerContainerFactoryConfigurer( - this.properties); - configurer.setMessageConverter(this.messageConverter.getIfUnique()); - configurer.setMessageRecoverer(this.messageRecoverer.getIfUnique()); - configurer.setRetryTemplateCustomizers(this.retryTemplateCustomizers.orderedStream().toList()); - this.virtualThreads.ifAvailable((virtualThreads) -> configurer.setTaskExecutor(virtualThreads.getExecutor())); + return directListenerConfigurer(); + } + + @Bean(name = "directRabbitListenerContainerFactoryConfigurer") + @ConditionalOnMissingBean + @ConditionalOnThreading(Threading.VIRTUAL) + DirectRabbitListenerContainerFactoryConfigurer directRabbitListenerContainerFactoryConfigurerVirtualThreads() { + DirectRabbitListenerContainerFactoryConfigurer configurer = directListenerConfigurer(); + configurer.setTaskExecutor(new VirtualThreadTaskExecutor()); return configurer; } @@ -114,6 +121,24 @@ DirectRabbitListenerContainerFactory directRabbitListenerContainerFactory( return factory; } + private SimpleRabbitListenerContainerFactoryConfigurer simpleListenerConfigurer() { + SimpleRabbitListenerContainerFactoryConfigurer configurer = new SimpleRabbitListenerContainerFactoryConfigurer( + this.properties); + configurer.setMessageConverter(this.messageConverter.getIfUnique()); + configurer.setMessageRecoverer(this.messageRecoverer.getIfUnique()); + configurer.setRetryTemplateCustomizers(this.retryTemplateCustomizers.orderedStream().toList()); + return configurer; + } + + private DirectRabbitListenerContainerFactoryConfigurer directListenerConfigurer() { + DirectRabbitListenerContainerFactoryConfigurer configurer = new DirectRabbitListenerContainerFactoryConfigurer( + this.properties); + configurer.setMessageConverter(this.messageConverter.getIfUnique()); + configurer.setMessageRecoverer(this.messageRecoverer.getIfUnique()); + configurer.setRetryTemplateCustomizers(this.retryTemplateCustomizers.orderedStream().toList()); + return configurer; + } + @Configuration(proxyBeanMethods = false) @EnableRabbit @ConditionalOnMissingBean(name = RabbitListenerConfigUtils.RABBIT_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/VirtualThreads.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/VirtualThreads.java deleted file mode 100644 index db4fd60b661f..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/VirtualThreads.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2012-2023 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.thread; - -import java.lang.reflect.Method; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; - -import org.springframework.util.Assert; -import org.springframework.util.ReflectionUtils; - -/** - * Virtual thread support. - * - * @author Moritz Halbritter - * @since 3.2.0 - */ -public class VirtualThreads { - - private final Executor executor; - - VirtualThreads() { - Method method = ReflectionUtils.findMethod(Executors.class, "newVirtualThreadPerTaskExecutor"); - Assert.notNull(method, "Executors.newVirtualThreadPerTaskExecutor() method is missing"); - this.executor = (Executor) ReflectionUtils.invokeMethod(method, null); - } - - /** - * Returns the virtual thread executor. - * @return the virtual thread executor - */ - public Executor getExecutor() { - return this.executor; - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsAutoConfiguration.java deleted file mode 100644 index b2936490e44b..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsAutoConfiguration.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2012-2023 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.thread; - -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnVirtualThreads; -import org.springframework.context.annotation.Bean; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for virtual threads. - * - * @author Moritz Halbritter - * @since 3.2.0 - */ -@AutoConfiguration -@ConditionalOnVirtualThreads -public class VirtualThreadsAutoConfiguration { - - @Bean - VirtualThreads virtualThreads() { - return new VirtualThreads(); - } - -} 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 46be5e5216ee..7bd25ab47168 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 @@ -119,7 +119,6 @@ org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfigurati org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration -org.springframework.boot.autoconfigure.thread.VirtualThreadsAutoConfiguration org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java index afe3271b4899..1d54051d7305 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java @@ -60,7 +60,6 @@ import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.thread.VirtualThreadsAutoConfiguration; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.system.CapturedOutput; @@ -71,6 +70,7 @@ import org.springframework.context.annotation.Primary; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; +import org.springframework.core.task.VirtualThreadTaskExecutor; import org.springframework.retry.RetryPolicy; import org.springframework.retry.backoff.BackOffPolicy; import org.springframework.retry.backoff.ExponentialBackOffPolicy; @@ -538,14 +538,13 @@ void testSimpleRabbitListenerContainerFactoryWithCustomSettings() { @Test @EnabledForJreRange(min = JRE.JAVA_21) void shouldConfigureVirtualThreads() { - this.contextRunner.withConfiguration(AutoConfigurations.of(VirtualThreadsAutoConfiguration.class)) - .withPropertyValues("spring.threads.virtual.enabled=true") - .run((context) -> { - SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory = context - .getBean("rabbitListenerContainerFactory", SimpleRabbitListenerContainerFactory.class); - Object executor = ReflectionTestUtils.getField(rabbitListenerContainerFactory, "taskExecutor"); - assertThat(executor).as("rabbitListenerContainerFactory.taskExecutor").isNotNull(); - }); + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> { + SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory = context + .getBean("rabbitListenerContainerFactory", SimpleRabbitListenerContainerFactory.class); + Object executor = ReflectionTestUtils.getField(rabbitListenerContainerFactory, "taskExecutor"); + assertThat(executor).as("rabbitListenerContainerFactory.taskExecutor") + .isInstanceOf(VirtualThreadTaskExecutor.class); + }); } @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsAutoConfigurationTests.java deleted file mode 100644 index 12d80789f9c4..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsAutoConfigurationTests.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2012-2023 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.thread; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledForJreRange; -import org.junit.jupiter.api.condition.JRE; - -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.context.annotation.ImportCandidates; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link VirtualThreadsAutoConfiguration}. - * - * @author Moritz Halbritter - */ -class VirtualThreadsAutoConfigurationTests { - - private final ApplicationContextRunner runner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(VirtualThreadsAutoConfiguration.class)); - - @Test - void shouldBeRegisteredInAutoConfigurationImports() { - assertThat(ImportCandidates.load(AutoConfiguration.class, null).getCandidates()) - .contains(VirtualThreadsAutoConfiguration.class.getName()); - } - - @Test - @EnabledForJreRange(min = JRE.JAVA_21) - void shouldSupplyBeans() { - this.runner.withPropertyValues("spring.threads.virtual.enabled=true") - .run((context) -> assertThat(context).hasSingleBean(VirtualThreads.class)); - } - - @Test - @EnabledForJreRange(min = JRE.JAVA_21) - void shouldNotSupplyBeansIfVirtualThreadsAreNotEnabled() { - this.runner.withPropertyValues("spring.threads.virtual.enabled=false") - .run((context) -> assertThat(context).doesNotHaveBean(VirtualThreads.class)); - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsTests.java deleted file mode 100644 index 9f2bf4c40d98..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/thread/VirtualThreadsTests.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2012-2023 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.thread; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledForJreRange; -import org.junit.jupiter.api.condition.JRE; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -/** - * Tests for {@link VirtualThreads}. - * - * @author Moritz Halbritter - */ -class VirtualThreadsTests { - - @Test - @EnabledForJreRange(max = JRE.JAVA_20) - void shouldThrowExceptionBelowJava21() { - assertThatThrownBy(VirtualThreads::new).isInstanceOf(IllegalArgumentException.class) - .hasMessage("Executors.newVirtualThreadPerTaskExecutor() method is missing"); - } - - @Test - @EnabledForJreRange(min = JRE.JAVA_21) - void shouldReturnExecutorOnJava21AndUp() { - VirtualThreads virtualThreads = new VirtualThreads(); - assertThat(virtualThreads.getExecutor()).isNotNull(); - } - -} From 1347b998e6f5b91071738b8c4b2689fec38de743 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 28 Jul 2023 14:20:20 +0200 Subject: [PATCH 0209/1215] Remove @ConditionalOnVirtualThreads See gh-36624 See gh-35892 --- .../ConditionalOnVirtualThreads.java | 42 ----------- .../task/TaskExecutorConfigurations.java | 5 +- ...verFactoryCustomizerAutoConfiguration.java | 7 +- .../ConditionalOnVirtualThreadsTests.java | 70 ------------------- 4 files changed, 7 insertions(+), 117 deletions(-) delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnVirtualThreads.java delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnVirtualThreadsTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnVirtualThreads.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnVirtualThreads.java deleted file mode 100644 index 34771eb3ef55..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnVirtualThreads.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2012-2023 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.condition; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import org.springframework.boot.system.JavaVersion; -import org.springframework.context.annotation.Conditional; - -/** - * {@link Conditional @Conditional} that only matches when virtual threads are available - * and enabled. - * - * @author Moritz Halbritter - * @since 3.2.0 - */ -@Target({ ElementType.TYPE, ElementType.METHOD }) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@ConditionalOnJava(JavaVersion.TWENTY_ONE) -@ConditionalOnProperty(name = "spring.threads.virtual.enabled", havingValue = "true") -public @interface ConditionalOnVirtualThreads { - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java index 1b9ebade9666..2e9646b90aae 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java @@ -20,7 +20,8 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnVirtualThreads; +import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading; +import org.springframework.boot.autoconfigure.thread.Threading; import org.springframework.boot.task.TaskExecutorBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -39,7 +40,7 @@ */ class TaskExecutorConfigurations { - @ConditionalOnVirtualThreads + @ConditionalOnThreading(Threading.VIRTUAL) @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(Executor.class) static class VirtualThreadTaskExecutorConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java index 846806696738..eb7be8fe2ff2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java @@ -29,8 +29,9 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnNotWarDeployment; -import org.springframework.boot.autoconfigure.condition.ConditionalOnVirtualThreads; +import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.thread.Threading; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -64,7 +65,7 @@ public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environ } @Bean - @ConditionalOnVirtualThreads + @ConditionalOnThreading(Threading.VIRTUAL) TomcatVirtualThreadsWebServerFactoryCustomizer tomcatVirtualThreadsProtocolHandlerCustomizer() { return new TomcatVirtualThreadsWebServerFactoryCustomizer(); } @@ -85,7 +86,7 @@ public JettyWebServerFactoryCustomizer jettyWebServerFactoryCustomizer(Environme } @Bean - @ConditionalOnVirtualThreads + @ConditionalOnThreading(Threading.VIRTUAL) JettyVirtualThreadsWebServerFactoryCustomizer jettyVirtualThreadsWebServerFactoryCustomizer( ServerProperties serverProperties) { return new JettyVirtualThreadsWebServerFactoryCustomizer(serverProperties); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnVirtualThreadsTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnVirtualThreadsTests.java deleted file mode 100644 index cf7b4148d101..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnVirtualThreadsTests.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2012-2023 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.condition; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledForJreRange; -import org.junit.jupiter.api.condition.JRE; - -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link ConditionalOnVirtualThreads @ConditionalOnVirtualThreads}. - * - * @author Moritz Halbritter - */ -class ConditionalOnVirtualThreadsTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withUserConfiguration(BasicConfiguration.class); - - @Test - @EnabledForJreRange(max = JRE.JAVA_20) - void isDisabledOnJdkBelow21EvenIfPropertyIsSet() { - this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true") - .run((context) -> assertThat(context).doesNotHaveBean("someBean")); - } - - @Test - @EnabledForJreRange(min = JRE.JAVA_21) - void isDisabledOnJdk21IfPropertyIsNotSet() { - this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean("someBean")); - } - - @Test - @EnabledForJreRange(min = JRE.JAVA_21) - void isEnabledOnJdk21IfPropertyIsSet() { - this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true") - .run((context) -> assertThat(context).hasBean("someBean")); - } - - @Configuration(proxyBeanMethods = false) - static class BasicConfiguration { - - @Bean - @ConditionalOnVirtualThreads - String someBean() { - return "someBean"; - } - - } - -} From 32c91af440544effe40516927cc768c3c401a412 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Mon, 31 Jul 2023 09:21:43 +0200 Subject: [PATCH 0210/1215] Add ThreadPoolTaskExecutorBuilder and deprecate TaskExecutorBuilder Closes gh-36637 --- .../task/TaskExecutionAutoConfiguration.java | 33 +- .../task/TaskExecutorConfigurations.java | 78 ++++- .../TaskExecutionAutoConfigurationTests.java | 124 ++++++- .../task-execution-and-scheduling.adoc | 4 +- .../boot/task/TaskExecutorBuilder.java | 6 +- .../boot/task/TaskExecutorCustomizer.java | 5 +- .../task/ThreadPoolTaskExecutorBuilder.java | 326 ++++++++++++++++++ .../ThreadPoolTaskExecutorCustomizer.java | 37 ++ .../boot/task/TaskExecutorBuilderTests.java | 1 + .../ThreadPoolTaskExecutorBuilderTests.java | 169 +++++++++ 10 files changed, 741 insertions(+), 42 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilder.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorCustomizer.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilderTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java index c0530797c4f8..935ef4dd17eb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java @@ -16,18 +16,11 @@ package org.springframework.boot.autoconfigure.task; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.task.TaskExecutionProperties.Shutdown; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.task.TaskExecutorBuilder; -import org.springframework.boot.task.TaskExecutorCustomizer; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; -import org.springframework.core.task.TaskDecorator; import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; @@ -36,12 +29,15 @@ * * @author Stephane Nicoll * @author Camille Vienot + * @author Moritz Halbritter * @since 2.1.0 */ @ConditionalOnClass(ThreadPoolTaskExecutor.class) @AutoConfiguration @EnableConfigurationProperties(TaskExecutionProperties.class) -@Import({ TaskExecutorConfigurations.VirtualThreadTaskExecutorConfiguration.class, +@Import({ TaskExecutorConfigurations.ThreadPoolTaskExecutorBuilderConfiguration.class, + TaskExecutorConfigurations.TaskExecutorBuilderConfiguration.class, + TaskExecutorConfigurations.VirtualThreadTaskExecutorConfiguration.class, TaskExecutorConfigurations.ThreadPoolTaskExecutorConfiguration.class }) public class TaskExecutionAutoConfiguration { @@ -50,25 +46,4 @@ public class TaskExecutionAutoConfiguration { */ public static final String APPLICATION_TASK_EXECUTOR_BEAN_NAME = "applicationTaskExecutor"; - @Bean - @ConditionalOnMissingBean - public TaskExecutorBuilder taskExecutorBuilder(TaskExecutionProperties properties, - ObjectProvider taskExecutorCustomizers, - ObjectProvider taskDecorator) { - TaskExecutionProperties.Pool pool = properties.getPool(); - TaskExecutorBuilder builder = new TaskExecutorBuilder(); - builder = builder.queueCapacity(pool.getQueueCapacity()); - builder = builder.corePoolSize(pool.getCoreSize()); - builder = builder.maxPoolSize(pool.getMaxSize()); - builder = builder.allowCoreThreadTimeOut(pool.isAllowCoreThreadTimeout()); - builder = builder.keepAlive(pool.getKeepAlive()); - Shutdown shutdown = properties.getShutdown(); - builder = builder.awaitTermination(shutdown.isAwaitTermination()); - builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod()); - builder = builder.threadNamePrefix(properties.getThreadNamePrefix()); - builder = builder.customizers(taskExecutorCustomizers.orderedStream()::iterator); - builder = builder.taskDecorator(taskDecorator.getIfUnique()); - return builder; - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java index 2e9646b90aae..390e3b0f174c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java @@ -21,8 +21,12 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading; +import org.springframework.boot.autoconfigure.task.TaskExecutionProperties.Shutdown; import org.springframework.boot.autoconfigure.thread.Threading; import org.springframework.boot.task.TaskExecutorBuilder; +import org.springframework.boot.task.TaskExecutorCustomizer; +import org.springframework.boot.task.ThreadPoolTaskExecutorBuilder; +import org.springframework.boot.task.ThreadPoolTaskExecutorCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; @@ -37,6 +41,7 @@ * {@link TaskExecutionAutoConfiguration} in a specific order. * * @author Andy Wilkinson + * @author Moritz Halbritter */ class TaskExecutorConfigurations { @@ -59,13 +64,82 @@ SimpleAsyncTaskExecutor applicationTaskExecutor(TaskExecutionProperties properti @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(Executor.class) + @SuppressWarnings("removal") static class ThreadPoolTaskExecutorConfiguration { @Lazy @Bean(name = { TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME, AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME }) - ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) { - return builder.build(); + ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder taskExecutorBuilder, + ObjectProvider threadPoolTaskExecutorBuilderProvider) { + ThreadPoolTaskExecutorBuilder threadPoolTaskExecutorBuilder = threadPoolTaskExecutorBuilderProvider + .getIfUnique(); + if (threadPoolTaskExecutorBuilder != null) { + return threadPoolTaskExecutorBuilder.build(); + } + return taskExecutorBuilder.build(); + } + + } + + @Configuration(proxyBeanMethods = false) + @SuppressWarnings("removal") + static class TaskExecutorBuilderConfiguration { + + @Bean + @ConditionalOnMissingBean + @Deprecated(since = "3.2.0", forRemoval = true) + TaskExecutorBuilder taskExecutorBuilder(TaskExecutionProperties properties, + ObjectProvider taskExecutorCustomizers, + ObjectProvider taskDecorator) { + TaskExecutionProperties.Pool pool = properties.getPool(); + TaskExecutorBuilder builder = new TaskExecutorBuilder(); + builder = builder.queueCapacity(pool.getQueueCapacity()); + builder = builder.corePoolSize(pool.getCoreSize()); + builder = builder.maxPoolSize(pool.getMaxSize()); + builder = builder.allowCoreThreadTimeOut(pool.isAllowCoreThreadTimeout()); + builder = builder.keepAlive(pool.getKeepAlive()); + Shutdown shutdown = properties.getShutdown(); + builder = builder.awaitTermination(shutdown.isAwaitTermination()); + builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod()); + builder = builder.threadNamePrefix(properties.getThreadNamePrefix()); + builder = builder.customizers(taskExecutorCustomizers.orderedStream()::iterator); + builder = builder.taskDecorator(taskDecorator.getIfUnique()); + return builder; + } + + } + + @Configuration(proxyBeanMethods = false) + @SuppressWarnings("removal") + static class ThreadPoolTaskExecutorBuilderConfiguration { + + @Bean + @ConditionalOnMissingBean({ TaskExecutorBuilder.class, ThreadPoolTaskExecutorBuilder.class }) + ThreadPoolTaskExecutorBuilder threadPoolTaskExecutorBuilder(TaskExecutionProperties properties, + ObjectProvider threadPoolTaskExecutorCustomizers, + ObjectProvider taskExecutorCustomizers, + ObjectProvider taskDecorator) { + TaskExecutionProperties.Pool pool = properties.getPool(); + ThreadPoolTaskExecutorBuilder builder = new ThreadPoolTaskExecutorBuilder(); + builder = builder.queueCapacity(pool.getQueueCapacity()); + builder = builder.corePoolSize(pool.getCoreSize()); + builder = builder.maxPoolSize(pool.getMaxSize()); + builder = builder.allowCoreThreadTimeOut(pool.isAllowCoreThreadTimeout()); + builder = builder.keepAlive(pool.getKeepAlive()); + Shutdown shutdown = properties.getShutdown(); + builder = builder.awaitTermination(shutdown.isAwaitTermination()); + builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod()); + builder = builder.threadNamePrefix(properties.getThreadNamePrefix()); + builder = builder.customizers(threadPoolTaskExecutorCustomizers.orderedStream()::iterator); + builder = builder.taskDecorator(taskDecorator.getIfUnique()); + // Apply the deprecated TaskExecutorCustomizers, too + builder = builder.additionalCustomizers(taskExecutorCustomizers.orderedStream().map(this::adapt).toList()); + return builder; + } + + private ThreadPoolTaskExecutorCustomizer adapt(TaskExecutorCustomizer customizer) { + return customizer::customize; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java index 8a1e09e922f0..5e79e8eeb208 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java @@ -25,7 +25,7 @@ import java.util.function.Consumer; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledForJreRange; +import org.junit.jupiter.api.condition.EnabledForJreRange; import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.extension.ExtendWith; @@ -33,6 +33,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.task.TaskExecutorBuilder; import org.springframework.boot.task.TaskExecutorCustomizer; +import org.springframework.boot.task.ThreadPoolTaskExecutorBuilder; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ContextConsumer; @@ -57,13 +58,33 @@ * * @author Stephane Nicoll * @author Camille Vienot + * @author Moritz Halbritter */ @ExtendWith(OutputCaptureExtension.class) +@SuppressWarnings("removal") class TaskExecutionAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class)); + @Test + void shouldSupplyBeans() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(TaskExecutorBuilder.class); + assertThat(context).hasSingleBean(ThreadPoolTaskExecutorBuilder.class); + assertThat(context).hasSingleBean(ThreadPoolTaskExecutor.class); + }); + } + + @Test + void shouldNotSupplyThreadPoolTaskExecutorBuilderIfCustomTaskExecutorBuilderIsPresent() { + this.contextRunner.withBean(TaskExecutorBuilder.class, TaskExecutorBuilder::new).run((context) -> { + assertThat(context).hasSingleBean(TaskExecutorBuilder.class); + assertThat(context).doesNotHaveBean(ThreadPoolTaskExecutorBuilder.class); + assertThat(context).hasSingleBean(ThreadPoolTaskExecutor.class); + }); + } + @Test void taskExecutorBuilderShouldApplyCustomSettings() { this.contextRunner @@ -85,6 +106,27 @@ void taskExecutorBuilderShouldApplyCustomSettings() { })); } + @Test + void threadPoolTaskExecutorBuilderShouldApplyCustomSettings() { + this.contextRunner + .withPropertyValues("spring.task.execution.pool.queue-capacity=10", + "spring.task.execution.pool.core-size=2", "spring.task.execution.pool.max-size=4", + "spring.task.execution.pool.allow-core-thread-timeout=true", + "spring.task.execution.pool.keep-alive=5s", "spring.task.execution.shutdown.await-termination=true", + "spring.task.execution.shutdown.await-termination-period=30s", + "spring.task.execution.thread-name-prefix=mytest-") + .run(assertThreadPoolTaskExecutor((taskExecutor) -> { + assertThat(taskExecutor).hasFieldOrPropertyWithValue("queueCapacity", 10); + assertThat(taskExecutor.getCorePoolSize()).isEqualTo(2); + assertThat(taskExecutor.getMaxPoolSize()).isEqualTo(4); + assertThat(taskExecutor).hasFieldOrPropertyWithValue("allowCoreThreadTimeOut", true); + assertThat(taskExecutor.getKeepAliveSeconds()).isEqualTo(5); + assertThat(taskExecutor).hasFieldOrPropertyWithValue("waitForTasksToCompleteOnShutdown", true); + assertThat(taskExecutor).hasFieldOrPropertyWithValue("awaitTerminationMillis", 30000L); + assertThat(taskExecutor.getThreadNamePrefix()).isEqualTo("mytest-"); + })); + } + @Test void taskExecutorBuilderWhenHasCustomBuilderShouldUseCustomBuilder() { this.contextRunner.withUserConfiguration(CustomTaskExecutorBuilderConfig.class).run((context) -> { @@ -94,6 +136,15 @@ void taskExecutorBuilderWhenHasCustomBuilderShouldUseCustomBuilder() { }); } + @Test + void threadPoolTaskExecutorBuilderWhenHasCustomBuilderShouldUseCustomBuilder() { + this.contextRunner.withUserConfiguration(CustomThreadPoolTaskExecutorBuilderConfig.class).run((context) -> { + assertThat(context).hasSingleBean(ThreadPoolTaskExecutorBuilder.class); + assertThat(context.getBean(ThreadPoolTaskExecutorBuilder.class)) + .isSameAs(context.getBean(CustomThreadPoolTaskExecutorBuilderConfig.class).builder); + }); + } + @Test void taskExecutorBuilderShouldUseTaskDecorator() { this.contextRunner.withUserConfiguration(TaskDecoratorConfig.class).run((context) -> { @@ -103,6 +154,15 @@ void taskExecutorBuilderShouldUseTaskDecorator() { }); } + @Test + void threadPoolTaskExecutorBuilderShouldUseTaskDecorator() { + this.contextRunner.withUserConfiguration(TaskDecoratorConfig.class).run((context) -> { + assertThat(context).hasSingleBean(ThreadPoolTaskExecutorBuilder.class); + ThreadPoolTaskExecutor executor = context.getBean(ThreadPoolTaskExecutorBuilder.class).build(); + assertThat(executor).extracting("taskDecorator").isSameAs(context.getBean(TaskDecorator.class)); + }); + } + @Test void whenThreadPoolTaskExecutorIsAutoConfiguredThenItIsLazy() { this.contextRunner.run((context) -> { @@ -116,7 +176,7 @@ void whenThreadPoolTaskExecutorIsAutoConfiguredThenItIsLazy() { } @Test - @DisabledForJreRange(max = JRE.JAVA_20) + @EnabledForJreRange(min = JRE.JAVA_21) void whenVirtualThreadsAreEnabledThenSimpleAsyncTaskExecutorWithVirtualThreadsIsAutoConfigured() { this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> { assertThat(context).hasSingleBean(Executor.class).hasBean("applicationTaskExecutor"); @@ -128,7 +188,7 @@ void whenVirtualThreadsAreEnabledThenSimpleAsyncTaskExecutorWithVirtualThreadsIs } @Test - @DisabledForJreRange(max = JRE.JAVA_20) + @EnabledForJreRange(min = JRE.JAVA_21) void whenTaskNamePrefixIsConfiguredThenSimpleAsyncTaskExecutorWithVirtualThreadsUsesIt() { this.contextRunner .withPropertyValues("spring.threads.virtual.enabled=true", @@ -141,7 +201,7 @@ void whenTaskNamePrefixIsConfiguredThenSimpleAsyncTaskExecutorWithVirtualThreads } @Test - @DisabledForJreRange(max = JRE.JAVA_20) + @EnabledForJreRange(min = JRE.JAVA_21) void whenVirtualThreadsAreAvailableButNotEnabledThenThreadPoolTaskExecutorIsAutoConfigured() { this.contextRunner.run((context) -> { assertThat(context).hasSingleBean(Executor.class).hasBean("applicationTaskExecutor"); @@ -150,7 +210,7 @@ void whenVirtualThreadsAreAvailableButNotEnabledThenThreadPoolTaskExecutorIsAuto } @Test - @DisabledForJreRange(max = JRE.JAVA_20) + @EnabledForJreRange(min = JRE.JAVA_21) void whenTaskDecoratorIsDefinedThenSimpleAsyncTaskExecutorWithVirtualThreadsUsesIt() { this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true") .withUserConfiguration(TaskDecoratorConfig.class) @@ -169,7 +229,7 @@ void taskExecutorWhenHasCustomTaskExecutorShouldBackOff() { } @Test - @DisabledForJreRange(max = JRE.JAVA_20) + @EnabledForJreRange(min = JRE.JAVA_21) void whenVirtualThreadsAreEnabledAndCustomTaskExecutorIsDefinedThenSimpleAsyncTaskExecutorThatUsesVirtualThreadsBacksOff() { this.contextRunner.withUserConfiguration(CustomTaskExecutorConfig.class) .withPropertyValues("spring.threads.virtual.enabled=true") @@ -188,6 +248,15 @@ void taskExecutorBuilderShouldApplyCustomizer() { }); } + @Test + void threadPoolTaskExecutorBuilderShouldApplyCustomizer() { + this.contextRunner.withUserConfiguration(TaskExecutorCustomizerConfig.class).run((context) -> { + TaskExecutorCustomizer customizer = context.getBean(TaskExecutorCustomizer.class); + ThreadPoolTaskExecutor executor = context.getBean(ThreadPoolTaskExecutorBuilder.class).build(); + then(customizer).should().customize(executor); + }); + } + @Test void enableAsyncUsesAutoConfiguredOneByDefault() { this.contextRunner.withPropertyValues("spring.task.execution.thread-name-prefix=task-test-") @@ -212,6 +281,25 @@ void enableAsyncUsesAutoConfiguredOneByDefaultEvenThoughSchedulingIsConfigured() }); } + @Test + void customTaskExecutorBuilderOverridesThreadPoolTaskExecutorBuilder() { + this.contextRunner.withUserConfiguration(CustomTaskExecutorBuilderConfig.class).run((context) -> { + ThreadPoolTaskExecutor bean = context.getBean(ThreadPoolTaskExecutor.class); + assertThat(bean.getThreadNamePrefix()).isEqualTo("CustomTaskExecutorBuilderConfig-"); + }); + } + + @Test + void threadPoolTaskExecutorBuilderAppliesTaskExecutorCustomizer() { + this.contextRunner + .withBean(TaskExecutorCustomizer.class, + () -> (taskExecutor) -> taskExecutor.setThreadNamePrefix("custom-prefix-")) + .run((context) -> { + ThreadPoolTaskExecutor bean = context.getBean(ThreadPoolTaskExecutor.class); + assertThat(bean.getThreadNamePrefix()).isEqualTo("custom-prefix-"); + }); + } + private ContextConsumer assertTaskExecutor( Consumer taskExecutor) { return (context) -> { @@ -221,6 +309,15 @@ private ContextConsumer assertTaskExecutor( }; } + private ContextConsumer assertThreadPoolTaskExecutor( + Consumer taskExecutor) { + return (context) -> { + assertThat(context).hasSingleBean(ThreadPoolTaskExecutorBuilder.class); + ThreadPoolTaskExecutorBuilder builder = context.getBean(ThreadPoolTaskExecutorBuilder.class); + taskExecutor.accept(builder.build()); + }; + } + private String virtualThreadName(SimpleAsyncTaskExecutor taskExecutor) throws InterruptedException { AtomicReference threadReference = new AtomicReference<>(); CountDownLatch latch = new CountDownLatch(1); @@ -238,7 +335,8 @@ private String virtualThreadName(SimpleAsyncTaskExecutor taskExecutor) throws In @Configuration(proxyBeanMethods = false) static class CustomTaskExecutorBuilderConfig { - private final TaskExecutorBuilder taskExecutorBuilder = new TaskExecutorBuilder(); + private final TaskExecutorBuilder taskExecutorBuilder = new TaskExecutorBuilder() + .threadNamePrefix("CustomTaskExecutorBuilderConfig-"); @Bean TaskExecutorBuilder customTaskExecutorBuilder() { @@ -247,6 +345,18 @@ TaskExecutorBuilder customTaskExecutorBuilder() { } + @Configuration(proxyBeanMethods = false) + static class CustomThreadPoolTaskExecutorBuilderConfig { + + private final ThreadPoolTaskExecutorBuilder builder = new ThreadPoolTaskExecutorBuilder(); + + @Bean + ThreadPoolTaskExecutorBuilder customThreadPoolTaskExecutorBuilder() { + return this.builder; + } + + } + @Configuration(proxyBeanMethods = false) static class TaskExecutorCustomizerConfig { diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc index 9ae721385599..6126919354fc 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc @@ -16,7 +16,7 @@ If you have defined a custom `Executor` in the context, both regular task execut However, the Spring MVC and Spring WebFlux support will only use it if it is an `AsyncTaskExecutor` implementation (named `applicationTaskExecutor`). Depending on your target arrangement, you could change your `Executor` into an `AsyncTaskExecutor` or define both an `AsyncTaskExecutor` and an `AsyncConfigurer` wrapping your custom `Executor`. -The auto-configured `TaskExecutorBuilder` allows you to easily create instances that reproduce what the auto-configuration does by default. +The auto-configured `ThreadPoolTaskExecutorBuilder` allows you to easily create instances that reproduce what the auto-configuration does by default. ==== When a `ThreadPoolTaskExecutor` is auto-configured, the thread pool uses 8 core threads that can grow and shrink according to the load. @@ -49,4 +49,4 @@ The thread pool uses one thread by default and its settings can be fine-tuned us size: 2 ---- -Both a `TaskExecutorBuilder` bean and a `TaskSchedulerBuilder` bean are made available in the context if a custom executor or scheduler needs to be created. +Both a `ThreadPoolTaskExecutorBuilder` bean and a `TaskSchedulerBuilder` bean are made available in the context if a custom executor or scheduler needs to be created. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskExecutorBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskExecutorBuilder.java index 34b45a83176b..304b9ce9320c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskExecutorBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskExecutorBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -42,7 +42,11 @@ * @author Stephane Nicoll * @author Filip Hrisafov * @since 2.1.0 + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of + * {@link ThreadPoolTaskExecutorBuilder} */ +@Deprecated(since = "3.2.0", forRemoval = true) +@SuppressWarnings("removal") public class TaskExecutorBuilder { private final Integer queueCapacity; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskExecutorCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskExecutorCustomizer.java index 4ceed9047b02..0ff969caaba7 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskExecutorCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskExecutorCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. @@ -24,8 +24,11 @@ * @author Stephane Nicoll * @since 2.1.0 * @see TaskExecutorBuilder + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of + * {@link ThreadPoolTaskExecutorCustomizer} */ @FunctionalInterface +@Deprecated(since = "3.2.0", forRemoval = true) public interface TaskExecutorCustomizer { /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilder.java new file mode 100644 index 000000000000..2609245832ad --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilder.java @@ -0,0 +1,326 @@ +/* + * Copyright 2012-2023 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.task; + +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.beans.BeanUtils; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.core.task.TaskDecorator; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Builder that can be used to configure and create a {@link ThreadPoolTaskExecutor}. + * Provides convenience methods to set common {@link ThreadPoolTaskExecutor} settings and + * register {@link #taskDecorator(TaskDecorator)}). For advanced configuration, consider + * using {@link ThreadPoolTaskExecutorCustomizer}. + *

+ * In a typical auto-configured Spring Boot application this builder is available as a + * bean and can be injected whenever a {@link ThreadPoolTaskExecutor} is needed. + * + * @author Stephane Nicoll + * @author Filip Hrisafov + * @since 3.2.0 + */ +public class ThreadPoolTaskExecutorBuilder { + + private final Integer queueCapacity; + + private final Integer corePoolSize; + + private final Integer maxPoolSize; + + private final Boolean allowCoreThreadTimeOut; + + private final Duration keepAlive; + + private final Boolean awaitTermination; + + private final Duration awaitTerminationPeriod; + + private final String threadNamePrefix; + + private final TaskDecorator taskDecorator; + + private final Set customizers; + + public ThreadPoolTaskExecutorBuilder() { + this.queueCapacity = null; + this.corePoolSize = null; + this.maxPoolSize = null; + this.allowCoreThreadTimeOut = null; + this.keepAlive = null; + this.awaitTermination = null; + this.awaitTerminationPeriod = null; + this.threadNamePrefix = null; + this.taskDecorator = null; + this.customizers = null; + } + + private ThreadPoolTaskExecutorBuilder(Integer queueCapacity, Integer corePoolSize, Integer maxPoolSize, + Boolean allowCoreThreadTimeOut, Duration keepAlive, Boolean awaitTermination, + Duration awaitTerminationPeriod, String threadNamePrefix, TaskDecorator taskDecorator, + Set customizers) { + this.queueCapacity = queueCapacity; + this.corePoolSize = corePoolSize; + this.maxPoolSize = maxPoolSize; + this.allowCoreThreadTimeOut = allowCoreThreadTimeOut; + this.keepAlive = keepAlive; + this.awaitTermination = awaitTermination; + this.awaitTerminationPeriod = awaitTerminationPeriod; + this.threadNamePrefix = threadNamePrefix; + this.taskDecorator = taskDecorator; + this.customizers = customizers; + } + + /** + * Set the capacity of the queue. An unbounded capacity does not increase the pool and + * therefore ignores {@link #maxPoolSize(int) maxPoolSize}. + * @param queueCapacity the queue capacity to set + * @return a new builder instance + */ + public ThreadPoolTaskExecutorBuilder queueCapacity(int queueCapacity) { + return new ThreadPoolTaskExecutorBuilder(queueCapacity, this.corePoolSize, this.maxPoolSize, + this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod, + this.threadNamePrefix, this.taskDecorator, this.customizers); + } + + /** + * Set the core number of threads. Effectively that maximum number of threads as long + * as the queue is not full. + *

+ * Core threads can grow and shrink if {@link #allowCoreThreadTimeOut(boolean)} is + * enabled. + * @param corePoolSize the core pool size to set + * @return a new builder instance + */ + public ThreadPoolTaskExecutorBuilder corePoolSize(int corePoolSize) { + return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, corePoolSize, this.maxPoolSize, + this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod, + this.threadNamePrefix, this.taskDecorator, this.customizers); + } + + /** + * Set the maximum allowed number of threads. When the {@link #queueCapacity(int) + * queue} is full, the pool can expand up to that size to accommodate the load. + *

+ * If the {@link #queueCapacity(int) queue capacity} is unbounded, this setting is + * ignored. + * @param maxPoolSize the max pool size to set + * @return a new builder instance + */ + public ThreadPoolTaskExecutorBuilder maxPoolSize(int maxPoolSize) { + return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, maxPoolSize, + this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod, + this.threadNamePrefix, this.taskDecorator, this.customizers); + } + + /** + * Set whether core threads are allowed to time out. When enabled, this enables + * dynamic growing and shrinking of the pool. + * @param allowCoreThreadTimeOut if core threads are allowed to time out + * @return a new builder instance + */ + public ThreadPoolTaskExecutorBuilder allowCoreThreadTimeOut(boolean allowCoreThreadTimeOut) { + return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize, + allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod, + this.threadNamePrefix, this.taskDecorator, this.customizers); + } + + /** + * Set the time limit for which threads may remain idle before being terminated. + * @param keepAlive the keep alive to set + * @return a new builder instance + */ + public ThreadPoolTaskExecutorBuilder keepAlive(Duration keepAlive) { + return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize, + this.allowCoreThreadTimeOut, keepAlive, this.awaitTermination, this.awaitTerminationPeriod, + this.threadNamePrefix, this.taskDecorator, this.customizers); + } + + /** + * Set whether the executor should wait for scheduled tasks to complete on shutdown, + * not interrupting running tasks and executing all tasks in the queue. + * @param awaitTermination whether the executor needs to wait for the tasks to + * complete on shutdown + * @return a new builder instance + * @see #awaitTerminationPeriod(Duration) + */ + public ThreadPoolTaskExecutorBuilder awaitTermination(boolean awaitTermination) { + return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize, + this.allowCoreThreadTimeOut, this.keepAlive, awaitTermination, this.awaitTerminationPeriod, + this.threadNamePrefix, this.taskDecorator, this.customizers); + } + + /** + * Set the maximum time the executor is supposed to block on shutdown. When set, the + * executor blocks on shutdown in order to wait for remaining tasks to complete their + * execution before the rest of the container continues to shut down. This is + * particularly useful if your remaining tasks are likely to need access to other + * resources that are also managed by the container. + * @param awaitTerminationPeriod the await termination period to set + * @return a new builder instance + */ + public ThreadPoolTaskExecutorBuilder awaitTerminationPeriod(Duration awaitTerminationPeriod) { + return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize, + this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, awaitTerminationPeriod, + this.threadNamePrefix, this.taskDecorator, this.customizers); + } + + /** + * Set the prefix to use for the names of newly created threads. + * @param threadNamePrefix the thread name prefix to set + * @return a new builder instance + */ + public ThreadPoolTaskExecutorBuilder threadNamePrefix(String threadNamePrefix) { + return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize, + this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod, + threadNamePrefix, this.taskDecorator, this.customizers); + } + + /** + * Set the {@link TaskDecorator} to use or {@code null} to not use any. + * @param taskDecorator the task decorator to use + * @return a new builder instance + */ + public ThreadPoolTaskExecutorBuilder taskDecorator(TaskDecorator taskDecorator) { + return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize, + this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod, + this.threadNamePrefix, taskDecorator, this.customizers); + } + + /** + * Set the {@link ThreadPoolTaskExecutorCustomizer ThreadPoolTaskExecutorCustomizers} + * that should be applied to the {@link ThreadPoolTaskExecutor}. Customizers are + * applied in the order that they were added after builder configuration has been + * applied. Setting this value will replace any previously configured customizers. + * @param customizers the customizers to set + * @return a new builder instance + * @see #additionalCustomizers(ThreadPoolTaskExecutorCustomizer...) + */ + public ThreadPoolTaskExecutorBuilder customizers(ThreadPoolTaskExecutorCustomizer... customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return customizers(Arrays.asList(customizers)); + } + + /** + * Set the {@link ThreadPoolTaskExecutorCustomizer ThreadPoolTaskExecutorCustomizers} + * that should be applied to the {@link ThreadPoolTaskExecutor}. Customizers are + * applied in the order that they were added after builder configuration has been + * applied. Setting this value will replace any previously configured customizers. + * @param customizers the customizers to set + * @return a new builder instance + * @see #additionalCustomizers(ThreadPoolTaskExecutorCustomizer...) + */ + public ThreadPoolTaskExecutorBuilder customizers(Iterable customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize, + this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod, + this.threadNamePrefix, this.taskDecorator, append(null, customizers)); + } + + /** + * Add {@link ThreadPoolTaskExecutorCustomizer ThreadPoolTaskExecutorCustomizers} that + * should be applied to the {@link ThreadPoolTaskExecutor}. Customizers are applied in + * the order that they were added after builder configuration has been applied. + * @param customizers the customizers to add + * @return a new builder instance + * @see #customizers(ThreadPoolTaskExecutorCustomizer...) + */ + public ThreadPoolTaskExecutorBuilder additionalCustomizers(ThreadPoolTaskExecutorCustomizer... customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return additionalCustomizers(Arrays.asList(customizers)); + } + + /** + * Add {@link ThreadPoolTaskExecutorCustomizer ThreadPoolTaskExecutorCustomizers} that + * should be applied to the {@link ThreadPoolTaskExecutor}. Customizers are applied in + * the order that they were added after builder configuration has been applied. + * @param customizers the customizers to add + * @return a new builder instance + * @see #customizers(ThreadPoolTaskExecutorCustomizer...) + */ + public ThreadPoolTaskExecutorBuilder additionalCustomizers( + Iterable customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize, + this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod, + this.threadNamePrefix, this.taskDecorator, append(this.customizers, customizers)); + } + + /** + * Build a new {@link ThreadPoolTaskExecutor} instance and configure it using this + * builder. + * @return a configured {@link ThreadPoolTaskExecutor} instance. + * @see #build(Class) + * @see #configure(ThreadPoolTaskExecutor) + */ + public ThreadPoolTaskExecutor build() { + return configure(new ThreadPoolTaskExecutor()); + } + + /** + * Build a new {@link ThreadPoolTaskExecutor} instance of the specified type and + * configure it using this builder. + * @param the type of task executor + * @param taskExecutorClass the template type to create + * @return a configured {@link ThreadPoolTaskExecutor} instance. + * @see #build() + * @see #configure(ThreadPoolTaskExecutor) + */ + public T build(Class taskExecutorClass) { + return configure(BeanUtils.instantiateClass(taskExecutorClass)); + } + + /** + * Configure the provided {@link ThreadPoolTaskExecutor} instance using this builder. + * @param the type of task executor + * @param taskExecutor the {@link ThreadPoolTaskExecutor} to configure + * @return the task executor instance + * @see #build() + * @see #build(Class) + */ + public T configure(T taskExecutor) { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(this.queueCapacity).to(taskExecutor::setQueueCapacity); + map.from(this.corePoolSize).to(taskExecutor::setCorePoolSize); + map.from(this.maxPoolSize).to(taskExecutor::setMaxPoolSize); + map.from(this.keepAlive).asInt(Duration::getSeconds).to(taskExecutor::setKeepAliveSeconds); + map.from(this.allowCoreThreadTimeOut).to(taskExecutor::setAllowCoreThreadTimeOut); + map.from(this.awaitTermination).to(taskExecutor::setWaitForTasksToCompleteOnShutdown); + map.from(this.awaitTerminationPeriod).as(Duration::toMillis).to(taskExecutor::setAwaitTerminationMillis); + map.from(this.threadNamePrefix).whenHasText().to(taskExecutor::setThreadNamePrefix); + map.from(this.taskDecorator).to(taskExecutor::setTaskDecorator); + if (!CollectionUtils.isEmpty(this.customizers)) { + this.customizers.forEach((customizer) -> customizer.customize(taskExecutor)); + } + return taskExecutor; + } + + private Set append(Set set, Iterable additions) { + Set result = new LinkedHashSet<>((set != null) ? set : Collections.emptySet()); + additions.forEach(result::add); + return Collections.unmodifiableSet(result); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorCustomizer.java new file mode 100644 index 000000000000..c81c5bfe7985 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2023 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.task; + +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +/** + * Callback interface that can be used to customize a {@link ThreadPoolTaskExecutor}. + * + * @author Stephane Nicoll + * @since 3.2.0 + * @see ThreadPoolTaskExecutorBuilder + */ +@FunctionalInterface +public interface ThreadPoolTaskExecutorCustomizer { + + /** + * Callback to customize a {@link ThreadPoolTaskExecutor} instance. + * @param taskExecutor the task executor to customize + */ + void customize(ThreadPoolTaskExecutor taskExecutor); + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/TaskExecutorBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/TaskExecutorBuilderTests.java index 52c205e8ed60..7df760b63f82 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/TaskExecutorBuilderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/TaskExecutorBuilderTests.java @@ -37,6 +37,7 @@ * @author Stephane Nicoll * @author Filip Hrisafov */ +@SuppressWarnings("removal") class TaskExecutorBuilderTests { private final TaskExecutorBuilder builder = new TaskExecutorBuilder(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilderTests.java new file mode 100644 index 000000000000..b57ffc6905a9 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilderTests.java @@ -0,0 +1,169 @@ +/* + * Copyright 2012-2023 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.task; + +import java.time.Duration; +import java.util.Collections; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.task.TaskDecorator; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +/** + * Tests for {@link ThreadPoolTaskExecutorBuilder}. + * + * @author Stephane Nicoll + * @author Filip Hrisafov + */ +class ThreadPoolTaskExecutorBuilderTests { + + private final ThreadPoolTaskExecutorBuilder builder = new ThreadPoolTaskExecutorBuilder(); + + @Test + void poolSettingsShouldApply() { + ThreadPoolTaskExecutor executor = this.builder.queueCapacity(10) + .corePoolSize(4) + .maxPoolSize(8) + .allowCoreThreadTimeOut(true) + .keepAlive(Duration.ofMinutes(1)) + .build(); + assertThat(executor).hasFieldOrPropertyWithValue("queueCapacity", 10); + assertThat(executor.getCorePoolSize()).isEqualTo(4); + assertThat(executor.getMaxPoolSize()).isEqualTo(8); + assertThat(executor).hasFieldOrPropertyWithValue("allowCoreThreadTimeOut", true); + assertThat(executor.getKeepAliveSeconds()).isEqualTo(60); + } + + @Test + void awaitTerminationShouldApply() { + ThreadPoolTaskExecutor executor = this.builder.awaitTermination(true).build(); + assertThat(executor).hasFieldOrPropertyWithValue("waitForTasksToCompleteOnShutdown", true); + } + + @Test + void awaitTerminationPeriodShouldApplyWithMillisecondPrecision() { + Duration period = Duration.ofMillis(50); + ThreadPoolTaskExecutor executor = this.builder.awaitTerminationPeriod(period).build(); + assertThat(executor).hasFieldOrPropertyWithValue("awaitTerminationMillis", period.toMillis()); + } + + @Test + void threadNamePrefixShouldApply() { + ThreadPoolTaskExecutor executor = this.builder.threadNamePrefix("test-").build(); + assertThat(executor.getThreadNamePrefix()).isEqualTo("test-"); + } + + @Test + void taskDecoratorShouldApply() { + TaskDecorator taskDecorator = mock(TaskDecorator.class); + ThreadPoolTaskExecutor executor = this.builder.taskDecorator(taskDecorator).build(); + assertThat(executor).extracting("taskDecorator").isSameAs(taskDecorator); + } + + @Test + void customizersWhenCustomizersAreNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.builder.customizers((ThreadPoolTaskExecutorCustomizer[]) null)) + .withMessageContaining("Customizers must not be null"); + } + + @Test + void customizersCollectionWhenCustomizersAreNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.builder.customizers((Set) null)) + .withMessageContaining("Customizers must not be null"); + } + + @Test + void customizersShouldApply() { + ThreadPoolTaskExecutorCustomizer customizer = mock(ThreadPoolTaskExecutorCustomizer.class); + ThreadPoolTaskExecutor executor = this.builder.customizers(customizer).build(); + then(customizer).should().customize(executor); + } + + @Test + void customizersShouldBeAppliedLast() { + TaskDecorator taskDecorator = mock(TaskDecorator.class); + ThreadPoolTaskExecutor executor = spy(new ThreadPoolTaskExecutor()); + this.builder.queueCapacity(10) + .corePoolSize(4) + .maxPoolSize(8) + .allowCoreThreadTimeOut(true) + .keepAlive(Duration.ofMinutes(1)) + .awaitTermination(true) + .awaitTerminationPeriod(Duration.ofSeconds(30)) + .threadNamePrefix("test-") + .taskDecorator(taskDecorator) + .additionalCustomizers((taskExecutor) -> { + then(taskExecutor).should().setQueueCapacity(10); + then(taskExecutor).should().setCorePoolSize(4); + then(taskExecutor).should().setMaxPoolSize(8); + then(taskExecutor).should().setAllowCoreThreadTimeOut(true); + then(taskExecutor).should().setKeepAliveSeconds(60); + then(taskExecutor).should().setWaitForTasksToCompleteOnShutdown(true); + then(taskExecutor).should().setAwaitTerminationSeconds(30); + then(taskExecutor).should().setThreadNamePrefix("test-"); + then(taskExecutor).should().setTaskDecorator(taskDecorator); + }); + this.builder.configure(executor); + } + + @Test + void customizersShouldReplaceExisting() { + ThreadPoolTaskExecutorCustomizer customizer1 = mock(ThreadPoolTaskExecutorCustomizer.class); + ThreadPoolTaskExecutorCustomizer customizer2 = mock(ThreadPoolTaskExecutorCustomizer.class); + ThreadPoolTaskExecutor executor = this.builder.customizers(customizer1) + .customizers(Collections.singleton(customizer2)) + .build(); + then(customizer1).shouldHaveNoInteractions(); + then(customizer2).should().customize(executor); + } + + @Test + void additionalCustomizersWhenCustomizersAreNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.builder.additionalCustomizers((ThreadPoolTaskExecutorCustomizer[]) null)) + .withMessageContaining("Customizers must not be null"); + } + + @Test + void additionalCustomizersCollectionWhenCustomizersAreNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.builder.additionalCustomizers((Set) null)) + .withMessageContaining("Customizers must not be null"); + } + + @Test + void additionalCustomizersShouldAddToExisting() { + ThreadPoolTaskExecutorCustomizer customizer1 = mock(ThreadPoolTaskExecutorCustomizer.class); + ThreadPoolTaskExecutorCustomizer customizer2 = mock(ThreadPoolTaskExecutorCustomizer.class); + ThreadPoolTaskExecutor executor = this.builder.customizers(customizer1) + .additionalCustomizers(customizer2) + .build(); + then(customizer1).should().customize(executor); + then(customizer2).should().customize(executor); + } + +} From e4c38e59a9837cf63dd24f144f69eb81bc4fc7ad Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Mon, 31 Jul 2023 12:43:28 +0200 Subject: [PATCH 0211/1215] Implement SimpleAsyncTaskExecutorBuilder The SimpleAsyncTaskExecutorBuilder can be used to create SimpleAsyncTaskExecutor. It will be auto-configured into the context. SimpleAsyncTaskExecutorCustomizer can be used to customize the built SimpleAsyncTaskExecutor. If virtual threads are enabled: - SimpleAsyncTaskExecutor will use virtual threads - SimpleAsyncTaskExecutorBuilder will be used as the application task executor A new property 'spring.task.execution.simple.concurrency-limit' has been added to control the concurrency limit of the SimpleAsyncTaskExecutor Closes gh-35711 --- .../task/TaskExecutionAutoConfiguration.java | 1 + .../task/TaskExecutionProperties.java | 26 +- .../task/TaskExecutorConfigurations.java | 55 ++++- .../TaskExecutionAutoConfigurationTests.java | 44 +++- .../task/SimpleAsyncTaskExecutorBuilder.java | 222 ++++++++++++++++++ .../SimpleAsyncTaskExecutorCustomizer.java | 38 +++ .../SimpleAsyncTaskExecutorBuilderTests.java | 152 ++++++++++++ 7 files changed, 530 insertions(+), 8 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilder.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorCustomizer.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilderTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java index 935ef4dd17eb..8d6ef29d6769 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java @@ -37,6 +37,7 @@ @EnableConfigurationProperties(TaskExecutionProperties.class) @Import({ TaskExecutorConfigurations.ThreadPoolTaskExecutorBuilderConfiguration.class, TaskExecutorConfigurations.TaskExecutorBuilderConfiguration.class, + TaskExecutorConfigurations.SimpleAsyncTaskExecutorBuilderConfiguration.class, TaskExecutorConfigurations.VirtualThreadTaskExecutorConfiguration.class, TaskExecutorConfigurations.ThreadPoolTaskExecutorConfiguration.class }) public class TaskExecutionAutoConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java index c8bcc17ce999..9530f198289a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. @@ -32,6 +32,8 @@ public class TaskExecutionProperties { private final Pool pool = new Pool(); + private final Simple simple = new Simple(); + private final Shutdown shutdown = new Shutdown(); /** @@ -39,6 +41,10 @@ public class TaskExecutionProperties { */ private String threadNamePrefix = "task-"; + public Simple getSimple() { + return this.simple; + } + public Pool getPool() { return this.pool; } @@ -55,6 +61,24 @@ public void setThreadNamePrefix(String threadNamePrefix) { this.threadNamePrefix = threadNamePrefix; } + public static class Simple { + + /** + * Set the maximum number of parallel accesses allowed. -1 indicates no + * concurrency limit at all. + */ + private Integer concurrencyLimit; + + public Integer getConcurrencyLimit() { + return this.concurrencyLimit; + } + + public void setConcurrencyLimit(Integer concurrencyLimit) { + this.concurrencyLimit = concurrencyLimit; + } + + } + public static class Pool { /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java index 390e3b0f174c..91ce9daef4a5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java @@ -23,6 +23,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading; import org.springframework.boot.autoconfigure.task.TaskExecutionProperties.Shutdown; import org.springframework.boot.autoconfigure.thread.Threading; +import org.springframework.boot.task.SimpleAsyncTaskExecutorBuilder; +import org.springframework.boot.task.SimpleAsyncTaskExecutorCustomizer; import org.springframework.boot.task.TaskExecutorBuilder; import org.springframework.boot.task.TaskExecutorCustomizer; import org.springframework.boot.task.ThreadPoolTaskExecutorBuilder; @@ -52,12 +54,8 @@ static class VirtualThreadTaskExecutorConfiguration { @Bean(name = { TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME, AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME }) - SimpleAsyncTaskExecutor applicationTaskExecutor(TaskExecutionProperties properties, - ObjectProvider taskDecorator) { - SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(properties.getThreadNamePrefix()); - executor.setVirtualThreads(true); - taskDecorator.ifUnique(executor::setTaskDecorator); - return executor; + SimpleAsyncTaskExecutor applicationTaskExecutor(SimpleAsyncTaskExecutorBuilder builder) { + return builder.build(); } } @@ -144,4 +142,49 @@ private ThreadPoolTaskExecutorCustomizer adapt(TaskExecutorCustomizer customizer } + @Configuration(proxyBeanMethods = false) + static class SimpleAsyncTaskExecutorBuilderConfiguration { + + private final TaskExecutionProperties properties; + + private final ObjectProvider taskExecutorCustomizers; + + private final ObjectProvider taskDecorator; + + SimpleAsyncTaskExecutorBuilderConfiguration(TaskExecutionProperties properties, + ObjectProvider taskExecutorCustomizers, + ObjectProvider taskDecorator) { + this.properties = properties; + this.taskExecutorCustomizers = taskExecutorCustomizers; + this.taskDecorator = taskDecorator; + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnThreading(Threading.PLATFORM) + SimpleAsyncTaskExecutorBuilder simpleAsyncTaskExecutorBuilder() { + return builder(); + } + + @Bean(name = "simpleAsyncTaskExecutorBuilder") + @ConditionalOnMissingBean + @ConditionalOnThreading(Threading.VIRTUAL) + SimpleAsyncTaskExecutorBuilder simpleAsyncTaskExecutorBuilderVirtualThreads() { + SimpleAsyncTaskExecutorBuilder builder = builder(); + builder = builder.virtualThreads(true); + return builder; + } + + private SimpleAsyncTaskExecutorBuilder builder() { + SimpleAsyncTaskExecutorBuilder builder = new SimpleAsyncTaskExecutorBuilder(); + builder = builder.threadNamePrefix(this.properties.getThreadNamePrefix()); + builder = builder.customizers(this.taskExecutorCustomizers.orderedStream()::iterator); + builder = builder.taskDecorator(this.taskDecorator.getIfUnique()); + TaskExecutionProperties.Simple simple = this.properties.getSimple(); + builder = builder.concurrencyLimit(simple.getConcurrencyLimit()); + return builder; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java index 5e79e8eeb208..1a3149255ca8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java @@ -31,6 +31,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.task.SimpleAsyncTaskExecutorBuilder; import org.springframework.boot.task.TaskExecutorBuilder; import org.springframework.boot.task.TaskExecutorCustomizer; import org.springframework.boot.task.ThreadPoolTaskExecutorBuilder; @@ -50,6 +51,7 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; @@ -73,6 +75,7 @@ void shouldSupplyBeans() { assertThat(context).hasSingleBean(TaskExecutorBuilder.class); assertThat(context).hasSingleBean(ThreadPoolTaskExecutorBuilder.class); assertThat(context).hasSingleBean(ThreadPoolTaskExecutor.class); + assertThat(context).hasSingleBean(SimpleAsyncTaskExecutorBuilder.class); }); } @@ -106,6 +109,17 @@ void taskExecutorBuilderShouldApplyCustomSettings() { })); } + @Test + void simpleAsyncTaskExecutorBuilderShouldReadProperties() { + this.contextRunner + .withPropertyValues("spring.task.execution.thread-name-prefix=mytest-", + "spring.task.execution.simple.concurrency-limit=1") + .run(assertSimpleAsyncTaskExecutor((taskExecutor) -> { + assertThat(taskExecutor.getConcurrencyLimit()).isEqualTo(1); + assertThat(taskExecutor.getThreadNamePrefix()).isEqualTo("mytest-"); + })); + } + @Test void threadPoolTaskExecutorBuilderShouldApplyCustomSettings() { this.contextRunner @@ -220,6 +234,23 @@ void whenTaskDecoratorIsDefinedThenSimpleAsyncTaskExecutorWithVirtualThreadsUses }); } + @Test + void simpleAsyncTaskExecutorBuilderUsesPlatformThreadsByDefault() { + this.contextRunner.run((context) -> { + SimpleAsyncTaskExecutorBuilder builder = context.getBean(SimpleAsyncTaskExecutorBuilder.class); + assertThat(builder).hasFieldOrPropertyWithValue("virtualThreads", null); + }); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void simpleAsyncTaskExecutorBuilderUsesVirtualThreadsWhenEnabled() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> { + SimpleAsyncTaskExecutorBuilder builder = context.getBean(SimpleAsyncTaskExecutorBuilder.class); + assertThat(builder).hasFieldOrPropertyWithValue("virtualThreads", true); + }); + } + @Test void taskExecutorWhenHasCustomTaskExecutorShouldBackOff() { this.contextRunner.withUserConfiguration(CustomTaskExecutorConfig.class).run((context) -> { @@ -318,6 +349,15 @@ private ContextConsumer assertThreadPoolTaskExecut }; } + private ContextConsumer assertSimpleAsyncTaskExecutor( + Consumer taskExecutor) { + return (context) -> { + assertThat(context).hasSingleBean(SimpleAsyncTaskExecutorBuilder.class); + SimpleAsyncTaskExecutorBuilder builder = context.getBean(SimpleAsyncTaskExecutorBuilder.class); + taskExecutor.accept(builder.build()); + }; + } + private String virtualThreadName(SimpleAsyncTaskExecutor taskExecutor) throws InterruptedException { AtomicReference threadReference = new AtomicReference<>(); CountDownLatch latch = new CountDownLatch(1); @@ -326,7 +366,9 @@ private String virtualThreadName(SimpleAsyncTaskExecutor taskExecutor) throws In threadReference.set(currentThread); latch.countDown(); }); - latch.await(30, TimeUnit.SECONDS); + if (!latch.await(30, TimeUnit.SECONDS)) { + fail("Timeout while waiting for latch"); + } Thread thread = threadReference.get(); assertThat(thread).extracting("virtual").as("%s is virtual", thread).isEqualTo(true); return thread.getName(); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilder.java new file mode 100644 index 000000000000..12d71da1af18 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilder.java @@ -0,0 +1,222 @@ +/* + * Copyright 2012-2023 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.task; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.beans.BeanUtils; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.TaskDecorator; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Builder that can be used to configure and create a {@link SimpleAsyncTaskExecutor}. + * Provides convenience methods to set common {@link SimpleAsyncTaskExecutor} settings and + * register {@link #taskDecorator(TaskDecorator)}). For advanced configuration, consider + * using {@link SimpleAsyncTaskExecutorCustomizer}. + *

+ * In a typical auto-configured Spring Boot application this builder is available as a + * bean and can be injected whenever a {@link SimpleAsyncTaskExecutor} is needed. + * + * @author Stephane Nicoll + * @author Filip Hrisafov + * @author Moritz Halbritter + * @since 3.2.0 + */ +public class SimpleAsyncTaskExecutorBuilder { + + private final Boolean virtualThreads; + + private final String threadNamePrefix; + + private final Integer concurrencyLimit; + + private final TaskDecorator taskDecorator; + + private final Set customizers; + + public SimpleAsyncTaskExecutorBuilder() { + this.virtualThreads = null; + this.threadNamePrefix = null; + this.concurrencyLimit = null; + this.taskDecorator = null; + this.customizers = null; + } + + private SimpleAsyncTaskExecutorBuilder(Boolean virtualThreads, String threadNamePrefix, Integer concurrencyLimit, + TaskDecorator taskDecorator, Set customizers) { + this.virtualThreads = virtualThreads; + this.threadNamePrefix = threadNamePrefix; + this.concurrencyLimit = concurrencyLimit; + this.taskDecorator = taskDecorator; + this.customizers = customizers; + } + + /** + * Set the prefix to use for the names of newly created threads. + * @param threadNamePrefix the thread name prefix to set + * @return a new builder instance + */ + public SimpleAsyncTaskExecutorBuilder threadNamePrefix(String threadNamePrefix) { + return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, threadNamePrefix, this.concurrencyLimit, + this.taskDecorator, this.customizers); + } + + /** + * Set whether to use virtual threads. + * @param virtualThreads whether to use virtual threads + * @return a new builder instance + */ + public SimpleAsyncTaskExecutorBuilder virtualThreads(Boolean virtualThreads) { + return new SimpleAsyncTaskExecutorBuilder(virtualThreads, this.threadNamePrefix, this.concurrencyLimit, + this.taskDecorator, this.customizers); + } + + /** + * Set the concurrency limit. + * @param concurrencyLimit the concurrency limit + * @return a new builder instance + */ + public SimpleAsyncTaskExecutorBuilder concurrencyLimit(Integer concurrencyLimit) { + return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, concurrencyLimit, + this.taskDecorator, this.customizers); + } + + /** + * Set the {@link TaskDecorator} to use or {@code null} to not use any. + * @param taskDecorator the task decorator to use + * @return a new builder instance + */ + public SimpleAsyncTaskExecutorBuilder taskDecorator(TaskDecorator taskDecorator) { + return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, this.concurrencyLimit, + taskDecorator, this.customizers); + } + + /** + * Set the {@link SimpleAsyncTaskExecutorCustomizer TaskExecutorCustomizers} that + * should be applied to the {@link SimpleAsyncTaskExecutor}. Customizers are applied + * in the order that they were added after builder configuration has been applied. + * Setting this value will replace any previously configured customizers. + * @param customizers the customizers to set + * @return a new builder instance + * @see #additionalCustomizers(SimpleAsyncTaskExecutorCustomizer...) + */ + public SimpleAsyncTaskExecutorBuilder customizers(SimpleAsyncTaskExecutorCustomizer... customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return customizers(Arrays.asList(customizers)); + } + + /** + * Set the {@link SimpleAsyncTaskExecutorCustomizer TaskExecutorCustomizers} that + * should be applied to the {@link SimpleAsyncTaskExecutor}. Customizers are applied + * in the order that they were added after builder configuration has been applied. + * Setting this value will replace any previously configured customizers. + * @param customizers the customizers to set + * @return a new builder instance + * @see #additionalCustomizers(SimpleAsyncTaskExecutorCustomizer...) + */ + public SimpleAsyncTaskExecutorBuilder customizers( + Iterable customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, this.concurrencyLimit, + this.taskDecorator, append(null, customizers)); + } + + /** + * Add {@link SimpleAsyncTaskExecutorCustomizer TaskExecutorCustomizers} that should + * be applied to the {@link SimpleAsyncTaskExecutor}. Customizers are applied in the + * order that they were added after builder configuration has been applied. + * @param customizers the customizers to add + * @return a new builder instance + * @see #customizers(SimpleAsyncTaskExecutorCustomizer...) + */ + public SimpleAsyncTaskExecutorBuilder additionalCustomizers(SimpleAsyncTaskExecutorCustomizer... customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return additionalCustomizers(Arrays.asList(customizers)); + } + + /** + * Add {@link SimpleAsyncTaskExecutorCustomizer TaskExecutorCustomizers} that should + * be applied to the {@link SimpleAsyncTaskExecutor}. Customizers are applied in the + * order that they were added after builder configuration has been applied. + * @param customizers the customizers to add + * @return a new builder instance + * @see #customizers(SimpleAsyncTaskExecutorCustomizer...) + */ + public SimpleAsyncTaskExecutorBuilder additionalCustomizers( + Iterable customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, this.concurrencyLimit, + this.taskDecorator, append(this.customizers, customizers)); + } + + /** + * Build a new {@link SimpleAsyncTaskExecutor} instance and configure it using this + * builder. + * @return a configured {@link SimpleAsyncTaskExecutor} instance. + * @see #build(Class) + * @see #configure(SimpleAsyncTaskExecutor) + */ + public SimpleAsyncTaskExecutor build() { + return configure(new SimpleAsyncTaskExecutor()); + } + + /** + * Build a new {@link SimpleAsyncTaskExecutor} instance of the specified type and + * configure it using this builder. + * @param the type of task executor + * @param taskExecutorClass the template type to create + * @return a configured {@link SimpleAsyncTaskExecutor} instance. + * @see #build() + * @see #configure(SimpleAsyncTaskExecutor) + */ + public T build(Class taskExecutorClass) { + return configure(BeanUtils.instantiateClass(taskExecutorClass)); + } + + /** + * Configure the provided {@link SimpleAsyncTaskExecutor} instance using this builder. + * @param the type of task executor + * @param taskExecutor the {@link SimpleAsyncTaskExecutor} to configure + * @return the task executor instance + * @see #build() + * @see #build(Class) + */ + public T configure(T taskExecutor) { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(this.virtualThreads).to(taskExecutor::setVirtualThreads); + map.from(this.threadNamePrefix).whenHasText().to(taskExecutor::setThreadNamePrefix); + map.from(this.concurrencyLimit).to(taskExecutor::setConcurrencyLimit); + map.from(this.taskDecorator).to(taskExecutor::setTaskDecorator); + if (!CollectionUtils.isEmpty(this.customizers)) { + this.customizers.forEach((customizer) -> customizer.customize(taskExecutor)); + } + return taskExecutor; + } + + private Set append(Set set, Iterable additions) { + Set result = new LinkedHashSet<>((set != null) ? set : Collections.emptySet()); + additions.forEach(result::add); + return Collections.unmodifiableSet(result); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorCustomizer.java new file mode 100644 index 000000000000..0f4218ecb4bd --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorCustomizer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2023 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.task; + +import org.springframework.core.task.SimpleAsyncTaskExecutor; + +/** + * Callback interface that can be used to customize a {@link SimpleAsyncTaskExecutor}. + * + * @author Stephane Nicoll + * @author Moritz Halbritter + * @since 3.2.0 + * @see SimpleAsyncTaskExecutorBuilder + */ +@FunctionalInterface +public interface SimpleAsyncTaskExecutorCustomizer { + + /** + * Callback to customize a {@link SimpleAsyncTaskExecutor} instance. + * @param taskExecutor the task executor to customize + */ + void customize(SimpleAsyncTaskExecutor taskExecutor); + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilderTests.java new file mode 100644 index 000000000000..fe856cd9cbae --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilderTests.java @@ -0,0 +1,152 @@ +/* + * Copyright 2012-2023 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.task; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; + +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.TaskDecorator; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +/** + * Tests for {@link SimpleAsyncTaskExecutorBuilder}. + * + * @author Stephane Nicoll + * @author Filip Hrisafov + * @author Moritz Halbritter + */ +class SimpleAsyncTaskExecutorBuilderTests { + + private final SimpleAsyncTaskExecutorBuilder builder = new SimpleAsyncTaskExecutorBuilder(); + + @Test + void threadNamePrefixShouldApply() { + SimpleAsyncTaskExecutor executor = this.builder.threadNamePrefix("test-").build(); + assertThat(executor.getThreadNamePrefix()).isEqualTo("test-"); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void virtualThreadsShouldApply() { + SimpleAsyncTaskExecutor executor = this.builder.virtualThreads(true).build(); + Field field = ReflectionUtils.findField(SimpleAsyncTaskExecutor.class, "virtualThreadDelegate"); + assertThat(field).as("executor.virtualThreadDelegate").isNotNull(); + field.setAccessible(true); + Object virtualThreadDelegate = ReflectionUtils.getField(field, executor); + assertThat(virtualThreadDelegate).as("executor.virtualThreadDelegate").isNotNull(); + } + + @Test + void concurrencyLimitShouldApply() { + SimpleAsyncTaskExecutor executor = this.builder.concurrencyLimit(1).build(); + assertThat(executor.getConcurrencyLimit()).isEqualTo(1); + } + + @Test + void taskDecoratorShouldApply() { + TaskDecorator taskDecorator = mock(TaskDecorator.class); + SimpleAsyncTaskExecutor executor = this.builder.taskDecorator(taskDecorator).build(); + assertThat(executor).extracting("taskDecorator").isSameAs(taskDecorator); + } + + @Test + void customizersWhenCustomizersAreNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.builder.customizers((SimpleAsyncTaskExecutorCustomizer[]) null)) + .withMessageContaining("Customizers must not be null"); + } + + @Test + void customizersCollectionWhenCustomizersAreNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.builder.customizers((Set) null)) + .withMessageContaining("Customizers must not be null"); + } + + @Test + void customizersShouldApply() { + SimpleAsyncTaskExecutorCustomizer customizer = mock(SimpleAsyncTaskExecutorCustomizer.class); + SimpleAsyncTaskExecutor executor = this.builder.customizers(customizer).build(); + then(customizer).should().customize(executor); + } + + @Test + void customizersShouldBeAppliedLast() { + TaskDecorator taskDecorator = mock(TaskDecorator.class); + SimpleAsyncTaskExecutor executor = spy(new SimpleAsyncTaskExecutor()); + this.builder.threadNamePrefix("test-") + .virtualThreads(true) + .concurrencyLimit(1) + .taskDecorator(taskDecorator) + .additionalCustomizers((taskExecutor) -> { + then(taskExecutor).should().setConcurrencyLimit(1); + then(taskExecutor).should().setVirtualThreads(true); + then(taskExecutor).should().setThreadNamePrefix("test-"); + then(taskExecutor).should().setTaskDecorator(taskDecorator); + }); + this.builder.configure(executor); + } + + @Test + void customizersShouldReplaceExisting() { + SimpleAsyncTaskExecutorCustomizer customizer1 = mock(SimpleAsyncTaskExecutorCustomizer.class); + SimpleAsyncTaskExecutorCustomizer customizer2 = mock(SimpleAsyncTaskExecutorCustomizer.class); + SimpleAsyncTaskExecutor executor = this.builder.customizers(customizer1) + .customizers(Collections.singleton(customizer2)) + .build(); + then(customizer1).shouldHaveNoInteractions(); + then(customizer2).should().customize(executor); + } + + @Test + void additionalCustomizersWhenCustomizersAreNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.builder.additionalCustomizers((SimpleAsyncTaskExecutorCustomizer[]) null)) + .withMessageContaining("Customizers must not be null"); + } + + @Test + void additionalCustomizersCollectionWhenCustomizersAreNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.builder.additionalCustomizers((Set) null)) + .withMessageContaining("Customizers must not be null"); + } + + @Test + void additionalCustomizersShouldAddToExisting() { + SimpleAsyncTaskExecutorCustomizer customizer1 = mock(SimpleAsyncTaskExecutorCustomizer.class); + SimpleAsyncTaskExecutorCustomizer customizer2 = mock(SimpleAsyncTaskExecutorCustomizer.class); + SimpleAsyncTaskExecutor executor = this.builder.customizers(customizer1) + .additionalCustomizers(customizer2) + .build(); + then(customizer1).should().customize(executor); + then(customizer2).should().customize(executor); + } + +} From de4b2d679f1821a3f0059264abb02da5c4edac6c Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Mon, 31 Jul 2023 14:16:35 +0200 Subject: [PATCH 0212/1215] Add documentation for SimpleAsyncTaskExecutorBuilder See gh-35711 --- .../docs/asciidoc/features/task-execution-and-scheduling.adoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc index 6126919354fc..dadb185ffc0f 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc @@ -49,4 +49,5 @@ The thread pool uses one thread by default and its settings can be fine-tuned us size: 2 ---- -Both a `ThreadPoolTaskExecutorBuilder` bean and a `TaskSchedulerBuilder` bean are made available in the context if a custom executor or scheduler needs to be created. +A `ThreadPoolTaskExecutorBuilder` bean, a `SimpleAsyncTaskExecutorBuilder` bean and a `TaskSchedulerBuilder` bean are made available in the context if a custom executor or scheduler needs to be created. +The `SimpleAsyncTaskExecutorBuilder` is auto-configured to use virtual threads if they are enabled (using Java 21+ and configprop:spring.threads.virtual.enabled[] set to `true`). From 51008a7d39f36620e87af6f325c6d897e5adbd86 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 1 Aug 2023 10:32:58 +0200 Subject: [PATCH 0213/1215] Add ThreadPoolTaskSchedulerBuilder and deprecate TaskSchedulerBuilder Closes gh-36651 --- .../IntegrationAutoConfiguration.java | 13 +- .../task/TaskSchedulingAutoConfiguration.java | 33 +-- .../task/TaskSchedulingConfigurations.java | 108 +++++++++ .../TaskSchedulingAutoConfigurationTests.java | 49 +++- .../task-execution-and-scheduling.adoc | 2 +- .../boot/task/TaskSchedulerBuilder.java | 6 +- .../boot/task/TaskSchedulerCustomizer.java | 5 +- .../task/ThreadPoolTaskSchedulerBuilder.java | 214 ++++++++++++++++++ .../ThreadPoolTaskSchedulerCustomizer.java | 36 +++ .../boot/task/TaskSchedulerBuilderTests.java | 1 + .../ThreadPoolTaskSchedulerBuilderTests.java | 134 +++++++++++ 11 files changed, 567 insertions(+), 34 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskSchedulerBuilder.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskSchedulerCustomizer.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/ThreadPoolTaskSchedulerBuilderTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfiguration.java index 9ca2706b611e..71f20ef9e7a2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/integration/IntegrationAutoConfiguration.java @@ -24,6 +24,7 @@ import io.rsocket.transport.netty.server.TcpServerTransport; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; @@ -43,6 +44,7 @@ import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException; import org.springframework.boot.task.TaskSchedulerBuilder; +import org.springframework.boot.task.ThreadPoolTaskSchedulerBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -168,11 +170,18 @@ private Trigger createPeriodicTrigger(Duration period, Duration initialDelay, bo @Configuration(proxyBeanMethods = false) @ConditionalOnBean(TaskSchedulerBuilder.class) @ConditionalOnMissingBean(name = IntegrationContextUtils.TASK_SCHEDULER_BEAN_NAME) + @SuppressWarnings("removal") protected static class IntegrationTaskSchedulerConfiguration { @Bean(name = IntegrationContextUtils.TASK_SCHEDULER_BEAN_NAME) - public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder builder) { - return builder.build(); + public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder taskSchedulerBuilder, + ObjectProvider threadPoolTaskSchedulerBuilderProvider) { + ThreadPoolTaskSchedulerBuilder threadPoolTaskSchedulerBuilder = threadPoolTaskSchedulerBuilderProvider + .getIfUnique(); + if (threadPoolTaskSchedulerBuilder != null) { + return threadPoolTaskSchedulerBuilder.build(); + } + return taskSchedulerBuilder.build(); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java index d7969608de36..5d14f7bcdc8a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java @@ -16,20 +16,14 @@ package org.springframework.boot.autoconfigure.task; -import java.util.concurrent.ScheduledExecutorService; - -import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.LazyInitializationExcludeFilter; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.task.TaskSchedulingProperties.Shutdown; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.task.TaskSchedulerBuilder; -import org.springframework.boot.task.TaskSchedulerCustomizer; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.scheduling.config.TaskManagementConfigUtils; @@ -38,38 +32,21 @@ * {@link EnableAutoConfiguration Auto-configuration} for {@link TaskScheduler}. * * @author Stephane Nicoll + * @author Moritz Halbritter * @since 2.1.0 */ @ConditionalOnClass(ThreadPoolTaskScheduler.class) @AutoConfiguration(after = TaskExecutionAutoConfiguration.class) @EnableConfigurationProperties(TaskSchedulingProperties.class) +@Import({ TaskSchedulingConfigurations.ThreadPoolTaskSchedulerBuilderConfiguration.class, + TaskSchedulingConfigurations.TaskSchedulerBuilderConfiguration.class, + TaskSchedulingConfigurations.ThreadPoolTaskSchedulerConfiguration.class }) public class TaskSchedulingAutoConfiguration { - @Bean - @ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME) - @ConditionalOnMissingBean({ TaskScheduler.class, ScheduledExecutorService.class }) - public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder builder) { - return builder.build(); - } - @Bean @ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME) public static LazyInitializationExcludeFilter scheduledBeanLazyInitializationExcludeFilter() { return new ScheduledBeanLazyInitializationExcludeFilter(); } - @Bean - @ConditionalOnMissingBean - public TaskSchedulerBuilder taskSchedulerBuilder(TaskSchedulingProperties properties, - ObjectProvider taskSchedulerCustomizers) { - TaskSchedulerBuilder builder = new TaskSchedulerBuilder(); - builder = builder.poolSize(properties.getPool().getSize()); - Shutdown shutdown = properties.getShutdown(); - builder = builder.awaitTermination(shutdown.isAwaitTermination()); - builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod()); - builder = builder.threadNamePrefix(properties.getThreadNamePrefix()); - builder = builder.customizers(taskSchedulerCustomizers); - return builder; - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java new file mode 100644 index 000000000000..5375620e3444 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java @@ -0,0 +1,108 @@ +/* + * Copyright 2012-2023 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.task; + +import java.util.concurrent.ScheduledExecutorService; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.task.TaskSchedulerBuilder; +import org.springframework.boot.task.TaskSchedulerCustomizer; +import org.springframework.boot.task.ThreadPoolTaskSchedulerBuilder; +import org.springframework.boot.task.ThreadPoolTaskSchedulerCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.scheduling.config.TaskManagementConfigUtils; + +/** + * {@link TaskScheduler} configurations to be imported by + * {@link TaskSchedulingAutoConfiguration} in a specific order. + * + * @author Moritz Halbritter + */ +class TaskSchedulingConfigurations { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME) + @ConditionalOnMissingBean({ TaskScheduler.class, ScheduledExecutorService.class }) + @SuppressWarnings("removal") + static class ThreadPoolTaskSchedulerConfiguration { + + @Bean + ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder taskSchedulerBuilder, + ObjectProvider threadPoolTaskSchedulerBuilderProvider) { + ThreadPoolTaskSchedulerBuilder threadPoolTaskSchedulerBuilder = threadPoolTaskSchedulerBuilderProvider + .getIfUnique(); + if (threadPoolTaskSchedulerBuilder != null) { + return threadPoolTaskSchedulerBuilder.build(); + } + return taskSchedulerBuilder.build(); + } + + } + + @Configuration(proxyBeanMethods = false) + @SuppressWarnings("removal") + static class TaskSchedulerBuilderConfiguration { + + @Bean + @ConditionalOnMissingBean + TaskSchedulerBuilder taskSchedulerBuilder(TaskSchedulingProperties properties, + ObjectProvider taskSchedulerCustomizers) { + TaskSchedulerBuilder builder = new TaskSchedulerBuilder(); + builder = builder.poolSize(properties.getPool().getSize()); + TaskSchedulingProperties.Shutdown shutdown = properties.getShutdown(); + builder = builder.awaitTermination(shutdown.isAwaitTermination()); + builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod()); + builder = builder.threadNamePrefix(properties.getThreadNamePrefix()); + builder = builder.customizers(taskSchedulerCustomizers); + return builder; + } + + } + + @Configuration(proxyBeanMethods = false) + @SuppressWarnings("removal") + static class ThreadPoolTaskSchedulerBuilderConfiguration { + + @Bean + @ConditionalOnMissingBean({ TaskSchedulerBuilder.class, ThreadPoolTaskSchedulerBuilder.class }) + ThreadPoolTaskSchedulerBuilder threadPoolTaskSchedulerBuilder(TaskSchedulingProperties properties, + ObjectProvider threadPoolTaskSchedulerCustomizers, + ObjectProvider taskSchedulerCustomizers) { + TaskSchedulingProperties.Shutdown shutdown = properties.getShutdown(); + ThreadPoolTaskSchedulerBuilder builder = new ThreadPoolTaskSchedulerBuilder(); + builder = builder.poolSize(properties.getPool().getSize()); + builder = builder.awaitTermination(shutdown.isAwaitTermination()); + builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod()); + builder = builder.threadNamePrefix(properties.getThreadNamePrefix()); + builder = builder.customizers(threadPoolTaskSchedulerCustomizers); + // Apply the deprecated TaskSchedulerCustomizers, too + builder = builder.additionalCustomizers(taskSchedulerCustomizers.orderedStream().map(this::adapt).toList()); + return builder; + } + + private ThreadPoolTaskSchedulerCustomizer adapt(TaskSchedulerCustomizer customizer) { + return customizer::customize; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java index 8d42ccd05171..0898ec2beeb9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java @@ -31,7 +31,10 @@ import org.springframework.boot.LazyInitializationBeanFactoryPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.task.TaskSchedulerBuilder; import org.springframework.boot.task.TaskSchedulerCustomizer; +import org.springframework.boot.task.ThreadPoolTaskSchedulerBuilder; +import org.springframework.boot.task.ThreadPoolTaskSchedulerCustomizer; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -49,7 +52,9 @@ * Tests for {@link TaskSchedulingAutoConfiguration}. * * @author Stephane Nicoll + * @author Moritz Halbritter */ +@SuppressWarnings("removal") class TaskSchedulingAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() @@ -67,6 +72,26 @@ void noSchedulingDoesNotExposeScheduledBeanLazyInitializationExcludeFilter() { .run((context) -> assertThat(context).doesNotHaveBean(ScheduledBeanLazyInitializationExcludeFilter.class)); } + @Test + void shouldSupplyBeans() { + this.contextRunner.withUserConfiguration(SchedulingConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(TaskSchedulerBuilder.class); + assertThat(context).hasSingleBean(ThreadPoolTaskSchedulerBuilder.class); + assertThat(context).hasSingleBean(ThreadPoolTaskScheduler.class); + }); + } + + @Test + void shouldNotSupplyThreadPoolTaskSchedulerBuilderIfCustomTaskSchedulerBuilderIsPresent() { + this.contextRunner.withUserConfiguration(SchedulingConfiguration.class) + .withBean(TaskSchedulerBuilder.class, TaskSchedulerBuilder::new) + .run((context) -> { + assertThat(context).hasSingleBean(TaskSchedulerBuilder.class); + assertThat(context).doesNotHaveBean(ThreadPoolTaskSchedulerBuilder.class); + assertThat(context).hasSingleBean(ThreadPoolTaskScheduler.class); + }); + } + @Test void enableSchedulingWithNoTaskExecutorAutoConfiguresOne() { this.contextRunner @@ -86,7 +111,7 @@ void enableSchedulingWithNoTaskExecutorAutoConfiguresOne() { } @Test - void enableSchedulingWithNoTaskExecutorAppliesCustomizers() { + void enableSchedulingWithNoTaskExecutorAppliesTaskSchedulerCustomizers() { this.contextRunner.withPropertyValues("spring.task.scheduling.thread-name-prefix=scheduling-test-") .withUserConfiguration(SchedulingConfiguration.class, TaskSchedulerCustomizerConfiguration.class) .run((context) -> { @@ -97,6 +122,18 @@ void enableSchedulingWithNoTaskExecutorAppliesCustomizers() { }); } + @Test + void enableSchedulingWithNoTaskExecutorAppliesCustomizers() { + this.contextRunner.withPropertyValues("spring.task.scheduling.thread-name-prefix=scheduling-test-") + .withUserConfiguration(SchedulingConfiguration.class, ThreadPoolTaskSchedulerCustomizerConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(TaskExecutor.class); + TestBean bean = context.getBean(TestBean.class); + assertThat(bean.latch.await(30, TimeUnit.SECONDS)).isTrue(); + assertThat(bean.threadNames).allMatch((name) -> name.contains("customized-scheduler-")); + }); + } + @Test void enableSchedulingWithExistingTaskSchedulerBacksOff() { this.contextRunner.withUserConfiguration(SchedulingConfiguration.class, TaskSchedulerConfiguration.class) @@ -175,6 +212,16 @@ TaskSchedulerCustomizer testTaskSchedulerCustomizer() { } + @Configuration(proxyBeanMethods = false) + static class ThreadPoolTaskSchedulerCustomizerConfiguration { + + @Bean + ThreadPoolTaskSchedulerCustomizer testTaskSchedulerCustomizer() { + return ((taskScheduler) -> taskScheduler.setThreadNamePrefix("customized-scheduler-")); + } + + } + @Configuration(proxyBeanMethods = false) static class SchedulingConfigurerConfiguration implements SchedulingConfigurer { diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc index dadb185ffc0f..5b3ed7f031df 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc @@ -49,5 +49,5 @@ The thread pool uses one thread by default and its settings can be fine-tuned us size: 2 ---- -A `ThreadPoolTaskExecutorBuilder` bean, a `SimpleAsyncTaskExecutorBuilder` bean and a `TaskSchedulerBuilder` bean are made available in the context if a custom executor or scheduler needs to be created. +A `ThreadPoolTaskExecutorBuilder` bean, a `SimpleAsyncTaskExecutorBuilder` bean and a `ThreadPoolTaskSchedulerBuilder` bean are made available in the context if a custom executor or scheduler needs to be created. The `SimpleAsyncTaskExecutorBuilder` is auto-configured to use virtual threads if they are enabled (using Java 21+ and configprop:spring.threads.virtual.enabled[] set to `true`). diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskSchedulerBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskSchedulerBuilder.java index 9ec2c3e6aeef..65c385d0c5a3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskSchedulerBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskSchedulerBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. @@ -38,7 +38,11 @@ * * @author Stephane Nicoll * @since 2.1.0 + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of + * {@link ThreadPoolTaskSchedulerBuilder} */ +@Deprecated(since = "3.2.0", forRemoval = true) +@SuppressWarnings("removal") public class TaskSchedulerBuilder { private final Integer poolSize; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskSchedulerCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskSchedulerCustomizer.java index 7c5252c68669..8acf391a42a8 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskSchedulerCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/TaskSchedulerCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. @@ -23,8 +23,11 @@ * * @author Stephane Nicoll * @since 2.1.0 + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of + * {@link ThreadPoolTaskSchedulerCustomizer} */ @FunctionalInterface +@Deprecated(since = "3.2.0", forRemoval = true) public interface TaskSchedulerCustomizer { /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskSchedulerBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskSchedulerBuilder.java new file mode 100644 index 000000000000..a36e48308ee4 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskSchedulerBuilder.java @@ -0,0 +1,214 @@ +/* + * Copyright 2012-2023 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.task; + +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Builder that can be used to configure and create a {@link ThreadPoolTaskScheduler}. + * Provides convenience methods to set common {@link ThreadPoolTaskScheduler} settings. + * For advanced configuration, consider using {@link ThreadPoolTaskSchedulerCustomizer}. + *

+ * In a typical auto-configured Spring Boot application this builder is available as a + * bean and can be injected whenever a {@link ThreadPoolTaskScheduler} is needed. + * + * @author Stephane Nicoll + * @since 3.2.0 + */ +public class ThreadPoolTaskSchedulerBuilder { + + private final Integer poolSize; + + private final Boolean awaitTermination; + + private final Duration awaitTerminationPeriod; + + private final String threadNamePrefix; + + private final Set customizers; + + public ThreadPoolTaskSchedulerBuilder() { + this.poolSize = null; + this.awaitTermination = null; + this.awaitTerminationPeriod = null; + this.threadNamePrefix = null; + this.customizers = null; + } + + public ThreadPoolTaskSchedulerBuilder(Integer poolSize, Boolean awaitTermination, Duration awaitTerminationPeriod, + String threadNamePrefix, Set taskSchedulerCustomizers) { + this.poolSize = poolSize; + this.awaitTermination = awaitTermination; + this.awaitTerminationPeriod = awaitTerminationPeriod; + this.threadNamePrefix = threadNamePrefix; + this.customizers = taskSchedulerCustomizers; + } + + /** + * Set the maximum allowed number of threads. + * @param poolSize the pool size to set + * @return a new builder instance + */ + public ThreadPoolTaskSchedulerBuilder poolSize(int poolSize) { + return new ThreadPoolTaskSchedulerBuilder(poolSize, this.awaitTermination, this.awaitTerminationPeriod, + this.threadNamePrefix, this.customizers); + } + + /** + * Set whether the executor should wait for scheduled tasks to complete on shutdown, + * not interrupting running tasks and executing all tasks in the queue. + * @param awaitTermination whether the executor needs to wait for the tasks to + * complete on shutdown + * @return a new builder instance + * @see #awaitTerminationPeriod(Duration) + */ + public ThreadPoolTaskSchedulerBuilder awaitTermination(boolean awaitTermination) { + return new ThreadPoolTaskSchedulerBuilder(this.poolSize, awaitTermination, this.awaitTerminationPeriod, + this.threadNamePrefix, this.customizers); + } + + /** + * Set the maximum time the executor is supposed to block on shutdown. When set, the + * executor blocks on shutdown in order to wait for remaining tasks to complete their + * execution before the rest of the container continues to shut down. This is + * particularly useful if your remaining tasks are likely to need access to other + * resources that are also managed by the container. + * @param awaitTerminationPeriod the await termination period to set + * @return a new builder instance + */ + public ThreadPoolTaskSchedulerBuilder awaitTerminationPeriod(Duration awaitTerminationPeriod) { + return new ThreadPoolTaskSchedulerBuilder(this.poolSize, this.awaitTermination, awaitTerminationPeriod, + this.threadNamePrefix, this.customizers); + } + + /** + * Set the prefix to use for the names of newly created threads. + * @param threadNamePrefix the thread name prefix to set + * @return a new builder instance + */ + public ThreadPoolTaskSchedulerBuilder threadNamePrefix(String threadNamePrefix) { + return new ThreadPoolTaskSchedulerBuilder(this.poolSize, this.awaitTermination, this.awaitTerminationPeriod, + threadNamePrefix, this.customizers); + } + + /** + * Set the {@link ThreadPoolTaskSchedulerCustomizer + * threadPoolTaskSchedulerCustomizers} that should be applied to the + * {@link ThreadPoolTaskScheduler}. Customizers are applied in the order that they + * were added after builder configuration has been applied. Setting this value will + * replace any previously configured customizers. + * @param customizers the customizers to set + * @return a new builder instance + * @see #additionalCustomizers(ThreadPoolTaskSchedulerCustomizer...) + */ + public ThreadPoolTaskSchedulerBuilder customizers(ThreadPoolTaskSchedulerCustomizer... customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return customizers(Arrays.asList(customizers)); + } + + /** + * Set the {@link ThreadPoolTaskSchedulerCustomizer + * threadPoolTaskSchedulerCustomizers} that should be applied to the + * {@link ThreadPoolTaskScheduler}. Customizers are applied in the order that they + * were added after builder configuration has been applied. Setting this value will + * replace any previously configured customizers. + * @param customizers the customizers to set + * @return a new builder instance + * @see #additionalCustomizers(ThreadPoolTaskSchedulerCustomizer...) + */ + public ThreadPoolTaskSchedulerBuilder customizers( + Iterable customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return new ThreadPoolTaskSchedulerBuilder(this.poolSize, this.awaitTermination, this.awaitTerminationPeriod, + this.threadNamePrefix, append(null, customizers)); + } + + /** + * Add {@link ThreadPoolTaskSchedulerCustomizer threadPoolTaskSchedulerCustomizers} + * that should be applied to the {@link ThreadPoolTaskScheduler}. Customizers are + * applied in the order that they were added after builder configuration has been + * applied. + * @param customizers the customizers to add + * @return a new builder instance + * @see #customizers(ThreadPoolTaskSchedulerCustomizer...) + */ + public ThreadPoolTaskSchedulerBuilder additionalCustomizers(ThreadPoolTaskSchedulerCustomizer... customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return additionalCustomizers(Arrays.asList(customizers)); + } + + /** + * Add {@link ThreadPoolTaskSchedulerCustomizer threadPoolTaskSchedulerCustomizers} + * that should be applied to the {@link ThreadPoolTaskScheduler}. Customizers are + * applied in the order that they were added after builder configuration has been + * applied. + * @param customizers the customizers to add + * @return a new builder instance + * @see #customizers(ThreadPoolTaskSchedulerCustomizer...) + */ + public ThreadPoolTaskSchedulerBuilder additionalCustomizers( + Iterable customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return new ThreadPoolTaskSchedulerBuilder(this.poolSize, this.awaitTermination, this.awaitTerminationPeriod, + this.threadNamePrefix, append(this.customizers, customizers)); + } + + /** + * Build a new {@link ThreadPoolTaskScheduler} instance and configure it using this + * builder. + * @return a configured {@link ThreadPoolTaskScheduler} instance. + * @see #configure(ThreadPoolTaskScheduler) + */ + public ThreadPoolTaskScheduler build() { + return configure(new ThreadPoolTaskScheduler()); + } + + /** + * Configure the provided {@link ThreadPoolTaskScheduler} instance using this builder. + * @param the type of task scheduler + * @param taskScheduler the {@link ThreadPoolTaskScheduler} to configure + * @return the task scheduler instance + * @see #build() + */ + public T configure(T taskScheduler) { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(this.poolSize).to(taskScheduler::setPoolSize); + map.from(this.awaitTermination).to(taskScheduler::setWaitForTasksToCompleteOnShutdown); + map.from(this.awaitTerminationPeriod).asInt(Duration::getSeconds).to(taskScheduler::setAwaitTerminationSeconds); + map.from(this.threadNamePrefix).to(taskScheduler::setThreadNamePrefix); + if (!CollectionUtils.isEmpty(this.customizers)) { + this.customizers.forEach((customizer) -> customizer.customize(taskScheduler)); + } + return taskScheduler; + } + + private Set append(Set set, Iterable additions) { + Set result = new LinkedHashSet<>((set != null) ? set : Collections.emptySet()); + additions.forEach(result::add); + return Collections.unmodifiableSet(result); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskSchedulerCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskSchedulerCustomizer.java new file mode 100644 index 000000000000..0e7cc44458e2 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskSchedulerCustomizer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2023 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.task; + +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +/** + * Callback interface that can be used to customize a {@link ThreadPoolTaskScheduler}. + * + * @author Stephane Nicoll + * @since 3.2.0 + */ +@FunctionalInterface +public interface ThreadPoolTaskSchedulerCustomizer { + + /** + * Callback to customize a {@link ThreadPoolTaskScheduler} instance. + * @param taskScheduler the task scheduler to customize + */ + void customize(ThreadPoolTaskScheduler taskScheduler); + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/TaskSchedulerBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/TaskSchedulerBuilderTests.java index e07573910c16..095e8fda4edb 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/TaskSchedulerBuilderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/TaskSchedulerBuilderTests.java @@ -35,6 +35,7 @@ * * @author Stephane Nicoll */ +@SuppressWarnings("removal") class TaskSchedulerBuilderTests { private final TaskSchedulerBuilder builder = new TaskSchedulerBuilder(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/ThreadPoolTaskSchedulerBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/ThreadPoolTaskSchedulerBuilderTests.java new file mode 100644 index 000000000000..11b4f15f49af --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/ThreadPoolTaskSchedulerBuilderTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2012-2023 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.task; + +import java.time.Duration; +import java.util.Collections; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +/** + * Tests for {@link ThreadPoolTaskSchedulerBuilder}. + * + * @author Stephane Nicoll + */ +class ThreadPoolTaskSchedulerBuilderTests { + + private final ThreadPoolTaskSchedulerBuilder builder = new ThreadPoolTaskSchedulerBuilder(); + + @Test + void poolSettingsShouldApply() { + ThreadPoolTaskScheduler scheduler = this.builder.poolSize(4).build(); + assertThat(scheduler.getPoolSize()).isEqualTo(4); + } + + @Test + void awaitTerminationShouldApply() { + ThreadPoolTaskScheduler executor = this.builder.awaitTermination(true).build(); + assertThat(executor).hasFieldOrPropertyWithValue("waitForTasksToCompleteOnShutdown", true); + } + + @Test + void awaitTerminationPeriodShouldApply() { + Duration period = Duration.ofMinutes(1); + ThreadPoolTaskScheduler executor = this.builder.awaitTerminationPeriod(period).build(); + assertThat(executor).hasFieldOrPropertyWithValue("awaitTerminationMillis", period.toMillis()); + } + + @Test + void threadNamePrefixShouldApply() { + ThreadPoolTaskScheduler scheduler = this.builder.threadNamePrefix("test-").build(); + assertThat(scheduler.getThreadNamePrefix()).isEqualTo("test-"); + } + + @Test + void customizersWhenCustomizersAreNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.builder.customizers((ThreadPoolTaskSchedulerCustomizer[]) null)) + .withMessageContaining("Customizers must not be null"); + } + + @Test + void customizersCollectionWhenCustomizersAreNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.builder.customizers((Set) null)) + .withMessageContaining("Customizers must not be null"); + } + + @Test + void customizersShouldApply() { + ThreadPoolTaskSchedulerCustomizer customizer = mock(ThreadPoolTaskSchedulerCustomizer.class); + ThreadPoolTaskScheduler scheduler = this.builder.customizers(customizer).build(); + then(customizer).should().customize(scheduler); + } + + @Test + void customizersShouldBeAppliedLast() { + ThreadPoolTaskScheduler scheduler = spy(new ThreadPoolTaskScheduler()); + this.builder.poolSize(4).threadNamePrefix("test-").additionalCustomizers((taskScheduler) -> { + then(taskScheduler).should().setPoolSize(4); + then(taskScheduler).should().setThreadNamePrefix("test-"); + }); + this.builder.configure(scheduler); + } + + @Test + void customizersShouldReplaceExisting() { + ThreadPoolTaskSchedulerCustomizer customizer1 = mock(ThreadPoolTaskSchedulerCustomizer.class); + ThreadPoolTaskSchedulerCustomizer customizer2 = mock(ThreadPoolTaskSchedulerCustomizer.class); + ThreadPoolTaskScheduler scheduler = this.builder.customizers(customizer1) + .customizers(Collections.singleton(customizer2)) + .build(); + then(customizer1).shouldHaveNoInteractions(); + then(customizer2).should().customize(scheduler); + } + + @Test + void additionalCustomizersWhenCustomizersAreNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.builder.additionalCustomizers((ThreadPoolTaskSchedulerCustomizer[]) null)) + .withMessageContaining("Customizers must not be null"); + } + + @Test + void additionalCustomizersCollectionWhenCustomizersAreNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.builder.additionalCustomizers((Set) null)) + .withMessageContaining("Customizers must not be null"); + } + + @Test + void additionalCustomizersShouldAddToExisting() { + ThreadPoolTaskSchedulerCustomizer customizer1 = mock(ThreadPoolTaskSchedulerCustomizer.class); + ThreadPoolTaskSchedulerCustomizer customizer2 = mock(ThreadPoolTaskSchedulerCustomizer.class); + ThreadPoolTaskScheduler scheduler = this.builder.customizers(customizer1) + .additionalCustomizers(customizer2) + .build(); + then(customizer1).should().customize(scheduler); + then(customizer2).should().customize(scheduler); + } + +} From 4ba7463d75ade289cb5c810d14927d72f8de4c1d Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 1 Aug 2023 15:34:52 +0200 Subject: [PATCH 0214/1215] Polish --- .../autoconfigure/mongo/MongoClientFactorySupportTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoClientFactorySupportTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoClientFactorySupportTests.java index 87861cf2d0b7..d596a1ff92c8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoClientFactorySupportTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoClientFactorySupportTests.java @@ -43,7 +43,7 @@ * @author Mark Paluch * @author Artsiom Yudovin * @author Scott Frederick - * @author Mortiz Halbritter + * @author Moritz Halbritter */ abstract class MongoClientFactorySupportTests { From 19859a9023b0bb0368556b525fd418421b053920 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 1 Aug 2023 15:40:05 +0200 Subject: [PATCH 0215/1215] Simplify TaskExecutionAutoConfiguration --- .../task/TaskExecutionAutoConfiguration.java | 3 +-- .../task/TaskExecutorConfigurations.java | 15 +++++---------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java index 8d6ef29d6769..2f76a06a8c76 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java @@ -38,8 +38,7 @@ @Import({ TaskExecutorConfigurations.ThreadPoolTaskExecutorBuilderConfiguration.class, TaskExecutorConfigurations.TaskExecutorBuilderConfiguration.class, TaskExecutorConfigurations.SimpleAsyncTaskExecutorBuilderConfiguration.class, - TaskExecutorConfigurations.VirtualThreadTaskExecutorConfiguration.class, - TaskExecutorConfigurations.ThreadPoolTaskExecutorConfiguration.class }) + TaskExecutorConfigurations.TaskExecutorConfiguration.class }) public class TaskExecutionAutoConfiguration { /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java index 91ce9daef4a5..ccefd966e49f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java @@ -47,27 +47,22 @@ */ class TaskExecutorConfigurations { - @ConditionalOnThreading(Threading.VIRTUAL) @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(Executor.class) - static class VirtualThreadTaskExecutorConfiguration { + @SuppressWarnings("removal") + static class TaskExecutorConfiguration { @Bean(name = { TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME, AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME }) - SimpleAsyncTaskExecutor applicationTaskExecutor(SimpleAsyncTaskExecutorBuilder builder) { + @ConditionalOnThreading(Threading.VIRTUAL) + SimpleAsyncTaskExecutor applicationTaskExecutorVirtualThreads(SimpleAsyncTaskExecutorBuilder builder) { return builder.build(); } - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(Executor.class) - @SuppressWarnings("removal") - static class ThreadPoolTaskExecutorConfiguration { - @Lazy @Bean(name = { TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME, AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME }) + @ConditionalOnThreading(Threading.PLATFORM) ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder taskExecutorBuilder, ObjectProvider threadPoolTaskExecutorBuilderProvider) { ThreadPoolTaskExecutorBuilder threadPoolTaskExecutorBuilder = threadPoolTaskExecutorBuilderProvider From 191ac10009cdf1f0663221dad16993493ca50205 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 1 Aug 2023 16:16:13 +0200 Subject: [PATCH 0216/1215] Deprecate support for OkHttp Closes gh-36632 --- .../src/docs/asciidoc/io/rest-client.adoc | 2 +- .../web/client/TestRestTemplateTests.java | 9 ++--- .../client/ClientHttpRequestFactories.java | 21 ++++++----- ...lientHttpRequestFactoriesRuntimeHints.java | 15 +++++--- ...lientHttpRequestFactoriesOkHttp3Tests.java | 4 ++- ...lientHttpRequestFactoriesOkHttp4Tests.java | 4 ++- ...HttpRequestFactoriesRuntimeHintsTests.java | 2 ++ .../ClientHttpRequestFactoriesTests.java | 2 ++ ...eSenderBuilderOkHttp3IntegrationTests.java | 22 ++++++------ .../build.gradle | 2 +- .../couchbase/SecureCouchbaseContainer.java | 36 +++++++++++-------- 11 files changed, 73 insertions(+), 46 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/rest-client.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/rest-client.adoc index b371623f77c4..84eff6c87eb8 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/rest-client.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/rest-client.adoc @@ -181,8 +181,8 @@ Spring Boot will auto-detect which HTTP client to use with `RestClient` and `Res In order of preference, the following clients are supported: . Apache HttpClient -. OkHttp . Jetty HttpClient +. OkHttp (deprecated) . Simple JDK client (`HttpURLConnection`) If multiple clients are available on the classpath, the most preferred client will be used. diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/TestRestTemplateTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/TestRestTemplateTests.java index 53cb7b836672..11b9b881af7c 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/TestRestTemplateTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/TestRestTemplateTests.java @@ -38,7 +38,7 @@ import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; -import org.springframework.http.client.OkHttp3ClientHttpRequestFactory; +import org.springframework.http.client.JettyClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.mock.env.MockEnvironment; import org.springframework.mock.http.client.MockClientHttpRequest; @@ -86,15 +86,16 @@ void simple() { @Test void doNotReplaceCustomRequestFactory() { - RestTemplateBuilder builder = new RestTemplateBuilder().requestFactory(OkHttp3ClientHttpRequestFactory.class); + RestTemplateBuilder builder = new RestTemplateBuilder() + .requestFactory(HttpComponentsClientHttpRequestFactory.class); TestRestTemplate testRestTemplate = new TestRestTemplate(builder); assertThat(testRestTemplate.getRestTemplate().getRequestFactory()) - .isInstanceOf(OkHttp3ClientHttpRequestFactory.class); + .isInstanceOf(HttpComponentsClientHttpRequestFactory.class); } @Test void useTheSameRequestFactoryClassWithBasicAuth() { - OkHttp3ClientHttpRequestFactory customFactory = new OkHttp3ClientHttpRequestFactory(); + JettyClientHttpRequestFactory customFactory = new JettyClientHttpRequestFactory(); RestTemplateBuilder builder = new RestTemplateBuilder().requestFactory(() -> customFactory); TestRestTemplate testRestTemplate = new TestRestTemplate(builder).withBasicAuth("test", "test"); RestTemplate restTemplate = testRestTemplate.getRestTemplate(); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java index 93e4e407c862..0ecdc850a0b5 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java @@ -89,24 +89,25 @@ private ClientHttpRequestFactories() { * dependencies {@link ClassUtils#isPresent are available} is returned: *

    *
  1. {@link HttpComponentsClientHttpRequestFactory}
  2. - *
  3. {@link OkHttp3ClientHttpRequestFactory}
  4. *
  5. {@link JettyClientHttpRequestFactory}
  6. + *
  7. {@link OkHttp3ClientHttpRequestFactory} (deprecated)
  8. *
  9. {@link SimpleClientHttpRequestFactory}
  10. *
* @param settings the settings to apply * @return a new {@link ClientHttpRequestFactory} */ + @SuppressWarnings("removal") public static ClientHttpRequestFactory get(ClientHttpRequestFactorySettings settings) { Assert.notNull(settings, "Settings must not be null"); if (APACHE_HTTP_CLIENT_PRESENT) { return HttpComponents.get(settings); } - if (OKHTTP_CLIENT_PRESENT) { - return OkHttp.get(settings); - } if (JETTY_CLIENT_PRESENT) { return Jetty.get(settings); } + if (OKHTTP_CLIENT_PRESENT) { + return OkHttp.get(settings); + } return Simple.get(settings); } @@ -119,7 +120,7 @@ public static ClientHttpRequestFactory get(ClientHttpRequestFactorySettings sett *
  • {@link HttpComponentsClientHttpRequestFactory}
  • *
  • {@link JdkClientHttpRequestFactory}
  • *
  • {@link JettyClientHttpRequestFactory}
  • - *
  • {@link OkHttp3ClientHttpRequestFactory}
  • + *
  • {@link OkHttp3ClientHttpRequestFactory} (deprecated)
  • *
  • {@link SimpleClientHttpRequestFactory}
  • * * A {@code requestFactoryType} of {@link ClientHttpRequestFactory} is equivalent to @@ -129,7 +130,7 @@ public static ClientHttpRequestFactory get(ClientHttpRequestFactorySettings sett * @param settings the settings to apply * @return a new {@link ClientHttpRequestFactory} instance */ - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "removal" }) public static T get(Class requestFactoryType, ClientHttpRequestFactorySettings settings) { Assert.notNull(settings, "Settings must not be null"); @@ -139,9 +140,6 @@ public static T get(Class requestFactory if (requestFactoryType == HttpComponentsClientHttpRequestFactory.class) { return (T) HttpComponents.get(settings); } - if (requestFactoryType == OkHttp3ClientHttpRequestFactory.class) { - return (T) OkHttp.get(settings); - } if (requestFactoryType == JettyClientHttpRequestFactory.class) { return (T) Jetty.get(settings); } @@ -151,6 +149,9 @@ public static T get(Class requestFactory if (requestFactoryType == SimpleClientHttpRequestFactory.class) { return (T) Simple.get(settings); } + if (requestFactoryType == OkHttp3ClientHttpRequestFactory.class) { + return (T) OkHttp.get(settings); + } return get(() -> createRequestFactory(requestFactoryType), settings); } @@ -220,6 +221,8 @@ private static HttpClient createHttpClient(Duration readTimeout, SslBundle sslBu /** * Support for {@link OkHttp3ClientHttpRequestFactory}. */ + @Deprecated(since = "3.2.0", forRemoval = true) + @SuppressWarnings("removal") static class OkHttp { static OkHttp3ClientHttpRequestFactory get(ClientHttpRequestFactorySettings settings) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHints.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHints.java index 90f3db9af163..45a3744a4b82 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHints.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHints.java @@ -56,10 +56,6 @@ private void registerHints(ReflectionHints hints, ClassLoader classLoader) { typeHint.onReachableType(TypeReference.of(ClientHttpRequestFactories.APACHE_HTTP_CLIENT_CLASS)); registerReflectionHints(hints, HttpComponentsClientHttpRequestFactory.class); }); - hints.registerTypeIfPresent(classLoader, ClientHttpRequestFactories.OKHTTP_CLIENT_CLASS, (typeHint) -> { - typeHint.onReachableType(TypeReference.of(ClientHttpRequestFactories.OKHTTP_CLIENT_CLASS)); - registerReflectionHints(hints, OkHttp3ClientHttpRequestFactory.class); - }); hints.registerTypeIfPresent(classLoader, ClientHttpRequestFactories.JETTY_CLIENT_CLASS, (typeHint) -> { typeHint.onReachableType(TypeReference.of(ClientHttpRequestFactories.JETTY_CLIENT_CLASS)); registerReflectionHints(hints, JettyClientHttpRequestFactory.class, long.class); @@ -68,6 +64,17 @@ private void registerHints(ReflectionHints hints, ClassLoader classLoader) { typeHint.onReachableType(HttpURLConnection.class); registerReflectionHints(hints, SimpleClientHttpRequestFactory.class); }); + registerOkHttpHints(hints, classLoader); + } + + @SuppressWarnings("removal") + @Deprecated(since = "3.2.0", forRemoval = true) + private void registerOkHttpHints(ReflectionHints hints, ClassLoader classLoader) { + hints.registerTypeIfPresent(classLoader, ClientHttpRequestFactories.OKHTTP_CLIENT_CLASS, (typeHint) -> { + typeHint.onReachableType(TypeReference.of(ClientHttpRequestFactories.OKHTTP_CLIENT_CLASS)); + registerReflectionHints(hints, OkHttp3ClientHttpRequestFactory.class); + }); + } private void registerReflectionHints(ReflectionHints hints, diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp3Tests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp3Tests.java index b54e8050d2e5..ad414d38f298 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp3Tests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp3Tests.java @@ -35,7 +35,9 @@ * @author Andy Wilkinson */ @ClassPathOverrides("com.squareup.okhttp3:okhttp:3.14.9") -@ClassPathExclusions("httpclient5-*.jar") +@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar" }) +@Deprecated(since = "3.2.0") +@SuppressWarnings("removal") class ClientHttpRequestFactoriesOkHttp3Tests extends AbstractClientHttpRequestFactoriesTests { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp4Tests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp4Tests.java index d1f533f73146..f6241c7a413e 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp4Tests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesOkHttp4Tests.java @@ -33,7 +33,9 @@ * * @author Andy Wilkinson */ -@ClassPathExclusions("httpclient5-*.jar") +@ClassPathExclusions({ "httpclient5-*.jar", "jetty-client-*.jar" }) +@Deprecated(since = "3.2.0") +@SuppressWarnings("removal") class ClientHttpRequestFactoriesOkHttp4Tests extends AbstractClientHttpRequestFactoriesTests { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHintsTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHintsTests.java index 7eee323f1130..1fc41a8e29e3 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHintsTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesRuntimeHintsTests.java @@ -63,6 +63,8 @@ void shouldRegisterHttpComponentHints() { } @Test + @Deprecated(since = "3.2.0") + @SuppressWarnings("removal") void shouldRegisterOkHttpHints() { RuntimeHints hints = new RuntimeHints(); new ClientHttpRequestFactoriesRuntimeHints().registerHints(hints, getClass().getClassLoader()); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesTests.java index 239023e8b9c0..ff27a20d03a5 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactoriesTests.java @@ -70,6 +70,8 @@ void getOfHttpComponentsFactoryReturnsHttpComponentsFactory() { } @Test + @Deprecated(since = "3.2.0") + @SuppressWarnings("removal") void getOfOkHttpFactoryReturnsOkHttpFactory() { ClientHttpRequestFactory requestFactory = ClientHttpRequestFactories.get(OkHttp3ClientHttpRequestFactory.class, ClientHttpRequestFactorySettings.DEFAULTS); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderOkHttp3IntegrationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderOkHttp3IntegrationTests.java index e48feb9be032..3215dad49900 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderOkHttp3IntegrationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderOkHttp3IntegrationTests.java @@ -18,12 +18,12 @@ import java.time.Duration; -import okhttp3.OkHttpClient; +import org.eclipse.jetty.client.HttpClient; import org.junit.jupiter.api.Test; import org.springframework.boot.testsupport.classpath.ClassPathExclusions; import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.http.client.OkHttp3ClientHttpRequestFactory; +import org.springframework.http.client.JettyClientHttpRequestFactory; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.ws.transport.WebServiceMessageSender; import org.springframework.ws.transport.http.ClientHttpRequestMessageSender; @@ -42,9 +42,9 @@ class HttpWebServiceMessageSenderBuilderOkHttp3IntegrationTests { private final HttpWebServiceMessageSenderBuilder builder = new HttpWebServiceMessageSenderBuilder(); @Test - void buildUseOkHttp3ByDefault() { + void buildUseJettyClientIfHttpComponentsIsNotAvailable() { WebServiceMessageSender messageSender = this.builder.build(); - assertOkHttp3RequestFactory(messageSender); + assertJettyClientHttpRequestFactory(messageSender); } @Test @@ -52,19 +52,19 @@ void buildWithCustomTimeouts() { WebServiceMessageSender messageSender = this.builder.setConnectTimeout(Duration.ofSeconds(5)) .setReadTimeout(Duration.ofSeconds(2)) .build(); - OkHttp3ClientHttpRequestFactory factory = assertOkHttp3RequestFactory(messageSender); - OkHttpClient client = (OkHttpClient) ReflectionTestUtils.getField(factory, "client"); + JettyClientHttpRequestFactory factory = assertJettyClientHttpRequestFactory(messageSender); + HttpClient client = (HttpClient) ReflectionTestUtils.getField(factory, "httpClient"); assertThat(client).isNotNull(); - assertThat(client.connectTimeoutMillis()).isEqualTo(5000); - assertThat(client.readTimeoutMillis()).isEqualTo(2000); + assertThat(client.getConnectTimeout()).isEqualTo(5000); + assertThat(factory).hasFieldOrPropertyWithValue("readTimeout", 2000L); } - private OkHttp3ClientHttpRequestFactory assertOkHttp3RequestFactory(WebServiceMessageSender messageSender) { + private JettyClientHttpRequestFactory assertJettyClientHttpRequestFactory(WebServiceMessageSender messageSender) { assertThat(messageSender).isInstanceOf(ClientHttpRequestMessageSender.class); ClientHttpRequestMessageSender sender = (ClientHttpRequestMessageSender) messageSender; ClientHttpRequestFactory requestFactory = sender.getRequestFactory(); - assertThat(requestFactory).isInstanceOf(OkHttp3ClientHttpRequestFactory.class); - return (OkHttp3ClientHttpRequestFactory) requestFactory; + assertThat(requestFactory).isInstanceOf(JettyClientHttpRequestFactory.class); + return (JettyClientHttpRequestFactory) requestFactory; } } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-couchbase/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-couchbase/build.gradle index e66d4db03d24..fba550646efa 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-couchbase/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-couchbase/build.gradle @@ -13,9 +13,9 @@ dependencies { testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) testImplementation(project(":spring-boot-project:spring-boot-testcontainers")) - testImplementation("com.squareup.okhttp3:okhttp") testImplementation("io.projectreactor:reactor-core") testImplementation("io.projectreactor:reactor-test") + testImplementation("org.apache.httpcomponents.client5:httpclient5") testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.junit.platform:junit-platform-engine") testImplementation("org.junit.platform:junit-platform-launcher") diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-couchbase/src/test/java/smoketest/data/couchbase/SecureCouchbaseContainer.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-couchbase/src/test/java/smoketest/data/couchbase/SecureCouchbaseContainer.java index 1b40442edc74..70141b45403f 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-couchbase/src/test/java/smoketest/data/couchbase/SecureCouchbaseContainer.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-couchbase/src/test/java/smoketest/data/couchbase/SecureCouchbaseContainer.java @@ -17,13 +17,14 @@ package smoketest.data.couchbase; import java.time.Duration; +import java.util.Base64; import com.github.dockerjava.api.command.InspectContainerResponse; -import okhttp3.Credentials; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; import org.testcontainers.couchbase.CouchbaseContainer; import org.testcontainers.utility.MountableFile; @@ -33,6 +34,7 @@ * A {@link CouchbaseContainer} for Couchbase with SSL configuration. * * @author Scott Frederick + * @author Stephane Nicoll */ public class SecureCouchbaseContainer extends CouchbaseContainer { @@ -69,20 +71,26 @@ protected void containerIsStarting(InspectContainerResponse containerInfo) { } private void doHttpRequest(String path) { - Response response; - try { + HttpResponse response = post(path); + if (response.getCode() != 200) { + throw new IllegalStateException("Error calling Couchbase HTTP endpoint: " + response); + } + } + + private HttpResponse post(String path) { + try (CloseableHttpClient httpclient = HttpClients.createDefault()) { + String basicAuth = "Basic " + + Base64.getEncoder().encodeToString("%s:%s".formatted(ADMIN_USER, ADMIN_PASSWORD).getBytes()); String url = "http://%s:%d/%s".formatted(getHost(), getMappedPort(MANAGEMENT_PORT), path); - Request.Builder requestBuilder = new Request.Builder().url(url) - .header("Authorization", Credentials.basic(ADMIN_USER, ADMIN_PASSWORD)) - .post(RequestBody.create("".getBytes())); - response = new OkHttpClient().newCall(requestBuilder.build()).execute(); + ClassicHttpRequest httpPost = ClassicRequestBuilder.post(url) + .addHeader("Authorization", basicAuth) + .setEntity("") + .build(); + return httpclient.execute(httpPost, (response) -> response); } catch (Exception ex) { throw new IllegalStateException("Error calling Couchbase HTTP endpoint", ex); } - if (!response.isSuccessful()) { - throw new IllegalStateException("Error calling Couchbase HTTP endpoint: " + response); - } } } From 279f8221a558694e3b2a20b5c91a817a0be2d7a0 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 1 Aug 2023 16:50:56 +0200 Subject: [PATCH 0217/1215] Remove invalid check for String-based FactoryBean.OBJECT_TYPE_ATTRIBUTE Closes gh-36659 --- .../boot/test/mock/mockito/MockitoPostProcessor.java | 3 +-- .../test/mock/mockito/MockitoPostProcessorTests.java | 12 ------------ 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java index 6ccc03f142cd..334d90dcfb1a 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java @@ -252,12 +252,11 @@ private Set getExistingBeans(ConfigurableListableBeanFactory beanFactory Set beans = new LinkedHashSet<>( Arrays.asList(beanFactory.getBeanNamesForType(resolvableType, true, false))); Class type = resolvableType.resolve(Object.class); - String typeName = type.getName(); for (String beanName : beanFactory.getBeanNamesForType(FactoryBean.class, true, false)) { beanName = BeanFactoryUtils.transformedBeanName(beanName); BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName); Object attribute = beanDefinition.getAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE); - if (resolvableType.equals(attribute) || type.equals(attribute) || typeName.equals(attribute)) { + if (resolvableType.equals(attribute) || type.equals(attribute)) { beans.add(beanName); } } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessorTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessorTests.java index 99969dadd9c9..5bb4bace7047 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessorTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessorTests.java @@ -73,18 +73,6 @@ void cannotMockMultipleQualifiedBeans() { + " expected a single matching bean to replace but found [example1, example3]"); } - @Test - void canMockBeanProducedByFactoryBeanWithStringObjectTypeAttribute() { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - MockitoPostProcessor.register(context); - RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(TestFactoryBean.class); - factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, SomeInterface.class.getName()); - context.registerBeanDefinition("beanToBeMocked", factoryBeanDefinition); - context.register(MockedFactoryBean.class); - context.refresh(); - assertThat(Mockito.mockingDetails(context.getBean("beanToBeMocked")).isMock()).isTrue(); - } - @Test void canMockBeanProducedByFactoryBeanWithClassObjectTypeAttribute() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); From 75bb862255cc3d7b9b018228374641fc167cbaa0 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 1 Aug 2023 16:51:48 +0200 Subject: [PATCH 0218/1215] Remove dead code --- .../condition/ConditionalOnMissingBeanTests.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java index 06072ca23151..da64265c301d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java @@ -468,18 +468,6 @@ static class NonspecificFactoryBeanStringAttributeConfiguration { } - static class NonspecificFactoryBeanStringAttributeRegistrar implements ImportBeanDefinitionRegistrar { - - @Override - public void registerBeanDefinitions(AnnotationMetadata meta, BeanDefinitionRegistry registry) { - BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(NonspecificFactoryBean.class); - builder.addConstructorArgValue("foo"); - builder.getBeanDefinition().setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, ExampleBean.class.getName()); - registry.registerBeanDefinition("exampleBeanFactoryBean", builder.getBeanDefinition()); - } - - } - @Configuration(proxyBeanMethods = false) @Import(FactoryBeanRegistrar.class) static class RegisteredFactoryBeanConfiguration { From d0d545468a3860081aa5aa5ee9e5226e2a0022dd Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 1 Aug 2023 17:57:34 +0100 Subject: [PATCH 0219/1215] Revert "Remove invalid check for String-based FactoryBean.OBJECT_TYPE_ATTRIBUTE" This reverts commit 279f8221a558694e3b2a20b5c91a817a0be2d7a0. See gh-36659 --- .../boot/test/mock/mockito/MockitoPostProcessor.java | 3 ++- .../test/mock/mockito/MockitoPostProcessorTests.java | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java index 334d90dcfb1a..6ccc03f142cd 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java @@ -252,11 +252,12 @@ private Set getExistingBeans(ConfigurableListableBeanFactory beanFactory Set beans = new LinkedHashSet<>( Arrays.asList(beanFactory.getBeanNamesForType(resolvableType, true, false))); Class type = resolvableType.resolve(Object.class); + String typeName = type.getName(); for (String beanName : beanFactory.getBeanNamesForType(FactoryBean.class, true, false)) { beanName = BeanFactoryUtils.transformedBeanName(beanName); BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName); Object attribute = beanDefinition.getAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE); - if (resolvableType.equals(attribute) || type.equals(attribute)) { + if (resolvableType.equals(attribute) || type.equals(attribute) || typeName.equals(attribute)) { beans.add(beanName); } } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessorTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessorTests.java index 5bb4bace7047..99969dadd9c9 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessorTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessorTests.java @@ -73,6 +73,18 @@ void cannotMockMultipleQualifiedBeans() { + " expected a single matching bean to replace but found [example1, example3]"); } + @Test + void canMockBeanProducedByFactoryBeanWithStringObjectTypeAttribute() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + MockitoPostProcessor.register(context); + RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(TestFactoryBean.class); + factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, SomeInterface.class.getName()); + context.registerBeanDefinition("beanToBeMocked", factoryBeanDefinition); + context.register(MockedFactoryBean.class); + context.refresh(); + assertThat(Mockito.mockingDetails(context.getBean("beanToBeMocked")).isMock()).isTrue(); + } + @Test void canMockBeanProducedByFactoryBeanWithClassObjectTypeAttribute() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); From 2e50d11d864da86c0d799ece46962ce2e6f6cafa Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Fri, 28 Jul 2023 16:20:23 -0500 Subject: [PATCH 0220/1215] Add since attribute to @DeprecatedConfigurationProperty annotation Closes gh-36482 --- .../configuration-metadata/format.adoc | 5 ++++ ...figurationMetadataAnnotationProcessor.java | 14 ++++----- .../MetadataGenerationEnvironment.java | 26 ++++++++++++---- .../PropertyDescriptorResolver.java | 2 +- .../metadata/ConfigurationMetadata.java | 3 ++ .../metadata/ItemDeprecation.java | 25 ++++++++++++---- .../metadata/JsonConverter.java | 3 ++ .../metadata/JsonMarshaller.java | 1 + ...ationMetadataAnnotationProcessorTests.java | 20 ++++++------- ...ablePropertiesMetadataGenerationTests.java | 2 +- .../LombokMetadataGenerationTests.java | 6 ++-- .../MergeMetadataGenerationTests.java | 30 +++++++++++-------- .../MethodBasedMetadataGenerationTests.java | 8 ++--- .../metadata/JsonMarshallerTests.java | 8 ++--- .../metadata/Metadata.java | 12 +++++--- .../DeprecatedConfigurationProperty.java | 6 ++++ .../simple/DeprecatedSingleProperty.java | 2 +- .../DeprecatedConfigurationProperty.java | 7 +++++ 18 files changed, 116 insertions(+), 64 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/configuration-metadata/format.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/configuration-metadata/format.adoc index 589851084265..b2ce6c5dae9f 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/configuration-metadata/format.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/configuration-metadata/format.adoc @@ -198,6 +198,11 @@ The JSON object contained in the `deprecation` attribute of each `properties` el | String | The full name of the property that _replaces_ this deprecated property. If there is no replacement for this property, it may be omitted. + +| `since` +| String +| The version in which the property became deprecated. + Can be omitted. |=== NOTE: Prior to Spring Boot 1.3, a single `deprecated` boolean attribute can be used instead of the `deprecation` element. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java index 6c805144a8d8..134422b11aff 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java @@ -57,6 +57,7 @@ * @author Phillip Webb * @author Kris De Volder * @author Jonas Keßler + * @author Scott Frederick * @since 1.2.0 */ @SupportedAnnotationTypes({ ConfigurationMetadataAnnotationProcessor.AUTO_CONFIGURATION_ANNOTATION, @@ -322,16 +323,11 @@ private boolean hasNoOrOptionalParameters(ExecutableElement method) { } private String getPrefix(AnnotationMirror annotation) { - Map elementValues = this.metadataEnv.getAnnotationElementValues(annotation); - Object prefix = elementValues.get("prefix"); - if (prefix != null && !"".equals(prefix)) { - return (String) prefix; - } - Object value = elementValues.get("value"); - if (value != null && !"".equals(value)) { - return (String) value; + String prefix = this.metadataEnv.getAnnotationElementStringValue(annotation, "prefix"); + if (prefix != null) { + return prefix; } - return null; + return this.metadataEnv.getAnnotationElementStringValue(annotation, "value"); } protected ConfigurationMetadata writeMetadata() throws Exception { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java index 5109ffed45d6..6d4cc6c3d2ac 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataGenerationEnvironment.java @@ -49,6 +49,7 @@ * Provide utilities to detect and validate configuration properties. * * @author Stephane Nicoll + * @author Scott Frederick */ class MetadataGenerationEnvironment { @@ -174,14 +175,13 @@ ItemDeprecation resolveItemDeprecation(Element element) { AnnotationMirror annotation = getAnnotation(element, this.deprecatedConfigurationPropertyAnnotation); String reason = null; String replacement = null; + String since = null; if (annotation != null) { - Map elementValues = getAnnotationElementValues(annotation); - reason = (String) elementValues.get("reason"); - replacement = (String) elementValues.get("replacement"); + reason = getAnnotationElementStringValue(annotation, "reason"); + replacement = getAnnotationElementStringValue(annotation, "replacement"); + since = getAnnotationElementStringValue(annotation, "since"); } - reason = (reason == null || reason.isEmpty()) ? null : reason; - replacement = (replacement == null || replacement.isEmpty()) ? null : replacement; - return new ItemDeprecation(reason, replacement); + return new ItemDeprecation(reason, replacement, since); } boolean hasConstructorBindingAnnotation(ExecutableElement element) { @@ -279,6 +279,16 @@ Map getAnnotationElementValues(AnnotationMirror annotation) { return values; } + String getAnnotationElementStringValue(AnnotationMirror annotation, String name) { + return annotation.getElementValues() + .entrySet() + .stream() + .filter((element) -> element.getKey().getSimpleName().toString().equals(name)) + .map((element) -> asString(getAnnotationValue(element.getValue()))) + .findFirst() + .orElse(null); + } + private Object getAnnotationValue(AnnotationValue annotationValue) { Object value = annotationValue.getValue(); if (value instanceof List) { @@ -289,6 +299,10 @@ private Object getAnnotationValue(AnnotationValue annotationValue) { return value; } + private String asString(Object value) { + return (value == null || value.toString().isEmpty()) ? null : (String) value; + } + TypeElement getConfigurationPropertiesAnnotationElement() { return this.elements.getTypeElement(this.configurationPropertiesAnnotation); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java index 31e5630a9541..e71fb074eb20 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java @@ -91,7 +91,7 @@ Stream> resolveConstructorProperties(TypeElement type, Typ private String getParameterName(VariableElement parameter) { AnnotationMirror nameAnnotation = this.environment.getNameAnnotation(parameter); if (nameAnnotation != null) { - return (String) this.environment.getAnnotationElementValues(nameAnnotation).get("value"); + return this.environment.getAnnotationElementStringValue(nameAnnotation, "value"); } return parameter.getSimpleName().toString(); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java index 1281954f591c..a1fff60dffad 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java @@ -136,6 +136,9 @@ protected void mergeItemMetadata(ItemMetadata metadata) { if (deprecation.getLevel() != null) { matchingDeprecation.setLevel(deprecation.getLevel()); } + if (deprecation.getSince() != null) { + matchingDeprecation.setSince(deprecation.getSince()); + } } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemDeprecation.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemDeprecation.java index 2947e94f1554..21ac303b394a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemDeprecation.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemDeprecation.java @@ -20,6 +20,7 @@ * Describe an item deprecation. * * @author Stephane Nicoll + * @author Scott Frederick * @since 1.3.0 */ public class ItemDeprecation { @@ -28,19 +29,22 @@ public class ItemDeprecation { private String replacement; + private String since; + private String level; public ItemDeprecation() { - this(null, null); + this(null, null, null); } - public ItemDeprecation(String reason, String replacement) { - this(reason, replacement, null); + public ItemDeprecation(String reason, String replacement, String since) { + this(reason, replacement, since, null); } - public ItemDeprecation(String reason, String replacement, String level) { + public ItemDeprecation(String reason, String replacement, String since, String level) { this.reason = reason; this.replacement = replacement; + this.since = since; this.level = level; } @@ -60,6 +64,14 @@ public void setReplacement(String replacement) { this.replacement = replacement; } + public String getSince() { + return this.since; + } + + public void setSince(String since) { + this.since = since; + } + public String getLevel() { return this.level; } @@ -78,7 +90,7 @@ public boolean equals(Object o) { } ItemDeprecation other = (ItemDeprecation) o; return nullSafeEquals(this.reason, other.reason) && nullSafeEquals(this.replacement, other.replacement) - && nullSafeEquals(this.level, other.level); + && nullSafeEquals(this.level, other.level) && nullSafeEquals(this.since, other.since); } @Override @@ -86,13 +98,14 @@ public int hashCode() { int result = nullSafeHashCode(this.reason); result = 31 * result + nullSafeHashCode(this.replacement); result = 31 * result + nullSafeHashCode(this.level); + result = 31 * result + nullSafeHashCode(this.since); return result; } @Override public String toString() { return "ItemDeprecation{reason='" + this.reason + '\'' + ", replacement='" + this.replacement + '\'' - + ", level='" + this.level + '\'' + '}'; + + ", level='" + this.level + '\'' + ", since='" + this.since + '\'' + '}'; } private boolean nullSafeEquals(Object o1, Object o2) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonConverter.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonConverter.java index 4c4cfda98ab4..3853a853fc27 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonConverter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonConverter.java @@ -83,6 +83,9 @@ JSONObject toJsonObject(ItemMetadata item) throws Exception { if (deprecation.getReplacement() != null) { deprecationJsonObject.put("replacement", deprecation.getReplacement()); } + if (deprecation.getSince() != null) { + deprecationJsonObject.put("since", deprecation.getSince()); + } jsonObject.put("deprecation", deprecationJsonObject); } return jsonObject; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java index 53370badb6da..3243adb17853 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java @@ -105,6 +105,7 @@ private ItemDeprecation toItemDeprecation(JSONObject object) throws Exception { deprecation.setLevel(deprecationJsonObject.optString("level", null)); deprecation.setReason(deprecationJsonObject.optString("reason", null)); deprecation.setReplacement(deprecationJsonObject.optString("replacement", null)); + deprecation.setSince(deprecationJsonObject.optString("since", null)); return deprecation; } return object.optBoolean("deprecated") ? new ItemDeprecation() : null; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java index 93227ecf6196..d1a831a95105 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java @@ -104,12 +104,12 @@ void simpleProperties() { .fromSource(SimpleProperties.class) .withDescription("The name of this simple properties.") .withDefaultValue("boot") - .withDeprecation(null, null)); + .withDeprecation()); assertThat(metadata).has(Metadata.withProperty("simple.flag", Boolean.class) .withDefaultValue(false) .fromSource(SimpleProperties.class) .withDescription("A simple flag.") - .withDeprecation(null, null)); + .withDeprecation()); assertThat(metadata).has(Metadata.withProperty("simple.comparator")); assertThat(metadata).doesNotHave(Metadata.withProperty("simple.counter")); assertThat(metadata).doesNotHave(Metadata.withProperty("simple.size")); @@ -188,10 +188,9 @@ void deprecatedProperties() { ConfigurationMetadata metadata = compile(type); assertThat(metadata).has(Metadata.withGroup("deprecated").fromSource(type)); assertThat(metadata) - .has(Metadata.withProperty("deprecated.name", String.class).fromSource(type).withDeprecation(null, null)); - assertThat(metadata).has(Metadata.withProperty("deprecated.description", String.class) - .fromSource(type) - .withDeprecation(null, null)); + .has(Metadata.withProperty("deprecated.name", String.class).fromSource(type).withDeprecation()); + assertThat(metadata) + .has(Metadata.withProperty("deprecated.description", String.class).fromSource(type).withDeprecation()); } @Test @@ -202,7 +201,7 @@ void singleDeprecatedProperty() { assertThat(metadata).has(Metadata.withProperty("singledeprecated.new-name", String.class).fromSource(type)); assertThat(metadata).has(Metadata.withProperty("singledeprecated.name", String.class) .fromSource(type) - .withDeprecation("renamed", "singledeprecated.new-name")); + .withDeprecation("renamed", "singledeprecated.new-name", "1.2.3")); } @Test @@ -210,9 +209,8 @@ void singleDeprecatedFieldProperty() { Class type = DeprecatedFieldSingleProperty.class; ConfigurationMetadata metadata = compile(type); assertThat(metadata).has(Metadata.withGroup("singlefielddeprecated").fromSource(type)); - assertThat(metadata).has(Metadata.withProperty("singlefielddeprecated.name", String.class) - .fromSource(type) - .withDeprecation(null, null)); + assertThat(metadata) + .has(Metadata.withProperty("singlefielddeprecated.name", String.class).fromSource(type).withDeprecation()); } @Test @@ -246,7 +244,7 @@ void deprecatedPropertyOnRecord() { assertThat(metadata).has(Metadata.withGroup("deprecated-record").fromSource(type)); assertThat(metadata).has(Metadata.withProperty("deprecated-record.alpha", String.class) .fromSource(type) - .withDeprecation("some-reason", null)); + .withDeprecation("some-reason", null, null)); assertThat(metadata).has(Metadata.withProperty("deprecated-record.bravo", String.class).fromSource(type)); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ImmutablePropertiesMetadataGenerationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ImmutablePropertiesMetadataGenerationTests.java index 6e3571539ff3..5b87c0137ffe 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ImmutablePropertiesMetadataGenerationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ImmutablePropertiesMetadataGenerationTests.java @@ -43,7 +43,7 @@ void immutableSimpleProperties() { .withDefaultValue(false) .fromSource(ImmutableSimpleProperties.class) .withDescription("A simple flag.") - .withDeprecation(null, null)); + .withDeprecation()); assertThat(metadata).has(Metadata.withProperty("immutable.comparator")); assertThat(metadata).has(Metadata.withProperty("immutable.counter")); assertThat(metadata.getItems()).hasSize(5); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/LombokMetadataGenerationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/LombokMetadataGenerationTests.java index 69f0bb871842..2721f0c28a52 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/LombokMetadataGenerationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/LombokMetadataGenerationTests.java @@ -139,10 +139,8 @@ private void assertSimpleLombokProperties(ConfigurationMetadata metadata, Class< .withDescription("Name description.")); assertThat(metadata).has(Metadata.withProperty(prefix + ".description")); assertThat(metadata).has(Metadata.withProperty(prefix + ".counter")); - assertThat(metadata).has(Metadata.withProperty(prefix + ".number") - .fromSource(source) - .withDefaultValue(0) - .withDeprecation(null, null)); + assertThat(metadata) + .has(Metadata.withProperty(prefix + ".number").fromSource(source).withDefaultValue(0).withDeprecation()); assertThat(metadata).has(Metadata.withProperty(prefix + ".items")); assertThat(metadata).doesNotHave(Metadata.withProperty(prefix + ".ignored")); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MergeMetadataGenerationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MergeMetadataGenerationTests.java index 167feb22f99e..c35e74755c85 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MergeMetadataGenerationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MergeMetadataGenerationTests.java @@ -74,7 +74,7 @@ void mergeExistingPropertyDefaultValue() throws Exception { assertThat(metadata).has(Metadata.withProperty("simple.flag", Boolean.class) .fromSource(SimpleProperties.class) .withDescription("A simple flag.") - .withDeprecation(null, null) + .withDeprecation() .withDefaultValue(true)); assertThat(metadata.getItems()).hasSize(4); } @@ -125,36 +125,36 @@ void mergeExistingPropertyDescription() throws Exception { @Test void mergeExistingPropertyDeprecation() throws Exception { ItemMetadata property = ItemMetadata.newProperty("simple", "comparator", null, null, null, null, null, - new ItemDeprecation("Don't use this.", "simple.complex-comparator", "error")); + new ItemDeprecation("Don't use this.", "simple.complex-comparator", "1.2.3", "error")); String additionalMetadata = buildAdditionalMetadata(property); ConfigurationMetadata metadata = compile(additionalMetadata, SimpleProperties.class); assertThat(metadata).has(Metadata.withProperty("simple.comparator", "java.util.Comparator") .fromSource(SimpleProperties.class) - .withDeprecation("Don't use this.", "simple.complex-comparator", "error")); + .withDeprecation("Don't use this.", "simple.complex-comparator", "1.2.3", "error")); assertThat(metadata.getItems()).hasSize(4); } @Test void mergeExistingPropertyDeprecationOverride() throws Exception { ItemMetadata property = ItemMetadata.newProperty("singledeprecated", "name", null, null, null, null, null, - new ItemDeprecation("Don't use this.", "single.name")); + new ItemDeprecation("Don't use this.", "single.name", "1.2.3")); String additionalMetadata = buildAdditionalMetadata(property); ConfigurationMetadata metadata = compile(additionalMetadata, DeprecatedSingleProperty.class); assertThat(metadata).has(Metadata.withProperty("singledeprecated.name", String.class.getName()) .fromSource(DeprecatedSingleProperty.class) - .withDeprecation("Don't use this.", "single.name")); + .withDeprecation("Don't use this.", "single.name", "1.2.3")); assertThat(metadata.getItems()).hasSize(3); } @Test void mergeExistingPropertyDeprecationOverrideLevel() throws Exception { ItemMetadata property = ItemMetadata.newProperty("singledeprecated", "name", null, null, null, null, null, - new ItemDeprecation(null, null, "error")); + new ItemDeprecation(null, null, null, "error")); String additionalMetadata = buildAdditionalMetadata(property); ConfigurationMetadata metadata = compile(additionalMetadata, DeprecatedSingleProperty.class); assertThat(metadata).has(Metadata.withProperty("singledeprecated.name", String.class.getName()) .fromSource(DeprecatedSingleProperty.class) - .withDeprecation("renamed", "singledeprecated.new-name", "error")); + .withDeprecation("renamed", "singledeprecated.new-name", "1.2.3", "error")); assertThat(metadata.getItems()).hasSize(3); } @@ -175,7 +175,7 @@ void mergingOfSimpleHint() throws Exception { .fromSource(SimpleProperties.class) .withDescription("The name of this simple properties.") .withDefaultValue("boot") - .withDeprecation(null, null)); + .withDeprecation()); assertThat(metadata) .has(Metadata.withHint("simple.the-name").withValue(0, "boot", "Bla bla").withValue(1, "spring", null)); } @@ -189,7 +189,7 @@ void mergingOfHintWithNonCanonicalName() throws Exception { .fromSource(SimpleProperties.class) .withDescription("The name of this simple properties.") .withDefaultValue("boot") - .withDeprecation(null, null)); + .withDeprecation()); assertThat(metadata).has(Metadata.withHint("simple.the-name").withValue(0, "boot", "Bla bla")); } @@ -203,18 +203,19 @@ void mergingOfHintWithProvider() throws Exception { .fromSource(SimpleProperties.class) .withDescription("The name of this simple properties.") .withDefaultValue("boot") - .withDeprecation(null, null)); + .withDeprecation()); assertThat(metadata).has( Metadata.withHint("simple.the-name").withProvider("first", "target", "org.foo").withProvider("second")); } @Test void mergingOfAdditionalDeprecation() throws Exception { - String deprecations = buildPropertyDeprecations(ItemMetadata.newProperty("simple", "wrongName", - "java.lang.String", null, null, null, null, new ItemDeprecation("Lame name.", "simple.the-name"))); + String deprecations = buildPropertyDeprecations( + ItemMetadata.newProperty("simple", "wrongName", "java.lang.String", null, null, null, null, + new ItemDeprecation("Lame name.", "simple.the-name", "1.2.3"))); ConfigurationMetadata metadata = compile(deprecations, SimpleProperties.class); assertThat(metadata).has(Metadata.withProperty("simple.wrong-name", String.class) - .withDeprecation("Lame name.", "simple.the-name")); + .withDeprecation("Lame name.", "simple.the-name", "1.2.3")); } @Test @@ -268,6 +269,9 @@ private String buildPropertyDeprecations(ItemMetadata... items) throws Exception if (deprecation.getReplacement() != null) { deprecationJson.put("replacement", deprecation.getReplacement()); } + if (deprecation.getSince() != null) { + deprecationJson.put("since", deprecation.getSince()); + } jsonObject.put("deprecation", deprecationJson); } propertiesArray.put(jsonObject); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MethodBasedMetadataGenerationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MethodBasedMetadataGenerationTests.java index e7391bf98080..591c410b16b0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MethodBasedMetadataGenerationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/MethodBasedMetadataGenerationTests.java @@ -114,11 +114,11 @@ void deprecatedMethodConfig() { assertThat(metadata).has(Metadata.withGroup("foo").fromSource(type)); assertThat(metadata).has(Metadata.withProperty("foo.name", String.class) .fromSource(DeprecatedMethodConfig.Foo.class) - .withDeprecation(null, null)); + .withDeprecation()); assertThat(metadata).has(Metadata.withProperty("foo.flag", Boolean.class) .withDefaultValue(false) .fromSource(DeprecatedMethodConfig.Foo.class) - .withDeprecation(null, null)); + .withDeprecation()); } @Test @@ -129,11 +129,11 @@ void deprecatedMethodConfigOnClass() { assertThat(metadata).has(Metadata.withGroup("foo").fromSource(type)); assertThat(metadata).has(Metadata.withProperty("foo.name", String.class) .fromSource(org.springframework.boot.configurationsample.method.DeprecatedClassMethodConfig.Foo.class) - .withDeprecation(null, null)); + .withDeprecation()); assertThat(metadata).has(Metadata.withProperty("foo.flag", Boolean.class) .withDefaultValue(false) .fromSource(org.springframework.boot.configurationsample.method.DeprecatedClassMethodConfig.Foo.class) - .withDeprecation(null, null)); + .withDeprecation()); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java index 2cbda570e8a0..f7054f721200 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java @@ -39,7 +39,7 @@ class JsonMarshallerTests { void marshallAndUnmarshal() throws Exception { ConfigurationMetadata metadata = new ConfigurationMetadata(); metadata.add(ItemMetadata.newProperty("a", "b", StringBuffer.class.getName(), InputStream.class.getName(), - "sourceMethod", "desc", "x", new ItemDeprecation("Deprecation comment", "b.c.d"))); + "sourceMethod", "desc", "x", new ItemDeprecation("Deprecation comment", "b.c.d", "1.2.3"))); metadata.add(ItemMetadata.newProperty("b.c.d", null, null, null, null, null, null, null)); metadata.add(ItemMetadata.newProperty("c", null, null, null, null, null, 123, null)); metadata.add(ItemMetadata.newProperty("d", null, null, null, null, null, true, null)); @@ -59,7 +59,7 @@ void marshallAndUnmarshal() throws Exception { .fromSource(InputStream.class) .withDescription("desc") .withDefaultValue("x") - .withDeprecation("Deprecation comment", "b.c.d")); + .withDeprecation("Deprecation comment", "b.c.d", "1.2.3")); assertThat(read).has(Metadata.withProperty("b.c.d")); assertThat(read).has(Metadata.withProperty("c").withDefaultValue(123)); assertThat(read).has(Metadata.withProperty("d").withDefaultValue(true)); @@ -96,10 +96,10 @@ void marshallPutDeprecatedItemsAtTheEnd() throws IOException { ConfigurationMetadata metadata = new ConfigurationMetadata(); metadata.add(ItemMetadata.newProperty("com.example.bravo", "bbb", null, null, null, null, null, null)); metadata.add(ItemMetadata.newProperty("com.example.bravo", "aaa", null, null, null, null, null, - new ItemDeprecation(null, null, "warning"))); + new ItemDeprecation(null, null, null, "warning"))); metadata.add(ItemMetadata.newProperty("com.example.alpha", "ddd", null, null, null, null, null, null)); metadata.add(ItemMetadata.newProperty("com.example.alpha", "ccc", null, null, null, null, null, - new ItemDeprecation(null, null, "warning"))); + new ItemDeprecation(null, null, null, "warning"))); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); JsonMarshaller marshaller = new JsonMarshaller(); marshaller.write(metadata, outputStream); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/Metadata.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/Metadata.java index 39be910daf52..0930084cf4cc 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/Metadata.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/Metadata.java @@ -193,13 +193,17 @@ public MetadataItemCondition withDefaultValue(Object defaultValue) { this.description, defaultValue, this.deprecation); } - public MetadataItemCondition withDeprecation(String reason, String replacement) { - return withDeprecation(reason, replacement, null); + public MetadataItemCondition withDeprecation() { + return withDeprecation(null, null, null, null); } - public MetadataItemCondition withDeprecation(String reason, String replacement, String level) { + public MetadataItemCondition withDeprecation(String reason, String replacement, String since) { + return withDeprecation(reason, replacement, since, null); + } + + public MetadataItemCondition withDeprecation(String reason, String replacement, String since, String level) { return new MetadataItemCondition(this.itemType, this.name, this.type, this.sourceType, this.sourceMethod, - this.description, this.defaultValue, new ItemDeprecation(reason, replacement, level)); + this.description, this.defaultValue, new ItemDeprecation(reason, replacement, since, level)); } public MetadataItemCondition withNoDeprecation() { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/DeprecatedConfigurationProperty.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/DeprecatedConfigurationProperty.java index 0853090e9388..29116a508503 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/DeprecatedConfigurationProperty.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/DeprecatedConfigurationProperty.java @@ -50,4 +50,10 @@ */ String replacement() default ""; + /** + * The version in which the property became deprecated. + * @return the version + */ + String since() default ""; + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/DeprecatedSingleProperty.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/DeprecatedSingleProperty.java index f8df3f7c4aeb..904d3100af22 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/DeprecatedSingleProperty.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/DeprecatedSingleProperty.java @@ -30,7 +30,7 @@ public class DeprecatedSingleProperty { private String newName; @Deprecated - @DeprecatedConfigurationProperty(reason = "renamed", replacement = "singledeprecated.new-name") + @DeprecatedConfigurationProperty(reason = "renamed", replacement = "singledeprecated.new-name", since = "1.2.3") public String getName() { return getNewName(); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/DeprecatedConfigurationProperty.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/DeprecatedConfigurationProperty.java index 23940cde1271..7ac7bc98592d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/DeprecatedConfigurationProperty.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/DeprecatedConfigurationProperty.java @@ -31,6 +31,7 @@ * This annotation must be used on the getter of the deprecated element. * * @author Phillip Webb + * @author Scott Frederick * @since 1.3.0 */ @Target(ElementType.METHOD) @@ -50,4 +51,10 @@ */ String replacement() default ""; + /** + * The version in which the property became deprecated. + * @return the version + */ + String since() default ""; + } From f2ad08c29278e663ae34e1983ccff9b1079b878f Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Tue, 1 Aug 2023 14:38:42 -0500 Subject: [PATCH 0221/1215] Add since version to deprecated config properties See gh-36482 --- .../actuate/autoconfigure/metrics/MetricsProperties.java | 3 ++- .../autoconfigure/metrics/PropertiesMeterFilter.java | 1 + .../metrics/export/otlp/OtlpProperties.java | 4 +++- .../metrics/export/otlp/OtlpPropertiesConfigAdapter.java | 1 + .../export/otlp/OtlpPropertiesConfigAdapterTests.java | 3 +++ .../autoconfigure/couchbase/CouchbaseProperties.java | 6 ++++-- .../boot/autoconfigure/flyway/FlywayProperties.java | 8 ++++---- .../boot/autoconfigure/influx/InfluxDbProperties.java | 9 ++++++--- .../boot/autoconfigure/kafka/KafkaProperties.java | 3 ++- .../boot/autoconfigure/web/ServerProperties.java | 6 ++++-- .../web/embedded/TomcatWebServerFactoryCustomizer.java | 1 + .../boot/autoconfigure/web/servlet/WebMvcProperties.java | 3 ++- .../boot/autoconfigure/web/ServerPropertiesTests.java | 2 ++ 13 files changed, 35 insertions(+), 15 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java index d3994aa32893..2bafe29923f2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java @@ -79,7 +79,8 @@ public Map getEnable() { return this.enable; } - @DeprecatedConfigurationProperty(replacement = "management.observations.key-values") + @Deprecated(since = "3.2.0", forRemoval = true) + @DeprecatedConfigurationProperty(replacement = "management.observations.key-values", since = "3.2.0") public Map getTags() { return this.tags; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilter.java index bf506756478a..6293056f58a4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilter.java @@ -50,6 +50,7 @@ public class PropertiesMeterFilter implements MeterFilter { private final MeterFilter mapFilter; + @SuppressWarnings("removal") public PropertiesMeterFilter(MetricsProperties properties) { Assert.notNull(properties, "Properties must not be null"); this.properties = properties; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpProperties.java index ba3cc1145b19..e9a038d3e664 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpProperties.java @@ -78,11 +78,13 @@ public void setAggregationTemporality(AggregationTemporality aggregationTemporal this.aggregationTemporality = aggregationTemporality; } - @DeprecatedConfigurationProperty(replacement = "management.opentelemetry.resource-attributes") + @Deprecated(since = "3.2.0", forRemoval = true) + @DeprecatedConfigurationProperty(replacement = "management.opentelemetry.resource-attributes", since = "3.2.0") public Map getResourceAttributes() { return this.resourceAttributes; } + @Deprecated(since = "3.2.0", forRemoval = true) public void setResourceAttributes(Map resourceAttributes) { this.resourceAttributes = resourceAttributes; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java index 2d225499cbb0..ebf406f30f93 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java @@ -58,6 +58,7 @@ public AggregationTemporality aggregationTemporality() { } @Override + @SuppressWarnings("removal") public Map resourceAttributes() { if (!CollectionUtils.isEmpty(this.openTelemetryProperties.getResourceAttributes())) { return this.openTelemetryProperties.getResourceAttributes(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java index 5d6cbea3ffa3..27bd22ccb7a3 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java @@ -65,6 +65,7 @@ void whenPropertiesAggregationTemporalityIsSetAdapterAggregationTemporalityRetur } @Test + @SuppressWarnings("removal") void whenPropertiesResourceAttributesIsSetAdapterResourceAttributesReturnsIt() { this.properties.setResourceAttributes(Map.of("service.name", "boot-service")); assertThat(createAdapter().resourceAttributes()).containsEntry("service.name", "boot-service"); @@ -88,6 +89,7 @@ void whenPropertiesBaseTimeUnitIsSetAdapterBaseTimeUnitReturnsIt() { } @Test + @SuppressWarnings("removal") void openTelemetryPropertiesShouldOverrideOtlpPropertiesIfNotEmpty() { this.properties.setResourceAttributes(Map.of("a", "alpha")); this.openTelemetryProperties.setResourceAttributes(Map.of("b", "beta")); @@ -95,6 +97,7 @@ void openTelemetryPropertiesShouldOverrideOtlpPropertiesIfNotEmpty() { } @Test + @SuppressWarnings("removal") void openTelemetryPropertiesShouldNotOverrideOtlpPropertiesIfEmpty() { this.properties.setResourceAttributes(Map.of("a", "alpha")); this.openTelemetryProperties.setResourceAttributes(Collections.emptyMap()); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseProperties.java index 6132c73bbd34..222d71794dce 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseProperties.java @@ -180,7 +180,8 @@ public void setEnabled(Boolean enabled) { @Deprecated(since = "3.1.0", forRemoval = true) @DeprecatedConfigurationProperty( - reason = "SSL bundle support with spring.ssl.bundle and spring.couchbase.env.ssl.bundle should be used instead") + reason = "SSL bundle support with spring.ssl.bundle and spring.couchbase.env.ssl.bundle should be used instead", + since = "3.1.0") public String getKeyStore() { return this.keyStore; } @@ -192,7 +193,8 @@ public void setKeyStore(String keyStore) { @Deprecated(since = "3.1.0", forRemoval = true) @DeprecatedConfigurationProperty( - reason = "SSL bundle support with spring.ssl.bundle and spring.couchbase.env.ssl.bundle should be used instead") + reason = "SSL bundle support with spring.ssl.bundle and spring.couchbase.env.ssl.bundle should be used instead", + since = "3.1.0") public String getKeyStorePassword() { return this.keyStorePassword; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java index bf1ea687e509..4a6d7db6e0ca 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayProperties.java @@ -736,7 +736,7 @@ public void setLicenseKey(String licenseKey) { this.licenseKey = licenseKey; } - @DeprecatedConfigurationProperty(replacement = "spring.flyway.oracle.sqlplus") + @DeprecatedConfigurationProperty(replacement = "spring.flyway.oracle.sqlplus", since = "3.2.0") @Deprecated(since = "3.2.0", forRemoval = true) public Boolean getOracleSqlplus() { return getOracle().getSqlplus(); @@ -747,7 +747,7 @@ public void setOracleSqlplus(Boolean oracleSqlplus) { getOracle().setSqlplus(oracleSqlplus); } - @DeprecatedConfigurationProperty(replacement = "spring.flyway.oracle.sqlplus-warn") + @DeprecatedConfigurationProperty(replacement = "spring.flyway.oracle.sqlplus-warn", since = "3.2.0") @Deprecated(since = "3.2.0", forRemoval = true) public Boolean getOracleSqlplusWarn() { return getOracle().getSqlplusWarn(); @@ -758,7 +758,7 @@ public void setOracleSqlplusWarn(Boolean oracleSqlplusWarn) { getOracle().setSqlplusWarn(oracleSqlplusWarn); } - @DeprecatedConfigurationProperty(replacement = "spring.flyway.oracle.wallet-location") + @DeprecatedConfigurationProperty(replacement = "spring.flyway.oracle.wallet-location", since = "3.2.0") @Deprecated(since = "3.2.0", forRemoval = true) public String getOracleWalletLocation() { return getOracle().getWalletLocation(); @@ -809,7 +809,7 @@ public void setKerberosConfigFile(String kerberosConfigFile) { this.kerberosConfigFile = kerberosConfigFile; } - @DeprecatedConfigurationProperty(replacement = "spring.flyway.oracle.kerberos-cache-file") + @DeprecatedConfigurationProperty(replacement = "spring.flyway.oracle.kerberos-cache-file", since = "3.2.0") @Deprecated(since = "3.2.0", forRemoval = true) public String getOracleKerberosCacheFile() { return getOracle().getKerberosCacheFile(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbProperties.java index e4fd9d26b18d..145c490c2762 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/influx/InfluxDbProperties.java @@ -48,7 +48,8 @@ public class InfluxDbProperties { */ private String password; - @DeprecatedConfigurationProperty(reason = "the new InfluxDb Java client provides Spring Boot integration") + @DeprecatedConfigurationProperty(reason = "the new InfluxDb Java client provides Spring Boot integration", + since = "3.2.0") public String getUrl() { return this.url; } @@ -57,7 +58,8 @@ public void setUrl(String url) { this.url = url; } - @DeprecatedConfigurationProperty(reason = "the new InfluxDb Java client provides Spring Boot integration") + @DeprecatedConfigurationProperty(reason = "the new InfluxDb Java client provides Spring Boot integration", + since = "3.2.0") public String getUser() { return this.user; } @@ -66,7 +68,8 @@ public void setUser(String user) { this.user = user; } - @DeprecatedConfigurationProperty(reason = "the new InfluxDb Java client provides Spring Boot integration") + @DeprecatedConfigurationProperty(reason = "the new InfluxDb Java client provides Spring Boot integration", + since = "3.2.0") public String getPassword() { return this.password; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java index c4b2f5573493..7227df43f233 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java @@ -837,7 +837,8 @@ public void setBootstrapServers(List bootstrapServers) { this.bootstrapServers = bootstrapServers; } - @DeprecatedConfigurationProperty(replacement = "spring.kafka.streams.state-store-cache-max-size") + @DeprecatedConfigurationProperty(replacement = "spring.kafka.streams.state-store-cache-max-size", + since = "3.1.0") @Deprecated(since = "3.1.0", forRemoval = true) public DataSize getCacheMaxSizeBuffering() { return this.cacheMaxSizeBuffering; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index 1a053cd529b0..e8742b414f6a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -623,11 +623,13 @@ public void setConnectionTimeout(Duration connectionTimeout) { this.connectionTimeout = connectionTimeout; } - @DeprecatedConfigurationProperty(reason = "The setting has been deprecated in Tomcat") + @Deprecated(since = "3.2.0", forRemoval = true) + @DeprecatedConfigurationProperty(reason = "The setting has been deprecated in Tomcat", since = "3.2.0") public boolean isRejectIllegalHeader() { return this.rejectIllegalHeader; } + @Deprecated(since = "3.2.0", forRemoval = true) public void setRejectIllegalHeader(boolean rejectIllegalHeader) { this.rejectIllegalHeader = rejectIllegalHeader; } @@ -1634,7 +1636,7 @@ public void setMaxCookies(Integer maxCookies) { this.maxCookies = maxCookies; } - @DeprecatedConfigurationProperty(replacement = "server.undertow.decode-slash") + @DeprecatedConfigurationProperty(replacement = "server.undertow.decode-slash", since = "3.0.3") @Deprecated(forRemoval = true, since = "3.0.3") public boolean isAllowEncodedSlash() { return this.allowEncodedSlash; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java index 050edc743384..1e8250deda87 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java @@ -84,6 +84,7 @@ public int getOrder() { } @Override + @SuppressWarnings("removal") public void customize(ConfigurableTomcatWebServerFactory factory) { ServerProperties.Tomcat properties = this.serverProperties.getTomcat(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java index 2ca9ae03743f..afdf35f65091 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcProperties.java @@ -126,7 +126,8 @@ public void setPublishRequestHandledEvents(boolean publishRequestHandledEvents) @Deprecated(since = "3.2.0", forRemoval = true) @DeprecatedConfigurationProperty( - reason = "DispatcherServlet property is deprecated for removal and should no longer need to be configured") + reason = "DispatcherServlet property is deprecated for removal and should no longer need to be configured", + since = "3.2.0") public boolean isThrowExceptionIfNoHandlerFound() { return this.throwExceptionIfNoHandlerFound; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java index 379aa6e32ce4..07b26273054f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java @@ -114,6 +114,7 @@ void testServerHeader() { } @Test + @SuppressWarnings("removal") void testTomcatBinding() { Map map = new HashMap<>(); map.put("server.tomcat.accesslog.conditionIf", "foo"); @@ -423,6 +424,7 @@ void tomcatInternalProxiesMatchesDefault() { } @Test + @SuppressWarnings("removal") void tomcatRejectIllegalHeaderMatchesProtocolDefault() throws Exception { assertThat(getDefaultProtocol()).hasFieldOrPropertyWithValue("rejectIllegalHeader", this.properties.getTomcat().isRejectIllegalHeader()); From 5d9d0f43b6a8619a2dd472e2d40e4a93bf964199 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 2 Aug 2023 10:08:46 +0200 Subject: [PATCH 0222/1215] Implement AssertJ assertions for SimpleAsyncTaskExecutor --- .../SimpleAsyncTaskExecutorAssert.java | 85 +++++++++++++++++++ .../testsupport/assertj/package-info.java | 20 +++++ .../SimpleAsyncTaskExecutorAssertTests.java | 47 ++++++++++ 3 files changed, 152 insertions(+) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/SimpleAsyncTaskExecutorAssert.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/assertj/SimpleAsyncTaskExecutorAssertTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/SimpleAsyncTaskExecutorAssert.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/SimpleAsyncTaskExecutorAssert.java new file mode 100644 index 000000000000..3e7fbde2ab93 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/SimpleAsyncTaskExecutorAssert.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-2023 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.testsupport.assertj; + +import java.lang.reflect.Field; + +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.Assert; + +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.util.ReflectionUtils; + +/** + * AssertJ {@link Assert} for {@link SimpleAsyncTaskExecutor}. + * + * @author Moritz Halbritter + * @since 3.2.0 + */ +public final class SimpleAsyncTaskExecutorAssert + extends AbstractAssert { + + private SimpleAsyncTaskExecutorAssert(SimpleAsyncTaskExecutor actual) { + super(actual, SimpleAsyncTaskExecutorAssert.class); + } + + /** + * Verifies that the actual executor uses platform threads. + * @return {@code this} assertion object + * @throws AssertionError if the actual executor doesn't use platform threads + */ + public SimpleAsyncTaskExecutorAssert usesPlatformThreads() { + isNotNull(); + if (producesVirtualThreads()) { + failWithMessage("Expected executor to use platform threads, but it uses virtual threads"); + } + return this; + } + + /** + * Verifies that the actual executor uses virtual threads. + * @return {@code this} assertion object + * @throws AssertionError if the actual executor doesn't use virtual threads + */ + public SimpleAsyncTaskExecutorAssert usesVirtualThreads() { + isNotNull(); + if (!producesVirtualThreads()) { + failWithMessage("Expected executor to use virtual threads, but it uses platform threads"); + } + return this; + } + + private boolean producesVirtualThreads() { + Field field = ReflectionUtils.findField(SimpleAsyncTaskExecutor.class, "virtualThreadDelegate"); + if (field == null) { + throw new IllegalStateException("Field SimpleAsyncTaskExecutor.virtualThreadDelegate not found"); + } + ReflectionUtils.makeAccessible(field); + Object virtualThreadDelegate = ReflectionUtils.getField(field, this.actual); + return virtualThreadDelegate != null; + } + + /** + * Creates a new assertion class with the given {@link SimpleAsyncTaskExecutor}. + * @param actual the {@link SimpleAsyncTaskExecutor} + * @return the assertion class + */ + public static SimpleAsyncTaskExecutorAssert assertThat(SimpleAsyncTaskExecutor actual) { + return new SimpleAsyncTaskExecutorAssert(actual); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/package-info.java new file mode 100644 index 000000000000..51fb5eaf4ef2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2021 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. + */ + +/** + * Custom AssertJ assertions. + */ +package org.springframework.boot.testsupport.assertj; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/assertj/SimpleAsyncTaskExecutorAssertTests.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/assertj/SimpleAsyncTaskExecutorAssertTests.java new file mode 100644 index 000000000000..a3775298f84b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/test/java/org/springframework/boot/testsupport/assertj/SimpleAsyncTaskExecutorAssertTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2023 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.testsupport.assertj; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; + +import org.springframework.core.task.SimpleAsyncTaskExecutor; + +/** + * Tests for {@link SimpleAsyncTaskExecutorAssert}. + * + * @author Moritz Halbritter + */ +class SimpleAsyncTaskExecutorAssertTests { + + @Test + void usesPlatformThreads() { + SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(); + executor.setVirtualThreads(false); + SimpleAsyncTaskExecutorAssert.assertThat(executor).usesPlatformThreads(); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void usesVirtualThreads() { + SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(); + executor.setVirtualThreads(true); + SimpleAsyncTaskExecutorAssert.assertThat(executor).usesVirtualThreads(); + } + +} From 7c5ec73724ead59534dd9f17ebd1ed70a22caac1 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 2 Aug 2023 10:11:12 +0200 Subject: [PATCH 0223/1215] Polish SimpleAsyncTaskExecutorBuilderTests --- .../boot/task/SimpleAsyncTaskExecutorBuilderTests.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilderTests.java index fe856cd9cbae..bd6f3607eb36 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilderTests.java @@ -16,7 +16,6 @@ package org.springframework.boot.task; -import java.lang.reflect.Field; import java.util.Collections; import java.util.Set; @@ -24,9 +23,9 @@ import org.junit.jupiter.api.condition.EnabledForJreRange; import org.junit.jupiter.api.condition.JRE; +import org.springframework.boot.testsupport.assertj.SimpleAsyncTaskExecutorAssert; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.core.task.TaskDecorator; -import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -55,11 +54,7 @@ void threadNamePrefixShouldApply() { @EnabledForJreRange(min = JRE.JAVA_21) void virtualThreadsShouldApply() { SimpleAsyncTaskExecutor executor = this.builder.virtualThreads(true).build(); - Field field = ReflectionUtils.findField(SimpleAsyncTaskExecutor.class, "virtualThreadDelegate"); - assertThat(field).as("executor.virtualThreadDelegate").isNotNull(); - field.setAccessible(true); - Object virtualThreadDelegate = ReflectionUtils.getField(field, executor); - assertThat(virtualThreadDelegate).as("executor.virtualThreadDelegate").isNotNull(); + SimpleAsyncTaskExecutorAssert.assertThat(executor).usesVirtualThreads(); } @Test From 3a9fadf30fe05eed7c9fc8dc2f657487fdee4851 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 2 Aug 2023 10:15:21 +0200 Subject: [PATCH 0224/1215] Enable virtual threads for Kafka listener Closes gh-36396 --- ...fkaListenerContainerFactoryConfigurer.java | 13 ++++++++ .../KafkaAnnotationDrivenConfiguration.java | 20 +++++++++++++ ...stenerContainerFactoryConfigurerTests.java | 10 +++++++ .../kafka/KafkaAutoConfigurationTests.java | 30 +++++++++++++++++++ 4 files changed, 73 insertions(+) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java index edd8f4a37c43..1a2102ad8a26 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java @@ -21,6 +21,7 @@ import org.springframework.boot.autoconfigure.kafka.KafkaProperties.Listener; import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; import org.springframework.kafka.core.ConsumerFactory; import org.springframework.kafka.core.KafkaTemplate; @@ -42,6 +43,7 @@ * @author Gary Russell * @author Eddú Meléndez * @author Thomas KÃ¥sene + * @author Moritz Halbritter * @since 1.5.0 */ public class ConcurrentKafkaListenerContainerFactoryConfigurer { @@ -70,6 +72,8 @@ public class ConcurrentKafkaListenerContainerFactoryConfigurer { private Function threadNameSupplier; + private SimpleAsyncTaskExecutor listenerTaskExecutor; + /** * Set the {@link KafkaProperties} to use. * @param properties the properties @@ -168,6 +172,14 @@ void setThreadNameSupplier(Function threadName this.threadNameSupplier = threadNameSupplier; } + /** + * Set the executor for threads that poll the consumer. + * @param listenerTaskExecutor task executor + */ + void setListenerTaskExecutor(SimpleAsyncTaskExecutor listenerTaskExecutor) { + this.listenerTaskExecutor = listenerTaskExecutor; + } + /** * Configure the specified Kafka listener container factory. The factory can be * further tuned and default settings can be overridden. @@ -226,6 +238,7 @@ private void configureContainer(ContainerProperties container) { map.from(properties::isImmediateStop).to(container::setStopImmediate); map.from(this.transactionManager).to(container::setTransactionManager); map.from(this.rebalanceListener).to(container::setConsumerRebalanceListener); + map.from(this.listenerTaskExecutor).to(container::setListenerTaskExecutor); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAnnotationDrivenConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAnnotationDrivenConfiguration.java index bedeec2844a1..f7a103305f05 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAnnotationDrivenConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAnnotationDrivenConfiguration.java @@ -21,8 +21,11 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading; +import org.springframework.boot.autoconfigure.thread.Threading; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.kafka.annotation.EnableKafka; import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; import org.springframework.kafka.config.ContainerCustomizer; @@ -49,6 +52,7 @@ * @author Gary Russell * @author Eddú Meléndez * @author Thomas KÃ¥sene + * @author Moritz Halbritter */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(EnableKafka.class) @@ -107,7 +111,23 @@ class KafkaAnnotationDrivenConfiguration { @Bean @ConditionalOnMissingBean + @ConditionalOnThreading(Threading.PLATFORM) ConcurrentKafkaListenerContainerFactoryConfigurer kafkaListenerContainerFactoryConfigurer() { + return configurer(); + } + + @Bean(name = "kafkaListenerContainerFactoryConfigurer") + @ConditionalOnMissingBean + @ConditionalOnThreading(Threading.VIRTUAL) + ConcurrentKafkaListenerContainerFactoryConfigurer kafkaListenerContainerFactoryConfigurerVirtualThreads() { + ConcurrentKafkaListenerContainerFactoryConfigurer configurer = configurer(); + SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor("kafka-"); + executor.setVirtualThreads(true); + configurer.setListenerTaskExecutor(executor); + return configurer; + } + + private ConcurrentKafkaListenerContainerFactoryConfigurer configurer() { ConcurrentKafkaListenerContainerFactoryConfigurer configurer = new ConcurrentKafkaListenerContainerFactoryConfigurer(); configurer.setKafkaProperties(this.properties); configurer.setBatchMessageConverter(this.batchMessageConverter); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurerTests.java index b82a6efcabda..779d9974d3f1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurerTests.java @@ -21,10 +21,12 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; import org.springframework.kafka.core.ConsumerFactory; import org.springframework.kafka.listener.MessageListenerContainer; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; @@ -70,4 +72,12 @@ void shouldApplyChangeConsumerThreadName() { then(this.factory).should().setChangeConsumerThreadName(true); } + @Test + void shouldApplyListenerTaskExecutor() { + SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(); + this.configurer.setListenerTaskExecutor(executor); + this.configurer.configure(this.factory, this.consumerFactory); + assertThat(this.factory.getContainerProperties().getListenerTaskExecutor()).isEqualTo(executor); + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java index f274a3b51779..516db1674508 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java @@ -40,6 +40,8 @@ import org.apache.kafka.streams.StreamsConfig; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -47,8 +49,11 @@ import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.boot.testsupport.assertj.SimpleAsyncTaskExecutorAssert; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.kafka.annotation.EnableKafkaStreams; import org.springframework.kafka.annotation.KafkaStreamsDefaultConfiguration; import org.springframework.kafka.config.AbstractKafkaListenerContainerFactory; @@ -570,6 +575,31 @@ void streamsApplicationIdIsNotMandatoryIfEnableKafkaStreamsIsNotSet() { }); } + @Test + void shouldUsePlatformThreadsByDefault() { + this.contextRunner.run((context) -> { + ConcurrentKafkaListenerContainerFactory factory = context + .getBean(ConcurrentKafkaListenerContainerFactory.class); + assertThat(factory).isNotNull(); + AsyncTaskExecutor listenerTaskExecutor = factory.getContainerProperties().getListenerTaskExecutor(); + assertThat(listenerTaskExecutor).isNull(); + }); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void shouldUseVirtualThreadsIfEnabled() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> { + ConcurrentKafkaListenerContainerFactory factory = context + .getBean(ConcurrentKafkaListenerContainerFactory.class); + assertThat(factory).isNotNull(); + AsyncTaskExecutor listenerTaskExecutor = factory.getContainerProperties().getListenerTaskExecutor(); + assertThat(listenerTaskExecutor).isInstanceOf(SimpleAsyncTaskExecutor.class); + SimpleAsyncTaskExecutorAssert.assertThat((SimpleAsyncTaskExecutor) listenerTaskExecutor) + .usesVirtualThreads(); + }); + } + @SuppressWarnings("unchecked") @Test void listenerProperties() { From 20d264150ba5fb78a9ecb17057922dad0fa18364 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 2 Aug 2023 11:41:30 +0200 Subject: [PATCH 0225/1215] Polish R2dbcObservationAutoConfiguration --- .../R2dbcObservationAutoConfiguration.java | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/r2dbc/R2dbcObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/r2dbc/R2dbcObservationAutoConfiguration.java index 6065ca676526..0634770640e6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/r2dbc/R2dbcObservationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/r2dbc/R2dbcObservationAutoConfiguration.java @@ -53,8 +53,9 @@ ConnectionFactoryDecorator connectionFactoryDecorator(R2dbcObservationProperties ObjectProvider queryObservationConvention, ObjectProvider queryParametersTagProvider) { return (connectionFactory) -> { + HostAndPort hostAndPort = extractHostAndPort(connectionFactory); ObservationProxyExecutionListener listener = new ObservationProxyExecutionListener(observationRegistry, - connectionFactory, extractUrl(connectionFactory)); + connectionFactory, hostAndPort.host(), hostAndPort.port()); listener.setIncludeParameterValues(properties.isIncludeParameterValues()); queryObservationConvention.ifAvailable(listener::setQueryObservationConvention); queryParametersTagProvider.ifAvailable(listener::setQueryParametersTagProvider); @@ -62,20 +63,25 @@ ConnectionFactoryDecorator connectionFactoryDecorator(R2dbcObservationProperties }; } - private String extractUrl(ConnectionFactory connectionFactory) { + private HostAndPort extractHostAndPort(ConnectionFactory connectionFactory) { OptionsCapableConnectionFactory optionsCapableConnectionFactory = OptionsCapableConnectionFactory .unwrapFrom(connectionFactory); if (optionsCapableConnectionFactory == null) { - return null; + return HostAndPort.empty(); } ConnectionFactoryOptions options = optionsCapableConnectionFactory.getOptions(); Object host = options.getValue(ConnectionFactoryOptions.HOST); Object port = options.getValue(ConnectionFactoryOptions.PORT); - if (host == null || !(port instanceof Integer portAsInt)) { - return null; + if ((!(host instanceof String hostAsString) || !(port instanceof Integer portAsInt))) { + return HostAndPort.empty(); + } + return new HostAndPort(hostAsString, portAsInt); + } + + private record HostAndPort(String host, Integer port) { + static HostAndPort empty() { + return new HostAndPort(null, null); } - // See https://github.com/r2dbc/r2dbc-proxy/issues/135 - return "r2dbc:dummy://%s:%d/".formatted(host, portAsInt); } } From e677eb7759cbe6418016e20970cf6e1e8d8e6ea0 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 2 Aug 2023 12:14:03 +0200 Subject: [PATCH 0226/1215] Polish Polish R2dbcObservationAutoConfiguration --- .../autoconfigure/r2dbc/R2dbcObservationAutoConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/r2dbc/R2dbcObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/r2dbc/R2dbcObservationAutoConfiguration.java index 0634770640e6..75f619c1bdfe 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/r2dbc/R2dbcObservationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/r2dbc/R2dbcObservationAutoConfiguration.java @@ -72,7 +72,7 @@ private HostAndPort extractHostAndPort(ConnectionFactory connectionFactory) { ConnectionFactoryOptions options = optionsCapableConnectionFactory.getOptions(); Object host = options.getValue(ConnectionFactoryOptions.HOST); Object port = options.getValue(ConnectionFactoryOptions.PORT); - if ((!(host instanceof String hostAsString) || !(port instanceof Integer portAsInt))) { + if (!(host instanceof String hostAsString) || !(port instanceof Integer portAsInt)) { return HostAndPort.empty(); } return new HostAndPort(hostAsString, portAsInt); From 9955ee7e8a46e0774b106f10bb0e17e0b332b20e Mon Sep 17 00:00:00 2001 From: Maurice Zeijen Date: Mon, 10 Jul 2023 10:59:06 +0200 Subject: [PATCH 0227/1215] Order auto-configured ProblemDetailsExceptionHandler beans Add `@Order(0)` to the WebMVC and Webflux `ProblemDetailsExceptionHandler` beans. This makes it easier to create custom `@ControllerAdvice` beans that must be ordered before or after the `ProblemDetailsExceptionHandler`. See gh-36288 --- .../reactive/WebFluxAutoConfiguration.java | 1 + .../web/servlet/WebMvcAutoConfiguration.java | 1 + .../WebFluxAutoConfigurationTests.java | 39 ++++++++++++++++++ .../servlet/WebMvcAutoConfigurationTests.java | 40 +++++++++++++++++++ 4 files changed, 81 insertions(+) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java index c3d77d94fad1..e9ebef362499 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java @@ -357,6 +357,7 @@ static class ProblemDetailsErrorHandlingConfiguration { @Bean @ConditionalOnMissingBean(ResponseEntityExceptionHandler.class) + @Order(0) ProblemDetailsExceptionHandler problemDetailsExceptionHandler() { return new ProblemDetailsExceptionHandler(); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java index f3e8097248fc..a6df6386d7e3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java @@ -662,6 +662,7 @@ static class ProblemDetailsErrorHandlingConfiguration { @Bean @ConditionalOnMissingBean(ResponseEntityExceptionHandler.class) + @Order(0) ProblemDetailsExceptionHandler problemDetailsExceptionHandler() { return new ProblemDetailsExceptionHandler(); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java index 65be8c0f79ea..d6e16cfe151d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java @@ -31,6 +31,7 @@ import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import java.util.stream.Collectors; import jakarta.validation.ValidatorFactory; import org.aspectj.lang.JoinPoint; @@ -80,6 +81,7 @@ import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.filter.reactive.HiddenHttpMethodFilter; +import org.springframework.web.method.ControllerAdviceBean; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.config.BlockingExecutionConfigurer; @@ -671,6 +673,24 @@ void problemDetailsBacksOffWhenExceptionHandler() { .hasSingleBean(CustomExceptionHandler.class)); } + @Test + void problemDetailsIsOrderedBetweenLowestAndHighestOrderedControllerHandlers() { + this.contextRunner.withPropertyValues("spring.webflux.problemdetails.enabled:true") + .withUserConfiguration(OrderedControllerAdviceBeansConfiguration.class) + .run((context) -> { + + List> controllerAdviceClasses = ControllerAdviceBean.findAnnotatedBeans(context) + .stream() + .map(ControllerAdviceBean::getBeanType) + .collect(Collectors.toList()); + + assertThat(controllerAdviceClasses).containsExactly( + OrderedControllerAdviceBeansConfiguration.HighestOrderedControllerAdvice.class, + ProblemDetailsExceptionHandler.class, + OrderedControllerAdviceBeansConfiguration.LowestOrderedControllerAdvice.class); + }); + } + @Test void asyncTaskExecutorWithApplicationTaskExecutor() { this.contextRunner.withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class)) @@ -1016,6 +1036,25 @@ static class CustomExceptionHandler extends ResponseEntityExceptionHandler { } + @Configuration(proxyBeanMethods = false) + @Import({ OrderedControllerAdviceBeansConfiguration.LowestOrderedControllerAdvice.class, + OrderedControllerAdviceBeansConfiguration.HighestOrderedControllerAdvice.class }) + static class OrderedControllerAdviceBeansConfiguration { + + @ControllerAdvice + @Order + static class LowestOrderedControllerAdvice { + + } + + @ControllerAdvice + @Order(Ordered.HIGHEST_PRECEDENCE) + static class HighestOrderedControllerAdvice { + + } + + } + @Aspect static class ExceptionHandlerInterceptor { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java index c2ebbb6d1588..5d89e0ec0a93 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java @@ -32,6 +32,7 @@ import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import java.util.stream.Collectors; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -51,6 +52,8 @@ import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfigurationTests.OrderedControllerAdviceBeansConfiguration.HighestOrderedControllerAdvice; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfigurationTests.OrderedControllerAdviceBeansConfiguration.LowestOrderedControllerAdvice; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; @@ -65,6 +68,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.core.convert.ConversionService; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; @@ -89,6 +94,7 @@ import org.springframework.web.filter.FormContentFilter; import org.springframework.web.filter.HiddenHttpMethodFilter; import org.springframework.web.filter.RequestContextFilter; +import org.springframework.web.method.ControllerAdviceBean; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.FlashMap; import org.springframework.web.servlet.FlashMapManager; @@ -972,6 +978,22 @@ void problemDetailsBacksOffWhenExceptionHandler() { .hasSingleBean(CustomExceptionHandler.class)); } + @Test + void problemDetailsIsOrderedBetweenLowestAndHighestOrderedControllerHandlers() { + this.contextRunner.withPropertyValues("spring.mvc.problemdetails.enabled:true") + .withUserConfiguration(OrderedControllerAdviceBeansConfiguration.class) + .run((context) -> { + + List> controllerAdviceClasses = ControllerAdviceBean.findAnnotatedBeans(context) + .stream() + .map(ControllerAdviceBean::getBeanType) + .collect(Collectors.toList()); + + assertThat(controllerAdviceClasses).containsExactly(HighestOrderedControllerAdvice.class, + ProblemDetailsExceptionHandler.class, LowestOrderedControllerAdvice.class); + }); + } + private void assertResourceHttpRequestHandler(AssertableWebApplicationContext context, Consumer handlerConsumer) { Map handlerMap = getHandlerMap(context.getBean("resourceHandlerMapping", HandlerMapping.class)); @@ -1496,6 +1518,24 @@ CustomExceptionHandler customExceptionHandler() { } + @Configuration(proxyBeanMethods = false) + @Import({ LowestOrderedControllerAdvice.class, HighestOrderedControllerAdvice.class }) + static class OrderedControllerAdviceBeansConfiguration { + + @ControllerAdvice + @Order + static class LowestOrderedControllerAdvice { + + } + + @ControllerAdvice + @Order(Ordered.HIGHEST_PRECEDENCE) + static class HighestOrderedControllerAdvice { + + } + + } + @ControllerAdvice static class CustomExceptionHandler extends ResponseEntityExceptionHandler { From a223834d572d4162b7efc0f27a98f3f125e02007 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 2 Aug 2023 15:28:01 +0200 Subject: [PATCH 0228/1215] Polish "Order auto-configured ProblemDetailsExceptionHandler beans" See gh-36288 --- .../WebFluxAutoConfigurationTests.java | 23 ++++++++----------- .../servlet/WebMvcAutoConfigurationTests.java | 19 ++++++--------- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java index d6e16cfe151d..b1753c4142d5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java @@ -31,13 +31,13 @@ import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; -import java.util.stream.Collectors; import jakarta.validation.ValidatorFactory; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.assertj.core.api.Assertions; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -50,6 +50,8 @@ import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration.WebFluxConfig; +import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfigurationTests.OrderedControllerAdviceBeansConfiguration.HighestOrderedControllerAdvice; +import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfigurationTests.OrderedControllerAdviceBeansConfiguration.LowestOrderedControllerAdvice; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; @@ -674,21 +676,14 @@ void problemDetailsBacksOffWhenExceptionHandler() { } @Test - void problemDetailsIsOrderedBetweenLowestAndHighestOrderedControllerHandlers() { + void problemDetailsExceptionHandlerIsOrderedAt0() { this.contextRunner.withPropertyValues("spring.webflux.problemdetails.enabled:true") .withUserConfiguration(OrderedControllerAdviceBeansConfiguration.class) - .run((context) -> { - - List> controllerAdviceClasses = ControllerAdviceBean.findAnnotatedBeans(context) - .stream() - .map(ControllerAdviceBean::getBeanType) - .collect(Collectors.toList()); - - assertThat(controllerAdviceClasses).containsExactly( - OrderedControllerAdviceBeansConfiguration.HighestOrderedControllerAdvice.class, - ProblemDetailsExceptionHandler.class, - OrderedControllerAdviceBeansConfiguration.LowestOrderedControllerAdvice.class); - }); + .run((context) -> assertThat( + ControllerAdviceBean.findAnnotatedBeans(context).stream().map(ControllerAdviceBean::getBeanType)) + .asInstanceOf(InstanceOfAssertFactories.list(Class.class)) + .containsExactly(HighestOrderedControllerAdvice.class, ProblemDetailsExceptionHandler.class, + LowestOrderedControllerAdvice.class)); } @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java index 5d89e0ec0a93..1ecc2395be40 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java @@ -32,7 +32,6 @@ import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; -import java.util.stream.Collectors; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -40,6 +39,7 @@ import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.springframework.aop.support.AopUtils; @@ -979,19 +979,14 @@ void problemDetailsBacksOffWhenExceptionHandler() { } @Test - void problemDetailsIsOrderedBetweenLowestAndHighestOrderedControllerHandlers() { + void problemDetailsExceptionHandlerIsOrderedAt0() { this.contextRunner.withPropertyValues("spring.mvc.problemdetails.enabled:true") .withUserConfiguration(OrderedControllerAdviceBeansConfiguration.class) - .run((context) -> { - - List> controllerAdviceClasses = ControllerAdviceBean.findAnnotatedBeans(context) - .stream() - .map(ControllerAdviceBean::getBeanType) - .collect(Collectors.toList()); - - assertThat(controllerAdviceClasses).containsExactly(HighestOrderedControllerAdvice.class, - ProblemDetailsExceptionHandler.class, LowestOrderedControllerAdvice.class); - }); + .run((context) -> assertThat( + ControllerAdviceBean.findAnnotatedBeans(context).stream().map(ControllerAdviceBean::getBeanType)) + .asInstanceOf(InstanceOfAssertFactories.list(Class.class)) + .containsExactly(HighestOrderedControllerAdvice.class, ProblemDetailsExceptionHandler.class, + OrderedControllerAdviceBeansConfiguration.LowestOrderedControllerAdvice.class)); } private void assertResourceHttpRequestHandler(AssertableWebApplicationContext context, From 1f0a3901b2c1177497c34275f0f698b11230e098 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Mon, 31 Jul 2023 13:48:08 +0200 Subject: [PATCH 0229/1215] Add support for using an AuthTokenManager with Neo4j Neo4j Java driver introduced support for an `AuthTokenManager` that can be used to define expiring tokens for authentication with a database. This commit adds an `ObjectProvider authTokenManagers` parameter to the corresponding auto configuration class. If the provider resolves to a unique object, that `AuthTokenManager` will have precedence over any static token. See gh-36650 --- .../neo4j/Neo4jAutoConfiguration.java | 12 ++- ...eo4jAutoConfigurationIntegrationTests.java | 83 +++++++++++++++---- 2 files changed, 74 insertions(+), 21 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java index a7580754f8ff..8eaf6569ee92 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java @@ -24,6 +24,7 @@ import java.util.concurrent.TimeUnit; import org.neo4j.driver.AuthToken; +import org.neo4j.driver.AuthTokenManager; import org.neo4j.driver.AuthTokens; import org.neo4j.driver.Config; import org.neo4j.driver.Config.TrustStrategy; @@ -70,11 +71,16 @@ PropertiesNeo4jConnectionDetails neo4jConnectionDetails(Neo4jProperties properti @Bean @ConditionalOnMissingBean public Driver neo4jDriver(Neo4jProperties properties, Environment environment, - Neo4jConnectionDetails connectionDetails, - ObjectProvider configBuilderCustomizers) { - AuthToken authToken = connectionDetails.getAuthToken(); + Neo4jConnectionDetails connectionDetails, ObjectProvider configBuilderCustomizers, + ObjectProvider authTokenManagers) { + Config config = mapDriverConfig(properties, connectionDetails, configBuilderCustomizers.orderedStream().toList()); + AuthTokenManager authTokenManager = authTokenManagers.getIfUnique(); + if (authTokenManager != null) { + return GraphDatabase.driver(connectionDetails.getUri(), authTokenManager, config); + } + AuthToken authToken = connectionDetails.getAuthToken(); return GraphDatabase.driver(connectionDetails.getUri(), authToken, config); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationIntegrationTests.java index c8febbbe6056..fb97387470a4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationIntegrationTests.java @@ -18,7 +18,11 @@ import java.time.Duration; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.neo4j.driver.AuthTokenManager; +import org.neo4j.driver.AuthTokenManagers; +import org.neo4j.driver.AuthTokens; import org.neo4j.driver.Driver; import org.neo4j.driver.Result; import org.neo4j.driver.Session; @@ -31,6 +35,7 @@ import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; @@ -43,7 +48,6 @@ * @author Michael J. Simons * @author Stephane Nicoll */ -@SpringBootTest @Testcontainers(disabledWithoutDocker = true) class Neo4jAutoConfigurationIntegrationTests { @@ -52,28 +56,71 @@ class Neo4jAutoConfigurationIntegrationTests { .withStartupAttempts(5) .withStartupTimeout(Duration.ofMinutes(10)); - @DynamicPropertySource - static void neo4jProperties(DynamicPropertyRegistry registry) { - registry.add("spring.neo4j.uri", neo4jServer::getBoltUrl); - registry.add("spring.neo4j.authentication.username", () -> "neo4j"); - registry.add("spring.neo4j.authentication.password", neo4jServer::getAdminPassword); - } + @SpringBootTest + @Nested + class DriverWithDefaultAuthToken { + + @DynamicPropertySource + static void neo4jProperties(DynamicPropertyRegistry registry) { + registry.add("spring.neo4j.uri", neo4jServer::getBoltUrl); + registry.add("spring.neo4j.authentication.username", () -> "neo4j"); + registry.add("spring.neo4j.authentication.password", neo4jServer::getAdminPassword); + } + + @Autowired + private Driver driver; - @Autowired - private Driver driver; + @Test + void driverCanHandleRequest() { + try (Session session = this.driver.session(); Transaction tx = session.beginTransaction()) { + Result statementResult = tx.run("MATCH (n:Thing) RETURN n LIMIT 1"); + assertThat(statementResult.hasNext()).isFalse(); + tx.commit(); + } + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(Neo4jAutoConfiguration.class) + static class TestConfiguration { - @Test - void driverCanHandleRequest() { - try (Session session = this.driver.session(); Transaction tx = session.beginTransaction()) { - Result statementResult = tx.run("MATCH (n:Thing) RETURN n LIMIT 1"); - assertThat(statementResult.hasNext()).isFalse(); - tx.commit(); } + } - @Configuration(proxyBeanMethods = false) - @ImportAutoConfiguration(Neo4jAutoConfiguration.class) - static class TestConfiguration { + @SpringBootTest + @Nested + class DriverWithDynamicAuthToken { + + @DynamicPropertySource + static void neo4jProperties(DynamicPropertyRegistry registry) { + registry.add("spring.neo4j.uri", neo4jServer::getBoltUrl); + registry.add("spring.neo4j.authentication.username", () -> "wrong"); + registry.add("spring.neo4j.authentication.password", () -> "alsowrong"); + } + + @Autowired + private Driver driver; + + @Test + void driverCanHandleRequest() { + try (Session session = this.driver.session(); Transaction tx = session.beginTransaction()) { + Result statementResult = tx.run("MATCH (n:Thing) RETURN n LIMIT 1"); + assertThat(statementResult.hasNext()).isFalse(); + tx.commit(); + } + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(Neo4jAutoConfiguration.class) + static class TestConfiguration { + + @Bean + AuthTokenManager authTokenManager() { + return AuthTokenManagers.expirationBased(() -> AuthTokens.basic("neo4j", neo4jServer.getAdminPassword()) + .expiringAt(System.currentTimeMillis() + 5_000)); + } + + } } From 77e382ec64c76deab65dd3ec93dd8bc0dc43e714 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 2 Aug 2023 13:20:54 +0100 Subject: [PATCH 0230/1215] Polish "Add support for using an AuthTokenManager with Neo4j" See gh-36650 --- .../neo4j/Neo4jAutoConfiguration.java | 21 +++++-- .../neo4j/Neo4jConnectionDetails.java | 11 ++++ ...eo4jAutoConfigurationIntegrationTests.java | 56 +++++++++++++++++++ .../neo4j/Neo4jAutoConfigurationTests.java | 36 ++++++++---- 4 files changed, 108 insertions(+), 16 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java index 8eaf6569ee92..9beb05e51818 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfiguration.java @@ -64,19 +64,20 @@ public class Neo4jAutoConfiguration { @Bean @ConditionalOnMissingBean(Neo4jConnectionDetails.class) - PropertiesNeo4jConnectionDetails neo4jConnectionDetails(Neo4jProperties properties) { - return new PropertiesNeo4jConnectionDetails(properties); + PropertiesNeo4jConnectionDetails neo4jConnectionDetails(Neo4jProperties properties, + ObjectProvider authTokenManager) { + return new PropertiesNeo4jConnectionDetails(properties, authTokenManager.getIfUnique()); } @Bean @ConditionalOnMissingBean public Driver neo4jDriver(Neo4jProperties properties, Environment environment, - Neo4jConnectionDetails connectionDetails, ObjectProvider configBuilderCustomizers, - ObjectProvider authTokenManagers) { + Neo4jConnectionDetails connectionDetails, + ObjectProvider configBuilderCustomizers) { Config config = mapDriverConfig(properties, connectionDetails, configBuilderCustomizers.orderedStream().toList()); - AuthTokenManager authTokenManager = authTokenManagers.getIfUnique(); + AuthTokenManager authTokenManager = connectionDetails.getAuthTokenManager(); if (authTokenManager != null) { return GraphDatabase.driver(connectionDetails.getUri(), authTokenManager, config); } @@ -187,8 +188,11 @@ static class PropertiesNeo4jConnectionDetails implements Neo4jConnectionDetails private final Neo4jProperties properties; - PropertiesNeo4jConnectionDetails(Neo4jProperties properties) { + private final AuthTokenManager authTokenManager; + + PropertiesNeo4jConnectionDetails(Neo4jProperties properties, AuthTokenManager authTokenManager) { this.properties = properties; + this.authTokenManager = authTokenManager; } @Override @@ -217,6 +221,11 @@ public AuthToken getAuthToken() { return AuthTokens.none(); } + @Override + public AuthTokenManager getAuthTokenManager() { + return this.authTokenManager; + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jConnectionDetails.java index 17a950ebd8bd..f10a122338b9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jConnectionDetails.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/neo4j/Neo4jConnectionDetails.java @@ -19,6 +19,7 @@ import java.net.URI; import org.neo4j.driver.AuthToken; +import org.neo4j.driver.AuthTokenManager; import org.neo4j.driver.AuthTokens; import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; @@ -49,4 +50,14 @@ default AuthToken getAuthToken() { return AuthTokens.none(); } + /** + * Returns the {@link AuthTokenManager} to use for authentication. Defaults to + * {@code null} in which case the {@link #getAuthToken() auth token} should be used. + * @return the auth token manager + * @since 3.2.0 + */ + default AuthTokenManager getAuthTokenManager() { + return null; + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationIntegrationTests.java index fb97387470a4..966fc33637bd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationIntegrationTests.java @@ -16,10 +16,12 @@ package org.springframework.boot.autoconfigure.neo4j; +import java.net.URI; import java.time.Duration; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.neo4j.driver.AuthToken; import org.neo4j.driver.AuthTokenManager; import org.neo4j.driver.AuthTokenManagers; import org.neo4j.driver.AuthTokens; @@ -124,4 +126,58 @@ AuthTokenManager authTokenManager() { } + @SpringBootTest + @Nested + class DriverWithCustomConnectionDetailsIgnoresAuthTokenManager { + + @DynamicPropertySource + static void neo4jProperties(DynamicPropertyRegistry registry) { + registry.add("spring.neo4j.uri", neo4jServer::getBoltUrl); + registry.add("spring.neo4j.authentication.username", () -> "wrong"); + registry.add("spring.neo4j.authentication.password", () -> "alsowrong"); + } + + @Autowired + private Driver driver; + + @Test + void driverCanHandleRequest() { + try (Session session = this.driver.session(); Transaction tx = session.beginTransaction()) { + Result statementResult = tx.run("MATCH (n:Thing) RETURN n LIMIT 1"); + assertThat(statementResult.hasNext()).isFalse(); + tx.commit(); + } + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(Neo4jAutoConfiguration.class) + static class TestConfiguration { + + @Bean + AuthTokenManager authTokenManager() { + return AuthTokenManagers.expirationBased(() -> AuthTokens.basic("wrongagain", "stillwrong") + .expiringAt(System.currentTimeMillis() + 5_000)); + } + + @Bean + Neo4jConnectionDetails connectionDetails() { + return new Neo4jConnectionDetails() { + + @Override + public URI getUri() { + return URI.create(neo4jServer.getBoltUrl()); + } + + @Override + public AuthToken getAuthToken() { + return AuthTokens.basic("neo4j", neo4jServer.getAdminPassword()); + } + + }; + } + + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java index df6821f2c5ef..68ca8ed3750f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java @@ -26,6 +26,7 @@ import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.neo4j.driver.AuthTokenManagers; import org.neo4j.driver.AuthTokens; import org.neo4j.driver.Config; import org.neo4j.driver.Config.ConfigBuilder; @@ -143,7 +144,7 @@ void maxTransactionRetryTime() { @Test void uriShouldDefaultToLocalhost() { - assertThat(new PropertiesNeo4jConnectionDetails(new Neo4jProperties()).getUri()) + assertThat(new PropertiesNeo4jConnectionDetails(new Neo4jProperties(), null).getUri()) .isEqualTo(URI.create("bolt://localhost:7687")); } @@ -152,12 +153,12 @@ void determineServerUriWithCustomUriShouldOverrideDefault() { URI customUri = URI.create("bolt://localhost:4242"); Neo4jProperties properties = new Neo4jProperties(); properties.setUri(customUri); - assertThat(new PropertiesNeo4jConnectionDetails(properties).getUri()).isEqualTo(customUri); + assertThat(new PropertiesNeo4jConnectionDetails(properties, null).getUri()).isEqualTo(customUri); } @Test void authenticationShouldDefaultToNone() { - assertThat(new PropertiesNeo4jConnectionDetails(new Neo4jProperties()).getAuthToken()) + assertThat(new PropertiesNeo4jConnectionDetails(new Neo4jProperties(), null).getAuthToken()) .isEqualTo(AuthTokens.none()); } @@ -166,8 +167,9 @@ void authenticationWithUsernameShouldEnableBasicAuth() { Neo4jProperties properties = new Neo4jProperties(); properties.getAuthentication().setUsername("Farin"); properties.getAuthentication().setPassword("Urlaub"); - assertThat(new PropertiesNeo4jConnectionDetails(properties).getAuthToken()) - .isEqualTo(AuthTokens.basic("Farin", "Urlaub")); + PropertiesNeo4jConnectionDetails connectionDetails = new PropertiesNeo4jConnectionDetails(properties, null); + assertThat(connectionDetails.getAuthToken()).isEqualTo(AuthTokens.basic("Farin", "Urlaub")); + assertThat(connectionDetails.getAuthTokenManager()).isNull(); } @Test @@ -177,8 +179,22 @@ void authenticationWithUsernameAndRealmShouldEnableBasicAuth() { authentication.setUsername("Farin"); authentication.setPassword("Urlaub"); authentication.setRealm("Test Realm"); - assertThat(new PropertiesNeo4jConnectionDetails(properties).getAuthToken()) - .isEqualTo(AuthTokens.basic("Farin", "Urlaub", "Test Realm")); + PropertiesNeo4jConnectionDetails connectionDetails = new PropertiesNeo4jConnectionDetails(properties, null); + assertThat(connectionDetails.getAuthToken()).isEqualTo(AuthTokens.basic("Farin", "Urlaub", "Test Realm")); + assertThat(connectionDetails.getAuthTokenManager()).isNull(); + } + + @Test + void authenticationWithAuthTokenManagerAndUsernameShouldProvideAuthTokenManger() { + Neo4jProperties properties = new Neo4jProperties(); + Authentication authentication = properties.getAuthentication(); + authentication.setUsername("Farin"); + authentication.setPassword("Urlaub"); + authentication.setRealm("Test Realm"); + assertThat(new PropertiesNeo4jConnectionDetails(properties, + AuthTokenManagers.expirationBased( + () -> AuthTokens.basic("username", "password").expiringAt(System.currentTimeMillis() + 5000))) + .getAuthTokenManager()).isNotNull(); } @Test @@ -186,7 +202,7 @@ void authenticationWithKerberosTicketShouldEnableKerberos() { Neo4jProperties properties = new Neo4jProperties(); Authentication authentication = properties.getAuthentication(); authentication.setKerberosTicket("AABBCCDDEE"); - assertThat(new PropertiesNeo4jConnectionDetails(properties).getAuthToken()) + assertThat(new PropertiesNeo4jConnectionDetails(properties, null).getAuthToken()) .isEqualTo(AuthTokens.kerberos("AABBCCDDEE")); } @@ -197,7 +213,7 @@ void authenticationWithBothUsernameAndKerberosShouldNotBeAllowed() { authentication.setUsername("Farin"); authentication.setKerberosTicket("AABBCCDDEE"); assertThatIllegalStateException() - .isThrownBy(() -> new PropertiesNeo4jConnectionDetails(properties).getAuthToken()) + .isThrownBy(() -> new PropertiesNeo4jConnectionDetails(properties, null).getAuthToken()) .withMessage("Cannot specify both username ('Farin') and kerberos ticket ('AABBCCDDEE')"); } @@ -313,7 +329,7 @@ void driverConfigShouldBeConfiguredToUseUseSpringJclLogging() { private Config mapDriverConfig(Neo4jProperties properties, ConfigBuilderCustomizer... customizers) { return new Neo4jAutoConfiguration().mapDriverConfig(properties, - new PropertiesNeo4jConnectionDetails(properties), Arrays.asList(customizers)); + new PropertiesNeo4jConnectionDetails(properties, null), Arrays.asList(customizers)); } } From b02c1877fc13b8bfb4385d62ad4ac0eb5405f27a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 2 Aug 2023 14:58:16 +0100 Subject: [PATCH 0231/1215] Fix configIsReadWithProvidedContext on Windows See gh-36445 --- .../configuration/DockerConfigurationMetadataTests.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadataTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadataTests.java index c619eac31614..b47bbaa3d80c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadataTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadataTests.java @@ -16,11 +16,13 @@ package org.springframework.boot.buildpack.platform.docker.configuration; +import java.io.File; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Paths; import java.util.LinkedHashMap; import java.util.Map; +import java.util.regex.Pattern; import org.junit.jupiter.api.Test; @@ -76,7 +78,8 @@ void configIsReadWithProvidedContext() throws Exception { DockerContext context = config.forContext("test-context"); assertThat(context.getDockerHost()).isEqualTo("unix:///home/user/.docker/docker.sock"); assertThat(context.isTlsVerify()).isTrue(); - assertThat(context.getTlsPath()).matches("^.*/with-default-context/contexts/tls/[a-zA-z0-9]*/docker$"); + assertThat(context.getTlsPath()).matches(String.join(Pattern.quote(File.separator), "^.*", + "with-default-context", "contexts", "tls", "[a-zA-z0-9]*", "docker$")); } @Test From 6506208d291e657e1ec5c4948d8a2054995b8b7f Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Wed, 2 Aug 2023 19:06:16 -0500 Subject: [PATCH 0232/1215] Upgrade default CNB builders to Paketo Jammy Closes gh-36689 --- ci/pipeline.yml | 3 +-- .../native-image/developing-your-first-application.adoc | 2 +- .../spring-boot-starter-parent/build.gradle | 2 +- .../boot/buildpack/platform/build/BuildRequest.java | 2 +- .../src/docs/asciidoc/packaging-oci-image.adoc | 2 +- .../spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc | 2 +- .../boot/gradle/plugin/NativeImagePluginAction.java | 2 +- .../gradle/plugin/NativeImagePluginActionIntegrationTests.java | 3 ++- .../boot/gradle/tasks/bundling/BootBuildImageTests.java | 3 ++- .../src/docs/asciidoc/packaging-oci-image.adoc | 2 +- .../test/java/org/springframework/boot/maven/ImageTests.java | 2 +- 11 files changed, 13 insertions(+), 12 deletions(-) diff --git a/ci/pipeline.yml b/ci/pipeline.yml index ad367a8ad435..4cdfaf312378 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -185,8 +185,7 @@ resources: type: registry-image icon: docker source: - repository: paketobuildpacks/builder - tag: base + repository: paketobuildpacks/builder-jammy-base - name: artifactory-repo type: artifactory-resource icon: package-variant diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/native-image/developing-your-first-application.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/native-image/developing-your-first-application.adoc index b4e70ac86ce5..a1e47a36b742 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/native-image/developing-your-first-application.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/native-image/developing-your-first-application.adoc @@ -32,7 +32,7 @@ This means you can just type a single command and quickly get a sensible image i The resulting image doesn't contain a JVM, instead the native image is compiled statically. This leads to smaller images. -NOTE: The builder used for the images is `paketobuildpacks/builder:tiny`. +NOTE: The builder used for the images is `paketobuildpacks/builder-jammy-tiny`. It has small footprint and reduced attack surface, but you can also use `paketobuildpacks/builder-jammy-base` or `paketobuildpacks/builder-jammy-full` to have more tools available in the image if required. diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle index 2b2028ea2cad..713f3ddd7826 100644 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle @@ -249,7 +249,7 @@ publishing.publications.withType(MavenPublication) { delegate.artifactId('spring-boot-maven-plugin') configuration { image { - delegate.builder("paketobuildpacks/builder:tiny"); + delegate.builder("paketobuildpacks/builder-jammy-tiny"); env { delegate.BP_NATIVE_IMAGE("true") } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java index 0bb75fe17e0e..5596b0badba3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java @@ -45,7 +45,7 @@ */ public class BuildRequest { - static final String DEFAULT_BUILDER_IMAGE_NAME = "paketobuildpacks/builder:base"; + static final String DEFAULT_BUILDER_IMAGE_NAME = "paketobuildpacks/builder-jammy-base"; private static final ImageReference DEFAULT_BUILDER = ImageReference.of(DEFAULT_BUILDER_IMAGE_NAME); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc index 824c79ee0049..b5df977ed59e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -115,7 +115,7 @@ The following table summarizes the available properties and their default values | `builder` | `--builder` | Name of the Builder image to use. -| `paketobuildpacks/builder:base` or `paketobuildpacks/builder:tiny` when {nbt-gradle-plugin}[GraalVM Native Image plugin] is applied. +| `paketobuildpacks/builder-jammy-base` or `paketobuildpacks/builder-jammy-tiny` when {nbt-gradle-plugin}[GraalVM Native Image plugin] is applied. | `runImage` | `--runImage` diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc index 660404b1a426..468002d26c98 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc @@ -81,6 +81,6 @@ When the {nbt-gradle-plugin}[GraalVM Native Image plugin] is applied to a projec . Configures the GraalVM extension to disable Toolchain detection. . Configures each GraalVM native binary to require GraalVM 22.3 or later. . Configures the `bootJar` task to include the reachability metadata produced by the `collectReachabilityMetadata` task in its jar. -. Configures the `bootBuildImage` task to use `paketobuildpacks/builder:tiny` as its builder and to set `BP_NATIVE_IMAGE` to `true` in its environment. +. Configures the `bootBuildImage` task to use `paketobuildpacks/builder-jammy-tiny` as its builder and to set `BP_NATIVE_IMAGE` to `true` in its environment. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java index 3cc2bf7b2b2d..fa0b967e6132 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java @@ -115,7 +115,7 @@ private void configureBootBuildImageToProduceANativeImage(Project project) { project.getTasks() .named(SpringBootPlugin.BOOT_BUILD_IMAGE_TASK_NAME, BootBuildImage.class) .configure((bootBuildImage) -> { - bootBuildImage.getBuilder().convention("paketobuildpacks/builder:tiny"); + bootBuildImage.getBuilder().convention("paketobuildpacks/builder-jammy-tiny"); bootBuildImage.getEnvironment().put("BP_NATIVE_IMAGE", "true"); }); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java index 08c094e2efb6..4d15614a7057 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java @@ -93,7 +93,8 @@ void bootBuildImageIsConfiguredToBuildANativeImage() { writeDummySpringApplicationAotProcessorMainClass(); BuildResult result = this.gradleBuild.expectDeprecationWarningsWithAtLeastVersion("8.2-rc-1") .build("bootBuildImageConfiguration"); - assertThat(result.getOutput()).contains("paketobuildpacks/builder:tiny").contains("BP_NATIVE_IMAGE = true"); + assertThat(result.getOutput()).contains("paketobuildpacks/builder-jammy-tiny") + .contains("BP_NATIVE_IMAGE = true"); } @TestTemplate diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java index fdb69d2a532d..33dac8a784e4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java @@ -171,7 +171,8 @@ void whenUsingDefaultConfigurationThenRequestHasPublishDisabled() { @Test void whenNoBuilderIsConfiguredThenRequestHasDefaultBuilder() { - assertThat(this.buildImage.createRequest().getBuilder().getName()).isEqualTo("paketobuildpacks/builder"); + assertThat(this.buildImage.createRequest().getBuilder().getName()) + .isEqualTo("paketobuildpacks/builder-jammy-base"); } @Test diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc index 09e85abb8209..7f76bfde2397 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -131,7 +131,7 @@ The following table summarizes the available parameters and their default values | `builder` + (`spring-boot.build-image.builder`) | Name of the Builder image to use. -| `paketobuildpacks/builder:base` +| `paketobuildpacks/builder-jammy-base` | `runImage` + (`spring-boot.build-image.runImage`) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java index 8f3558701c46..86625106bdbe 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java @@ -67,7 +67,7 @@ void getBuildRequestWhenNameIsSetUsesName() { void getBuildRequestWhenNoCustomizationsUsesDefaults() { BuildRequest request = new Image().getBuildRequest(createArtifact(), mockApplicationContent()); assertThat(request.getName()).hasToString("docker.io/library/my-app:0.0.1-SNAPSHOT"); - assertThat(request.getBuilder().toString()).contains("paketobuildpacks/builder"); + assertThat(request.getBuilder().toString()).contains("paketobuildpacks/builder-jammy-base"); assertThat(request.getRunImage()).isNull(); assertThat(request.getEnv()).isEmpty(); assertThat(request.isCleanCache()).isFalse(); From 497bbf9c2d0fafa49e5e9e2688fcc8000d9f5675 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 2 Aug 2023 14:30:38 +0200 Subject: [PATCH 0233/1215] Revise synchronized blocks - Replace synchronized with Lock when guarding long-running operations - Remove unnecessary synchronization in FileSystemWatcher - Replace HashMap with ConcurrentHashMap in Restarter - Remove unnecessary locking on AtomicBoolean in SpringApplicationBuilder - Remove unnecessary locking in SimpleFormatter Closes gh-36670 --- .../TemplateAvailabilityProviders.java | 18 +++- .../devtools/filewatch/FileSystemWatcher.java | 6 +- .../devtools/livereload/LiveReloadServer.java | 59 ++++++++++--- .../boot/devtools/restart/Restarter.java | 17 ++-- .../devtools/tunnel/client/TunnelClient.java | 30 +++++-- .../payload/HttpTunnelPayloadForwarder.java | 12 ++- .../tunnel/server/HttpTunnelServer.java | 86 +++++++++++++++---- .../web/servlet/WebDriverScope.java | 27 +++++- .../web/SpringBootMockServletContext.java | 21 +++-- .../loader/data/RandomAccessDataFile.java | 24 ++++-- .../builder/SpringApplicationBuilder.java | 8 +- .../boot/env/ConfigTreePropertySource.java | 13 ++- .../boot/logging/java/SimpleFormatter.java | 10 +-- ...cationContextServerWebExchangeMatcher.java | 12 ++- .../ApplicationContextRequestMatcher.java | 10 ++- .../boot/system/ApplicationTemp.java | 15 +++- .../web/embedded/jetty/JettyWebServer.java | 64 ++++++++------ .../web/embedded/tomcat/TomcatWebServer.java | 78 ++++++++++------- .../embedded/undertow/UndertowWebServer.java | 16 +++- 19 files changed, 378 insertions(+), 148 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/TemplateAvailabilityProviders.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/TemplateAvailabilityProviders.java index b787dc413a09..de476fe9c2d7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/TemplateAvailabilityProviders.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/TemplateAvailabilityProviders.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -22,6 +22,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.springframework.context.ApplicationContext; import org.springframework.core.env.Environment; @@ -52,7 +54,13 @@ public class TemplateAvailabilityProviders { private final Map resolved = new ConcurrentHashMap<>(CACHE_LIMIT); /** - * Map from view name resolve template view, synchronized when accessed. + * Guards access to {@link #cache}. + */ + private final Lock cacheLock = new ReentrantLock(); + + /** + * Map from view name resolve template view, protected by {@link #cacheLock} when + * accessed. */ private final Map cache = new LinkedHashMap<>(CACHE_LIMIT, 0.75f, true) { @@ -133,12 +141,16 @@ public TemplateAvailabilityProvider getProvider(String view, Environment environ } TemplateAvailabilityProvider provider = this.resolved.get(view); if (provider == null) { - synchronized (this.cache) { + this.cacheLock.lock(); + try { provider = findProvider(view, environment, classLoader, resourceLoader); provider = (provider != null) ? provider : NONE; this.resolved.put(view, provider); this.cache.put(view, provider); } + finally { + this.cacheLock.unlock(); + } } return (provider != NONE) ? provider : null; } diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcher.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcher.java index 99ef112ea3b5..35a950c6e3b1 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcher.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -158,9 +158,7 @@ public void setTriggerFilter(FileFilter triggerFilter) { } private void checkNotStarted() { - synchronized (this.monitor) { - Assert.state(this.watchThread == null, "FileSystemWatcher already started"); - } + Assert.state(this.watchThread == null, "FileSystemWatcher already started"); } /** diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/LiveReloadServer.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/LiveReloadServer.java index 773fcabc87fe..a22216be0948 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/LiveReloadServer.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/LiveReloadServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. @@ -29,6 +29,8 @@ import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -57,7 +59,12 @@ public class LiveReloadServer { private final List connections = new ArrayList<>(); - private final Object monitor = new Object(); + /** + * Guards access to {@link #connections}. + */ + private final Lock connectionsLock = new ReentrantLock(); + + private final Lock lock = new ReentrantLock(); private final int port; @@ -108,7 +115,8 @@ public LiveReloadServer(int port, ThreadFactory threadFactory) { * @throws IOException in case of I/O errors */ public int start() throws IOException { - synchronized (this.monitor) { + this.lock.lock(); + try { Assert.state(!isStarted(), "Server already started"); logger.debug(LogMessage.format("Starting live reload server on port %s", this.port)); this.serverSocket = new ServerSocket(this.port); @@ -119,6 +127,9 @@ public int start() throws IOException { this.listenThread.start(); return localPort; } + finally { + this.lock.unlock(); + } } /** @@ -126,9 +137,13 @@ public int start() throws IOException { * @return {@code true} if the server is running */ public boolean isStarted() { - synchronized (this.monitor) { + this.lock.lock(); + try { return this.listenThread != null; } + finally { + this.lock.unlock(); + } } /** @@ -163,7 +178,8 @@ private void acceptConnections() { * @throws IOException in case of I/O errors */ public void stop() throws IOException { - synchronized (this.monitor) { + this.lock.lock(); + try { if (this.listenThread != null) { closeAllConnections(); try { @@ -184,22 +200,31 @@ public void stop() throws IOException { this.serverSocket = null; } } + finally { + this.lock.unlock(); + } } private void closeAllConnections() throws IOException { - synchronized (this.connections) { + this.connectionsLock.lock(); + try { for (Connection connection : this.connections) { connection.close(); } } + finally { + this.connectionsLock.unlock(); + } } /** * Trigger livereload of all connected clients. */ public void triggerReload() { - synchronized (this.monitor) { - synchronized (this.connections) { + this.lock.lock(); + try { + this.connectionsLock.lock(); + try { for (Connection connection : this.connections) { try { connection.triggerReload(); @@ -209,19 +234,33 @@ public void triggerReload() { } } } + finally { + this.connectionsLock.unlock(); + } + } + finally { + this.lock.unlock(); } } private void addConnection(Connection connection) { - synchronized (this.connections) { + this.connectionsLock.lock(); + try { this.connections.add(connection); } + finally { + this.connectionsLock.unlock(); + } } private void removeConnection(Connection connection) { - synchronized (this.connections) { + this.connectionsLock.lock(); + try { this.connections.remove(connection); } + finally { + this.connectionsLock.unlock(); + } } /** diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java index bdc69f02d84d..410b269d1d66 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -22,7 +22,6 @@ import java.net.URL; import java.util.Arrays; import java.util.Collection; -import java.util.HashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; @@ -30,6 +29,7 @@ import java.util.Set; import java.util.concurrent.BlockingDeque; import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ThreadFactory; @@ -92,7 +92,7 @@ public class Restarter { private final ClassLoaderFiles classLoaderFiles = new ClassLoaderFiles(); - private final Map attributes = new HashMap<>(); + private final Map attributes = new ConcurrentHashMap<>(); private final BlockingDeque leakSafeThreads = new LinkedBlockingDeque<>(); @@ -440,18 +440,11 @@ private LeakSafeThread getLeakSafeThread() { } public Object getOrAddAttribute(String name, final ObjectFactory objectFactory) { - synchronized (this.attributes) { - if (!this.attributes.containsKey(name)) { - this.attributes.put(name, objectFactory.getObject()); - } - return this.attributes.get(name); - } + return this.attributes.computeIfAbsent(name, (ignore) -> objectFactory.getObject()); } public Object removeAttribute(String name) { - synchronized (this.attributes) { - return this.attributes.remove(name); - } + return this.attributes.remove(name); } /** diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/TunnelClient.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/TunnelClient.java index e4a439492d3c..3ab00f4ed3a5 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/TunnelClient.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/TunnelClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. @@ -25,6 +25,8 @@ import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.channels.WritableByteChannel; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -49,7 +51,7 @@ public class TunnelClient implements SmartInitializingSingleton { private final TunnelClientListeners listeners = new TunnelClientListeners(); - private final Object monitor = new Object(); + private final Lock lock = new ReentrantLock(); private final int listenPort; @@ -66,7 +68,8 @@ public TunnelClient(int listenPort, TunnelConnection tunnelConnection) { @Override public void afterSingletonsInstantiated() { - synchronized (this.monitor) { + this.lock.lock(); + try { if (this.serverThread == null) { try { start(); @@ -76,6 +79,9 @@ public void afterSingletonsInstantiated() { } } } + finally { + this.lock.unlock(); + } } /** @@ -84,7 +90,8 @@ public void afterSingletonsInstantiated() { * @throws IOException in case of I/O errors */ public int start() throws IOException { - synchronized (this.monitor) { + this.lock.lock(); + try { Assert.state(this.serverThread == null, "Server already started"); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.socket().bind(new InetSocketAddress(this.listenPort)); @@ -94,6 +101,9 @@ public int start() throws IOException { this.serverThread.start(); return port; } + finally { + this.lock.unlock(); + } } /** @@ -101,7 +111,8 @@ public int start() throws IOException { * @throws IOException in case of I/O errors */ public void stop() throws IOException { - synchronized (this.monitor) { + this.lock.lock(); + try { if (this.serverThread != null) { this.serverThread.close(); try { @@ -113,12 +124,19 @@ public void stop() throws IOException { this.serverThread = null; } } + finally { + this.lock.unlock(); + } } protected final ServerThread getServerThread() { - synchronized (this.monitor) { + this.lock.lock(); + try { return this.serverThread; } + finally { + this.lock.unlock(); + } } public void addListener(TunnelClientListener listener) { diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/payload/HttpTunnelPayloadForwarder.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/payload/HttpTunnelPayloadForwarder.java index 0bf486fcaa2d..f1a0e40e71d1 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/payload/HttpTunnelPayloadForwarder.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/payload/HttpTunnelPayloadForwarder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. @@ -20,6 +20,8 @@ import java.nio.channels.WritableByteChannel; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.springframework.util.Assert; @@ -36,7 +38,7 @@ public class HttpTunnelPayloadForwarder { private final Map queue = new HashMap<>(); - private final Object monitor = new Object(); + private final Lock lock = new ReentrantLock(); private final WritableByteChannel targetChannel; @@ -52,7 +54,8 @@ public HttpTunnelPayloadForwarder(WritableByteChannel targetChannel) { } public void forward(HttpTunnelPayload payload) throws IOException { - synchronized (this.monitor) { + this.lock.lock(); + try { long seq = payload.getSequence(); if (this.lastRequestSeq != seq - 1) { Assert.state(this.queue.size() < MAXIMUM_QUEUE_SIZE, "Too many messages queued"); @@ -67,6 +70,9 @@ public void forward(HttpTunnelPayload payload) throws IOException { forward(queuedItem); } } + finally { + this.lock.unlock(); + } } } diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/HttpTunnelServer.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/HttpTunnelServer.java index b03e724dfbd8..4d16788699c1 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/HttpTunnelServer.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/HttpTunnelServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -25,6 +25,9 @@ import java.util.Iterator; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -122,7 +125,12 @@ public class HttpTunnelServer { private long disconnectTimeout = DEFAULT_DISCONNECT_TIMEOUT; - private volatile ServerThread serverThread; + /** + * Guards access to {@link #serverThread}. + */ + private final Lock serverThreadLock = new ReentrantLock(); + + private ServerThread serverThread; /** * Creates a new {@link HttpTunnelServer} instance. @@ -164,7 +172,8 @@ protected void handle(HttpConnection httpConnection) throws IOException { * @throws IOException in case of I/O errors */ protected ServerThread getServerThread() throws IOException { - synchronized (this) { + this.serverThreadLock.lock(); + try { if (this.serverThread == null) { ByteChannel channel = this.serverConnection.open(this.longPollTimeout); this.serverThread = new ServerThread(channel); @@ -172,15 +181,22 @@ protected ServerThread getServerThread() throws IOException { } return this.serverThread; } + finally { + this.serverThreadLock.unlock(); + } } /** * Called when the server thread exits. */ void clearServerThread() { - synchronized (this) { + this.serverThreadLock.lock(); + try { this.serverThread = null; } + finally { + this.serverThreadLock.unlock(); + } } /** @@ -210,6 +226,13 @@ protected class ServerThread extends Thread { private final Deque httpConnections; + /** + * Guards access to {@link #httpConnections}. + */ + private final Lock httpConnectionsLock = new ReentrantLock(); + + private final Condition httpConnectionsCondition = this.httpConnectionsLock.newCondition(); + private final HttpTunnelPayloadForwarder payloadForwarder; private boolean closed; @@ -247,7 +270,8 @@ private void readAndForwardTargetServerData() throws IOException { while (this.targetServer.isOpen()) { closeStaleHttpConnections(); ByteBuffer data = HttpTunnelPayload.getPayloadData(this.targetServer); - synchronized (this.httpConnections) { + this.httpConnectionsLock.lock(); + try { if (data != null) { HttpTunnelPayload payload = new HttpTunnelPayload(this.responseSeq.incrementAndGet(), data); payload.logIncoming(); @@ -255,15 +279,20 @@ private void readAndForwardTargetServerData() throws IOException { connection.respond(payload); } } + finally { + this.httpConnectionsLock.unlock(); + } } } private HttpConnection getOrWaitForHttpConnection() { - synchronized (this.httpConnections) { + this.httpConnectionsLock.lock(); + try { HttpConnection httpConnection = this.httpConnections.pollFirst(); while (httpConnection == null) { try { - this.httpConnections.wait(HttpTunnelServer.this.longPollTimeout); + this.httpConnectionsCondition.await(HttpTunnelServer.this.longPollTimeout, + TimeUnit.MILLISECONDS); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); @@ -273,10 +302,14 @@ private HttpConnection getOrWaitForHttpConnection() { } return httpConnection; } + finally { + this.httpConnectionsLock.unlock(); + } } private void closeStaleHttpConnections() throws IOException { - synchronized (this.httpConnections) { + this.httpConnectionsLock.lock(); + try { checkNotDisconnected(); Iterator iterator = this.httpConnections.iterator(); while (iterator.hasNext()) { @@ -287,6 +320,9 @@ private void closeStaleHttpConnections() throws IOException { } } } + finally { + this.httpConnectionsLock.unlock(); + } } private void checkNotDisconnected() { @@ -298,7 +334,8 @@ private void checkNotDisconnected() { } private void closeHttpConnections() { - synchronized (this.httpConnections) { + this.httpConnectionsLock.lock(); + try { while (!this.httpConnections.isEmpty()) { try { this.httpConnections.removeFirst().respond(HttpStatus.GONE); @@ -308,6 +345,9 @@ private void closeHttpConnections() { } } } + finally { + this.httpConnectionsLock.unlock(); + } } private void closeTargetServer() { @@ -328,13 +368,17 @@ public void handleIncomingHttp(HttpConnection httpConnection) throws IOException if (this.closed) { httpConnection.respond(HttpStatus.GONE); } - synchronized (this.httpConnections) { + this.httpConnectionsLock.lock(); + try { while (this.httpConnections.size() > 1) { this.httpConnections.removeFirst().respond(HttpStatus.TOO_MANY_REQUESTS); } this.lastHttpRequestTime = System.currentTimeMillis(); this.httpConnections.addLast(httpConnection); - this.httpConnections.notify(); + this.httpConnectionsCondition.signal(); + } + finally { + this.httpConnectionsLock.unlock(); } forwardToTargetServer(httpConnection); } @@ -368,6 +412,10 @@ protected static class HttpConnection { private volatile boolean complete = false; + private final Lock lock = new ReentrantLock(); + + private final Condition lockCondition = this.lock.newCondition(); + public HttpConnection(ServerHttpRequest request, ServerHttpResponse response) { this.createTime = System.currentTimeMillis(); this.request = request; @@ -426,8 +474,12 @@ public void waitForResponse() { if (this.async == null) { while (!this.complete) { try { - synchronized (this) { - wait(1000); + this.lock.lock(); + try { + this.lockCondition.await(1, TimeUnit.SECONDS); + } + finally { + this.lock.unlock(); } } catch (InterruptedException ex) { @@ -476,9 +528,13 @@ protected void complete() { this.async.complete(); } else { - synchronized (this) { + this.lock.lock(); + try { this.complete = true; - notifyAll(); + this.lockCondition.signalAll(); + } + finally { + this.lock.unlock(); } } } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverScope.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverScope.java index 5b91ef38e44f..f9338002e169 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverScope.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverScope.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -18,6 +18,8 @@ import java.util.HashMap; import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.openqa.selenium.WebDriver; @@ -52,11 +54,17 @@ public class WebDriverScope implements Scope { private static final String[] BEAN_CLASSES = { WEB_DRIVER_CLASS, "org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDriverBuilder" }; + /** + * Guards access to {@link #instances}. + */ + private final Lock instancesLock = new ReentrantLock(); + private final Map instances = new HashMap<>(); @Override public Object get(String name, ObjectFactory objectFactory) { - synchronized (this.instances) { + this.instancesLock.lock(); + try { Object instance = this.instances.get(name); if (instance == null) { instance = objectFactory.getObject(); @@ -64,13 +72,20 @@ public Object get(String name, ObjectFactory objectFactory) { } return instance; } + finally { + this.instancesLock.unlock(); + } } @Override public Object remove(String name) { - synchronized (this.instances) { + this.instancesLock.lock(); + try { return this.instances.remove(name); } + finally { + this.instancesLock.unlock(); + } } @Override @@ -93,7 +108,8 @@ public String getConversationId() { */ boolean reset() { boolean reset = false; - synchronized (this.instances) { + this.instancesLock.lock(); + try { for (Object instance : this.instances.values()) { reset = true; if (instance instanceof WebDriver webDriver) { @@ -102,6 +118,9 @@ boolean reset() { } this.instances.clear(); } + finally { + this.instancesLock.unlock(); + } return reset; } diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/web/SpringBootMockServletContext.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/web/SpringBootMockServletContext.java index ec810bac3fd5..30d800a85dfc 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/web/SpringBootMockServletContext.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/web/SpringBootMockServletContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. @@ -21,6 +21,8 @@ import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Files; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.springframework.core.io.FileSystemResourceLoader; import org.springframework.core.io.Resource; @@ -44,6 +46,11 @@ public class SpringBootMockServletContext extends MockServletContext { private File emptyRootDirectory; + /** + * Guards access to {@link #emptyRootDirectory}. + */ + private final Lock emptyRootDirectoryLock = new ReentrantLock(); + public SpringBootMockServletContext(String resourceBasePath) { this(resourceBasePath, new FileSystemResourceLoader()); } @@ -91,19 +98,21 @@ public URL getResource(String path) throws MalformedURLException { if (resource == null && "/".equals(path)) { // Liquibase assumes that "/" always exists, if we don't have a directory // use a temporary location. + this.emptyRootDirectoryLock.lock(); try { if (this.emptyRootDirectory == null) { - synchronized (this) { - File tempDirectory = Files.createTempDirectory("spr-servlet").toFile(); - tempDirectory.deleteOnExit(); - this.emptyRootDirectory = tempDirectory; - } + File tempDirectory = Files.createTempDirectory("spr-servlet").toFile(); + tempDirectory.deleteOnExit(); + this.emptyRootDirectory = tempDirectory; } return this.emptyRootDirectory.toURI().toURL(); } catch (IOException ex) { // Ignore } + finally { + this.emptyRootDirectoryLock.unlock(); + } } return resource; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java index 06d9abcda51a..91474edd42b7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -22,6 +22,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; /** * {@link RandomAccessData} implementation backed by a {@link RandomAccessFile}. @@ -209,7 +211,7 @@ private long moveOn(int amount) { private static final class FileAccess { - private final Object monitor = new Object(); + private final Lock lock = new ReentrantLock(); private final File file; @@ -221,11 +223,15 @@ private FileAccess(File file) { } private int read(byte[] bytes, long position, int offset, int length) throws IOException { - synchronized (this.monitor) { + this.lock.lock(); + try { openIfNecessary(); this.randomAccessFile.seek(position); return this.randomAccessFile.read(bytes, offset, length); } + finally { + this.lock.unlock(); + } } private void openIfNecessary() { @@ -241,20 +247,28 @@ private void openIfNecessary() { } private void close() throws IOException { - synchronized (this.monitor) { + this.lock.lock(); + try { if (this.randomAccessFile != null) { this.randomAccessFile.close(); this.randomAccessFile = null; } } + finally { + this.lock.unlock(); + } } private int readByte(long position) throws IOException { - synchronized (this.monitor) { + this.lock.lock(); + try { openIfNecessary(); this.randomAccessFile.seek(position); return this.randomAccessFile.read(); } + finally { + this.lock.unlock(); + } } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java index 712caa5e6f15..b479568fcb53 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java @@ -76,7 +76,7 @@ public class SpringApplicationBuilder { private final SpringApplication application; - private ConfigurableApplicationContext context; + private volatile ConfigurableApplicationContext context; private SpringApplicationBuilder parent; @@ -145,10 +145,8 @@ public ConfigurableApplicationContext run(String... args) { } configureAsChildIfNecessary(args); if (this.running.compareAndSet(false, true)) { - synchronized (this.running) { - // If not already running copy the sources over and then run. - this.context = build().run(args); - } + // If not already running copy the sources over and then run. + this.context = build().run(args); } return this.context; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/ConfigTreePropertySource.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/ConfigTreePropertySource.java index 866404b46674..1760b240f1ae 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/ConfigTreePropertySource.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/ConfigTreePropertySource.java @@ -29,6 +29,8 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Stream; import org.springframework.boot.convert.ApplicationConversionService; @@ -258,6 +260,11 @@ private static final class PropertyFileContent implements Value, OriginProvider private final Path path; + /** + * Guards access to {@link #resource}. + */ + private final Lock resourceLock = new ReentrantLock(); + private final Resource resource; private final Origin origin; @@ -341,11 +348,15 @@ private byte[] getBytes() { } if (this.content == null) { assertStillExists(); - synchronized (this.resource) { + this.resourceLock.lock(); + try { if (this.content == null) { this.content = FileCopyUtils.copyToByteArray(this.resource.getInputStream()); } } + finally { + this.resourceLock.unlock(); + } } return this.content; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/java/SimpleFormatter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/java/SimpleFormatter.java index 1701a6a03a23..fc9dbfbcaf29 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/java/SimpleFormatter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/java/SimpleFormatter.java @@ -38,17 +38,15 @@ public class SimpleFormatter extends Formatter { private final String pid = getOrUseDefault(LoggingSystemProperty.PID.getEnvironmentVariableName(), "????"); - private final Date date = new Date(); - @Override - public synchronized String format(LogRecord record) { - this.date.setTime(record.getMillis()); + public String format(LogRecord record) { + Date date = new Date(record.getMillis()); String source = record.getLoggerName(); String message = formatMessage(record); String throwable = getThrowable(record); String thread = getThreadName(); - return String.format(this.format, this.date, source, record.getLoggerName(), - record.getLevel().getLocalizedName(), message, throwable, thread, this.pid); + return String.format(this.format, date, source, record.getLoggerName(), record.getLevel().getLocalizedName(), + message, throwable, thread, this.pid); } private String getThrowable(LogRecord record) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/security/reactive/ApplicationContextServerWebExchangeMatcher.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/security/reactive/ApplicationContextServerWebExchangeMatcher.java index 59264b353ff2..e3732e219d59 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/security/reactive/ApplicationContextServerWebExchangeMatcher.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/security/reactive/ApplicationContextServerWebExchangeMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. @@ -16,6 +16,8 @@ package org.springframework.boot.security.reactive; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; import reactor.core.publisher.Mono; @@ -44,7 +46,7 @@ public abstract class ApplicationContextServerWebExchangeMatcher implements S private volatile Supplier context; - private final Object contextLock = new Object(); + private final Lock contextLock = new ReentrantLock(); public ApplicationContextServerWebExchangeMatcher(Class contextClass) { Assert.notNull(contextClass, "Context class must not be null"); @@ -81,13 +83,17 @@ protected boolean ignoreApplicationContext(ApplicationContext applicationContext protected Supplier getContext(ServerWebExchange exchange) { if (this.context == null) { - synchronized (this.contextLock) { + this.contextLock.lock(); + try { if (this.context == null) { Supplier createdContext = createContext(exchange); initialized(createdContext); this.context = createdContext; } } + finally { + this.contextLock.unlock(); + } } return this.context; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/security/servlet/ApplicationContextRequestMatcher.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/security/servlet/ApplicationContextRequestMatcher.java index 0f2d9e3858ec..3c6542af3fca 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/security/servlet/ApplicationContextRequestMatcher.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/security/servlet/ApplicationContextRequestMatcher.java @@ -16,6 +16,8 @@ package org.springframework.boot.security.servlet; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; import jakarta.servlet.http.HttpServletRequest; @@ -45,7 +47,7 @@ public abstract class ApplicationContextRequestMatcher implements RequestMatc private volatile boolean initialized; - private final Object initializeLock = new Object(); + private final Lock initializeLock = new ReentrantLock(); public ApplicationContextRequestMatcher(Class contextClass) { Assert.notNull(contextClass, "Context class must not be null"); @@ -61,12 +63,16 @@ public final boolean matches(HttpServletRequest request) { } Supplier context = () -> getContext(webApplicationContext); if (!this.initialized) { - synchronized (this.initializeLock) { + this.initializeLock.lock(); + try { if (!this.initialized) { initialized(context); this.initialized = true; } } + finally { + this.initializeLock.unlock(); + } } return matches(request, context); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/system/ApplicationTemp.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/system/ApplicationTemp.java index 9c413a0ab702..852db487cd2b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/system/ApplicationTemp.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/system/ApplicationTemp.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -28,6 +28,8 @@ import java.security.MessageDigest; import java.util.EnumSet; import java.util.HexFormat; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -51,6 +53,11 @@ public class ApplicationTemp { private volatile Path path; + /** + * Guards access to {@link #path}. + */ + private final Lock pathLock = new ReentrantLock(); + /** * Create a new {@link ApplicationTemp} instance. */ @@ -90,10 +97,14 @@ public File getDir(String subDir) { private Path getPath() { if (this.path == null) { - synchronized (this) { + this.pathLock.lock(); + try { String hash = HexFormat.of().withUpperCase().formatHex(generateHash(this.sourceClass)); this.path = createDirectory(getTempDirectory().resolve(hash)); } + finally { + this.pathLock.unlock(); + } } return this.path; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java index f85210163754..a09164b54c41 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java @@ -20,6 +20,8 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import org.apache.commons.logging.Log; @@ -57,7 +59,7 @@ public class JettyWebServer implements WebServer { private static final Log logger = LogFactory.getLog(JettyWebServer.class); - private final Object monitor = new Object(); + private final Lock lock = new ReentrantLock(); private final Server server; @@ -113,21 +115,23 @@ private StatisticsHandler findStatisticsHandler(Handler handler) { } private void initialize() { - synchronized (this.monitor) { - try { - // Cache the connectors and then remove them to prevent requests being - // handled before the application context is ready. - this.connectors = this.server.getConnectors(); - JettyWebServer.this.server.setConnectors(null); - // Start the server so that the ServletContext is available - this.server.start(); - this.server.setStopAtShutdown(false); - } - catch (Throwable ex) { - // Ensure process isn't left running - stopSilently(); - throw new WebServerException("Unable to start embedded Jetty web server", ex); - } + this.lock.lock(); + try { + // Cache the connectors and then remove them to prevent requests being + // handled before the application context is ready. + this.connectors = this.server.getConnectors(); + JettyWebServer.this.server.setConnectors(null); + // Start the server so that the ServletContext is available + this.server.start(); + this.server.setStopAtShutdown(false); + } + catch (Throwable ex) { + // Ensure process isn't left running + stopSilently(); + throw new WebServerException("Unable to start embedded Jetty web server", ex); + } + finally { + this.lock.unlock(); } } @@ -142,7 +146,8 @@ private void stopSilently() { @Override public void start() throws WebServerException { - synchronized (this.monitor) { + this.lock.lock(); + try { if (this.started) { return; } @@ -179,6 +184,9 @@ public void start() throws WebServerException { throw new WebServerException("Unable to start embedded Jetty server", ex); } } + finally { + this.lock.unlock(); + } } String getStartedLogMessage() { @@ -241,7 +249,8 @@ else if (handler instanceof HandlerCollection handlerCollection) { @Override public void stop() { - synchronized (this.monitor) { + this.lock.lock(); + try { this.started = false; if (this.gracefulShutdown != null) { this.gracefulShutdown.abort(); @@ -258,17 +267,22 @@ public void stop() { throw new WebServerException("Unable to stop embedded Jetty server", ex); } } + finally { + this.lock.unlock(); + } } @Override public void destroy() { - synchronized (this.monitor) { - try { - this.server.stop(); - } - catch (Exception ex) { - throw new WebServerException("Unable to destroy embedded Jetty server", ex); - } + this.lock.lock(); + try { + this.server.stop(); + } + catch (Exception ex) { + throw new WebServerException("Unable to destroy embedded Jetty server", ex); + } + finally { + this.lock.unlock(); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java index 9290aeada33d..8eb9b17dd8e6 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java @@ -20,6 +20,8 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import javax.naming.NamingException; @@ -60,7 +62,7 @@ public class TomcatWebServer implements WebServer { private static final AtomicInteger containerCounter = new AtomicInteger(-1); - private final Object monitor = new Object(); + private final Lock lock = new ReentrantLock(); private final Map serviceConnectors = new HashMap<>(); @@ -106,41 +108,43 @@ public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) { private void initialize() throws WebServerException { logger.info("Tomcat initialized with " + getPortsDescription(false)); - synchronized (this.monitor) { - try { - addInstanceIdToEngineName(); - - Context context = findContext(); - context.addLifecycleListener((event) -> { - if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) { - // Remove service connectors so that protocol binding doesn't - // happen when the service is started. - removeServiceConnectors(); - } - }); - - // Start the server to trigger initialization listeners - this.tomcat.start(); + this.lock.lock(); + try { + addInstanceIdToEngineName(); + + Context context = findContext(); + context.addLifecycleListener((event) -> { + if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) { + // Remove service connectors so that protocol binding doesn't + // happen when the service is started. + removeServiceConnectors(); + } + }); - // We can re-throw failure exception directly in the main thread - rethrowDeferredStartupExceptions(); + // Start the server to trigger initialization listeners + this.tomcat.start(); - try { - ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader()); - } - catch (NamingException ex) { - // Naming is not enabled. Continue - } + // We can re-throw failure exception directly in the main thread + rethrowDeferredStartupExceptions(); - // Unlike Jetty, all Tomcat threads are daemon threads. We create a - // blocking non-daemon to stop immediate shutdown - startDaemonAwaitThread(); + try { + ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader()); } - catch (Exception ex) { - stopSilently(); - destroySilently(); - throw new WebServerException("Unable to start embedded Tomcat", ex); + catch (NamingException ex) { + // Naming is not enabled. Continue } + + // Unlike Jetty, all Tomcat threads are daemon threads. We create a + // blocking non-daemon to stop immediate shutdown + startDaemonAwaitThread(); + } + catch (Exception ex) { + stopSilently(); + destroySilently(); + throw new WebServerException("Unable to start embedded Tomcat", ex); + } + finally { + this.lock.unlock(); } } @@ -205,7 +209,8 @@ public void run() { @Override public void start() throws WebServerException { - synchronized (this.monitor) { + this.lock.lock(); + try { if (this.started) { return; } @@ -233,6 +238,9 @@ public void start() throws WebServerException { ContextBindings.unbindClassLoader(context, context.getNamingToken(), getClass().getClassLoader()); } } + finally { + this.lock.unlock(); + } } String getStartedLogMessage() { @@ -324,7 +332,8 @@ Map getServiceConnectors() { @Override public void stop() throws WebServerException { - synchronized (this.monitor) { + this.lock.lock(); + try { boolean wasStarted = this.started; try { this.started = false; @@ -342,6 +351,9 @@ public void stop() throws WebServerException { } } } + finally { + this.lock.unlock(); + } } @Override diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java index bce6152a7e68..0ac3b76ff2fa 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java @@ -25,6 +25,8 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import io.undertow.Undertow; import io.undertow.server.HttpHandler; @@ -63,7 +65,7 @@ public class UndertowWebServer implements WebServer { private final AtomicReference gracefulShutdownCallback = new AtomicReference<>(); - private final Object monitor = new Object(); + private final Lock lock = new ReentrantLock(); private final Undertow.Builder builder; @@ -104,7 +106,8 @@ public UndertowWebServer(Undertow.Builder builder, Iterable @Override public void start() throws WebServerException { - synchronized (this.monitor) { + this.lock.lock(); + try { if (this.started) { return; } @@ -136,6 +139,9 @@ public void start() throws WebServerException { } } } + finally { + this.lock.unlock(); + } } private void destroySilently() { @@ -268,7 +274,8 @@ private UndertowWebServer.Port getPortFromListener(Object listener) { @Override public void stop() throws WebServerException { - synchronized (this.monitor) { + this.lock.lock(); + try { if (!this.started) { return; } @@ -286,6 +293,9 @@ public void stop() throws WebServerException { throw new WebServerException("Unable to stop Undertow", ex); } } + finally { + this.lock.unlock(); + } } @Override From 3515196c2b5bab254dc5ae08a51e613e370b437c Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 2 Aug 2023 14:51:46 +0200 Subject: [PATCH 0234/1215] Add missing synchronization and remove unnecessary volatile --- .../org/springframework/boot/logging/DeferredLog.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/DeferredLog.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/DeferredLog.java index cf5f7b4713f1..05026a362d62 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/DeferredLog.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/DeferredLog.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -35,7 +35,7 @@ */ public class DeferredLog implements Log { - private volatile Log destination; + private Log destination; private final Supplier destinationSupplier; @@ -175,7 +175,9 @@ private void log(LogLevel level, Object message, Throwable t) { } void switchOver() { - this.destination = this.destinationSupplier.get(); + synchronized (this.lines) { + this.destination = this.destinationSupplier.get(); + } } /** From 9f5749832b3607fe42c37402871da378389bfe3d Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 2 Aug 2023 16:50:12 +0200 Subject: [PATCH 0235/1215] Polish JettyWebServer --- .../boot/web/embedded/jetty/JettyWebServer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java index a09164b54c41..c7661d2e1845 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java @@ -290,15 +290,15 @@ public void destroy() { public int getPort() { Connector[] connectors = this.server.getConnectors(); for (Connector connector : connectors) { - Integer localPort = getLocalPort(connector); - if (localPort != null && localPort > 0) { + int localPort = getLocalPort(connector); + if (localPort > 0) { return localPort; } } return -1; } - private Integer getLocalPort(Connector connector) { + private int getLocalPort(Connector connector) { if (connector instanceof NetworkConnector networkConnector) { return networkConnector.getLocalPort(); } From 6fc585c5d2e19ad1a07b65bbb6decddae78b854e Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 3 Aug 2023 10:18:41 +0200 Subject: [PATCH 0236/1215] Use virtual threads in JmsHealthIndicator if enabled Closes gh-36694 --- ...JmsHealthContributorAutoConfiguration.java | 19 +++++++++- ...althContributorAutoConfigurationTests.java | 18 +++++++++ .../boot/actuate/jms/JmsHealthIndicator.java | 38 +++++++++++++++++-- .../actuate/jms/JmsHealthIndicatorTests.java | 18 ++++++--- 4 files changed, 82 insertions(+), 11 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfiguration.java index aa7d72ab27aa..64ba0e4568e7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfiguration.java @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.jms; +import java.time.Duration; import java.util.Map; import jakarta.jms.ConnectionFactory; @@ -31,12 +32,16 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration; import org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration; +import org.springframework.boot.autoconfigure.thread.Threading; import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; +import org.springframework.core.task.SimpleAsyncTaskExecutor; /** * {@link EnableAutoConfiguration Auto-configuration} for {@link JmsHealthIndicator}. * * @author Stephane Nicoll + * @author Moritz Halbritter * @since 2.0.0 */ @AutoConfiguration(after = { ActiveMQAutoConfiguration.class, ArtemisAutoConfiguration.class }) @@ -46,8 +51,10 @@ public class JmsHealthContributorAutoConfiguration extends CompositeHealthContributorConfiguration { - public JmsHealthContributorAutoConfiguration() { - super(JmsHealthIndicator::new); + private static final Duration TIMEOUT = Duration.ofSeconds(5); + + public JmsHealthContributorAutoConfiguration(Environment environment) { + super((connectionFactory) -> new JmsHealthIndicator(connectionFactory, getTaskExecutor(environment), TIMEOUT)); } @Bean @@ -56,4 +63,12 @@ public HealthContributor jmsHealthContributor(Map con return createContributor(connectionFactories); } + private static SimpleAsyncTaskExecutor getTaskExecutor(Environment environment) { + SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("jms-health-indicator"); + if (Threading.VIRTUAL.isActive(environment)) { + taskExecutor.setVirtualThreads(true); + } + return taskExecutor; + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfigurationTests.java index c8fa3c696200..e630b95c78eb 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfigurationTests.java @@ -17,6 +17,8 @@ package org.springframework.boot.actuate.autoconfigure.jms; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration; import org.springframework.boot.actuate.jms.JmsHealthIndicator; @@ -24,6 +26,9 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.assertj.SimpleAsyncTaskExecutorAssert; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -31,6 +36,7 @@ * Tests for {@link JmsHealthContributorAutoConfiguration}. * * @author Phillip Webb + * @author Moritz Halbritter */ class JmsHealthContributorAutoConfigurationTests { @@ -43,6 +49,18 @@ void runShouldCreateIndicator() { this.contextRunner.run((context) -> assertThat(context).hasSingleBean(JmsHealthIndicator.class)); } + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void shouldUseVirtualThreadsIfEnabled() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> { + JmsHealthIndicator jmsHealthIndicator = context.getBean(JmsHealthIndicator.class); + assertThat(jmsHealthIndicator).isNotNull(); + Object taskExecutor = ReflectionTestUtils.getField(jmsHealthIndicator, "taskExecutor"); + assertThat(taskExecutor).isInstanceOf(SimpleAsyncTaskExecutor.class); + SimpleAsyncTaskExecutorAssert.assertThat((SimpleAsyncTaskExecutor) taskExecutor).usesVirtualThreads(); + }); + } + @Test void runWhenDisabledShouldNotCreateIndicator() { this.contextRunner.withPropertyValues("management.health.jms.enabled:false") diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jms/JmsHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jms/JmsHealthIndicator.java index 3c8f961d5372..fd1322f59cce 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jms/JmsHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jms/JmsHealthIndicator.java @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.jms; +import java.time.Duration; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -28,11 +29,15 @@ import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.core.log.LogMessage; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.core.task.SimpleAsyncTaskExecutor; /** * {@link HealthIndicator} for a JMS {@link ConnectionFactory}. * * @author Stephane Nicoll + * @author Moritz Halbritter * @since 2.0.0 */ public class JmsHealthIndicator extends AbstractHealthIndicator { @@ -41,9 +46,33 @@ public class JmsHealthIndicator extends AbstractHealthIndicator { private final ConnectionFactory connectionFactory; + private final AsyncTaskExecutor taskExecutor; + + private final Duration timeout; + + /** + * Creates a new {@link JmsHealthIndicator}, using a {@link SimpleAsyncTaskExecutor} + * and a timeout of 5 seconds. + * @param connectionFactory the connection factory + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of + * {@link #JmsHealthIndicator(ConnectionFactory, AsyncTaskExecutor, Duration)} + */ + @Deprecated(since = "3.2.0", forRemoval = true) public JmsHealthIndicator(ConnectionFactory connectionFactory) { + this(connectionFactory, new SimpleAsyncTaskExecutor("jms-health-indicator"), Duration.ofSeconds(5)); + } + + /** + * Creates a new {@link JmsHealthIndicator}. + * @param connectionFactory the connection factory + * @param taskExecutor the task executor used to run timeout checks + * @param timeout the connection timeout + */ + public JmsHealthIndicator(ConnectionFactory connectionFactory, AsyncTaskExecutor taskExecutor, Duration timeout) { super("JMS health check failed"); this.connectionFactory = connectionFactory; + this.taskExecutor = taskExecutor; + this.timeout = timeout; } @Override @@ -65,18 +94,19 @@ private final class MonitoredConnection { } void start() throws JMSException { - new Thread(() -> { + JmsHealthIndicator.this.taskExecutor.execute(() -> { try { - if (!this.latch.await(5, TimeUnit.SECONDS)) { + if (!this.latch.await(JmsHealthIndicator.this.timeout.toMillis(), TimeUnit.MILLISECONDS)) { JmsHealthIndicator.this.logger - .warn("Connection failed to start within 5 seconds and will be closed."); + .warn(LogMessage.format("Connection failed to start within %s and will be closed.", + JmsHealthIndicator.this.timeout)); closeConnection(); } } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } - }, "jms-health-indicator").start(); + }); this.connection.start(); this.latch.countDown(); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/jms/JmsHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/jms/JmsHealthIndicatorTests.java index 2e005d591d2e..f52082859df5 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/jms/JmsHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/jms/JmsHealthIndicatorTests.java @@ -16,6 +16,8 @@ package org.springframework.boot.actuate.jms; +import java.time.Duration; + import jakarta.jms.Connection; import jakarta.jms.ConnectionFactory; import jakarta.jms.ConnectionMetaData; @@ -26,6 +28,8 @@ import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Status; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.core.task.SimpleAsyncTaskExecutor; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -41,6 +45,10 @@ */ class JmsHealthIndicatorTests { + private static final Duration TIMEOUT = Duration.ofMillis(100); + + private final AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); + @Test void jmsBrokerIsUp() throws JMSException { ConnectionMetaData connectionMetaData = mock(ConnectionMetaData.class); @@ -49,7 +57,7 @@ void jmsBrokerIsUp() throws JMSException { given(connection.getMetaData()).willReturn(connectionMetaData); ConnectionFactory connectionFactory = mock(ConnectionFactory.class); given(connectionFactory.createConnection()).willReturn(connection); - JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory); + JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory, this.taskExecutor, TIMEOUT); Health health = indicator.health(); assertThat(health.getStatus()).isEqualTo(Status.UP); assertThat(health.getDetails()).containsEntry("provider", "JMS test provider"); @@ -60,7 +68,7 @@ void jmsBrokerIsUp() throws JMSException { void jmsBrokerIsDown() throws JMSException { ConnectionFactory connectionFactory = mock(ConnectionFactory.class); given(connectionFactory.createConnection()).willThrow(new JMSException("test", "123")); - JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory); + JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory, this.taskExecutor, TIMEOUT); Health health = indicator.health(); assertThat(health.getStatus()).isEqualTo(Status.DOWN); assertThat(health.getDetails()).doesNotContainKey("provider"); @@ -74,7 +82,7 @@ void jmsBrokerCouldNotRetrieveProviderMetadata() throws JMSException { given(connection.getMetaData()).willReturn(connectionMetaData); ConnectionFactory connectionFactory = mock(ConnectionFactory.class); given(connectionFactory.createConnection()).willReturn(connection); - JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory); + JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory, this.taskExecutor, TIMEOUT); Health health = indicator.health(); assertThat(health.getStatus()).isEqualTo(Status.DOWN); assertThat(health.getDetails()).doesNotContainKey("provider"); @@ -90,7 +98,7 @@ void jmsBrokerUsesFailover() throws JMSException { given(connection.getMetaData()).willReturn(connectionMetaData); willThrow(new JMSException("Could not start", "123")).given(connection).start(); given(connectionFactory.createConnection()).willReturn(connection); - JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory); + JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory, this.taskExecutor, TIMEOUT); Health health = indicator.health(); assertThat(health.getStatus()).isEqualTo(Status.DOWN); assertThat(health.getDetails()).doesNotContainKey("provider"); @@ -109,7 +117,7 @@ void whenConnectionStartIsUnresponsiveStatusIsDown() throws JMSException { }).given(connection).close(); ConnectionFactory connectionFactory = mock(ConnectionFactory.class); given(connectionFactory.createConnection()).willReturn(connection); - JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory); + JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory, this.taskExecutor, TIMEOUT); Health health = indicator.health(); assertThat(health.getStatus()).isEqualTo(Status.DOWN); assertThat((String) health.getDetails().get("error")).contains("Connection closed"); From 25eb3c8c18345e4b651a4fd1be44e72afa62ce90 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 3 Aug 2023 10:52:50 +0200 Subject: [PATCH 0237/1215] Polish --- .../jms/JmsHealthContributorAutoConfiguration.java | 2 +- .../springframework/boot/actuate/jms/JmsHealthIndicator.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfiguration.java index 64ba0e4568e7..70bface1ce01 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfiguration.java @@ -64,7 +64,7 @@ public HealthContributor jmsHealthContributor(Map con } private static SimpleAsyncTaskExecutor getTaskExecutor(Environment environment) { - SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("jms-health-indicator"); + SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("jms-health-indicator-"); if (Threading.VIRTUAL.isActive(environment)) { taskExecutor.setVirtualThreads(true); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jms/JmsHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jms/JmsHealthIndicator.java index fd1322f59cce..e31578520732 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jms/JmsHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jms/JmsHealthIndicator.java @@ -59,7 +59,7 @@ public class JmsHealthIndicator extends AbstractHealthIndicator { */ @Deprecated(since = "3.2.0", forRemoval = true) public JmsHealthIndicator(ConnectionFactory connectionFactory) { - this(connectionFactory, new SimpleAsyncTaskExecutor("jms-health-indicator"), Duration.ofSeconds(5)); + this(connectionFactory, new SimpleAsyncTaskExecutor("jms-health-indicator-"), Duration.ofSeconds(5)); } /** From 4bbc3363213fffa6a366ecbb603ddd38f330f6a6 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 3 Aug 2023 11:00:59 +0200 Subject: [PATCH 0238/1215] Use virtual threads in BackgroundPreinitializer if enabled Closes gh-36695 --- .../BackgroundPreinitializer.java | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/BackgroundPreinitializer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/BackgroundPreinitializer.java index 7fc2382303ef..bacc9df0dda0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/BackgroundPreinitializer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/BackgroundPreinitializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -23,6 +23,7 @@ import jakarta.validation.Configuration; import jakarta.validation.Validation; +import org.springframework.boot.autoconfigure.thread.Threading; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.boot.context.event.ApplicationFailedEvent; import org.springframework.boot.context.event.ApplicationReadyEvent; @@ -31,6 +32,9 @@ import org.springframework.context.ApplicationListener; import org.springframework.core.NativeDetector; import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; @@ -47,6 +51,7 @@ * @author Andy Wilkinson * @author Artsiom Yudovin * @author Sebastien Deleuze + * @author Moritz Halbritter * @since 1.3.0 */ public class BackgroundPreinitializer implements ApplicationListener, Ordered { @@ -79,7 +84,8 @@ public void onApplicationEvent(SpringApplicationEvent event) { } if (event instanceof ApplicationEnvironmentPreparedEvent && preinitializationStarted.compareAndSet(false, true)) { - performPreinitialization(); + ConfigurableEnvironment environment = ((ApplicationEnvironmentPreparedEvent) event).getEnvironment(); + performPreinitialization(environment); } if ((event instanceof ApplicationReadyEvent || event instanceof ApplicationFailedEvent) && preinitializationStarted.get()) { @@ -92,9 +98,9 @@ public void onApplicationEvent(SpringApplicationEvent event) { } } - private void performPreinitialization() { + private void performPreinitialization(Environment environment) { try { - Thread thread = new Thread(new Runnable() { + Runnable runnable = new Runnable() { @Override public void run() { @@ -102,8 +108,7 @@ public void run() { runSafely(new ValidationInitializer()); if (!runSafely(new MessageConverterInitializer())) { // If the MessageConverterInitializer fails to run, we still might - // be able to - // initialize Jackson + // be able to initialize Jackson runSafely(new JacksonInitializer()); } runSafely(new CharsetInitializer()); @@ -120,8 +125,12 @@ boolean runSafely(Runnable runnable) { } } - }, "background-preinit"); - thread.start(); + }; + SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("background-preinit-"); + if (Threading.VIRTUAL.isActive(environment)) { + taskExecutor.setVirtualThreads(true); + } + taskExecutor.execute(runnable); } catch (Exception ex) { // This will fail on GAE where creating threads is prohibited. We can safely From 677db72210f0a6faca2260242c60af6b889fd385 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Thu, 3 Aug 2023 16:58:27 +0900 Subject: [PATCH 0239/1215] Add Javadoc since to a new constructor for PemSslStoreBundle See gh-36693 --- .../java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java index 251ff0b52799..83f6146fce8e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java @@ -70,6 +70,7 @@ public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails * @param trustStoreDetails the trust store details * @param keyAlias the key alias to use or {@code null} to use a default alias * @param keyPassword the password to use for the key + * @since 3.2.0 */ public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String keyAlias, String keyPassword) { From 36e31c06120367fc324482421decfc47284691ce Mon Sep 17 00:00:00 2001 From: Marc Becker Date: Wed, 2 Aug 2023 13:30:12 +0000 Subject: [PATCH 0240/1215] Add resource hints for MessageSource This only registers the default locations, not the one users can provide via 'spring.messages.basename'. This is similar to the approach taken for schema.sql and data.sql in class SqlInitializationScriptsRuntimeHints. See gh-36682 --- .../context/MessageSourceAutoConfiguration.java | 15 +++++++++++++++ .../MessageSourceAutoConfigurationTests.java | 13 +++++++++++++ 2 files changed, 28 insertions(+) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfiguration.java index ed306db9882e..0ec92b6568b9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfiguration.java @@ -18,6 +18,8 @@ import java.time.Duration; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -26,6 +28,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.SearchStrategy; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration.MessageSourceRuntimeHints; import org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration.ResourceBundleCondition; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -33,6 +36,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ConditionContext; import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.core.Ordered; @@ -48,6 +52,7 @@ * @author Dave Syer * @author Phillip Webb * @author Eddú Meléndez + * @author Marc Becker * @since 1.5.0 */ @AutoConfiguration @@ -55,6 +60,7 @@ @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @Conditional(ResourceBundleCondition.class) @EnableConfigurationProperties +@ImportRuntimeHints(MessageSourceRuntimeHints.class) public class MessageSourceAutoConfiguration { private static final Resource[] NO_RESOURCES = {}; @@ -125,4 +131,13 @@ private Resource[] getResources(ClassLoader classLoader, String name) { } + static class MessageSourceRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + hints.resources().registerPattern("messages.properties").registerPattern("messages_*.properties"); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfigurationTests.java index 313624d8779f..e30983164136 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/MessageSourceAutoConfigurationTests.java @@ -21,7 +21,10 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration.MessageSourceRuntimeHints; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ContextConsumer; @@ -40,6 +43,7 @@ * @author Eddú Meléndez * @author Stephane Nicoll * @author Kedar Joshi + * @author Marc Becker */ class MessageSourceAutoConfigurationTests { @@ -180,6 +184,15 @@ void messageSourceWithNonStandardBeanNameIsIgnored() { .run((context) -> assertThat(context.getMessage("foo", null, Locale.US)).isEqualTo("bar")); } + @Test + void shouldRegisterDefaultHints() { + RuntimeHints hints = new RuntimeHints(); + new MessageSourceRuntimeHints().registerHints(hints, getClass().getClassLoader()); + assertThat(RuntimeHintsPredicates.resource().forResource("messages.properties")).accepts(hints); + assertThat(RuntimeHintsPredicates.resource().forResource("messages_de.properties")).accepts(hints); + assertThat(RuntimeHintsPredicates.resource().forResource("messages_zh-CN.properties")).accepts(hints); + } + @Configuration(proxyBeanMethods = false) @PropertySource("classpath:/switch-messages.properties") static class Config { From f34dc0545285106d98a97d5a987c38282e2f9cc4 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 3 Aug 2023 16:22:07 +0200 Subject: [PATCH 0241/1215] Rename run goal's directories property to additionalClasspathElements This clarifies what used to be called "directories" as both a directory and a jar file can be provided. A directory with `/*` would also load all the jar files from that directory. The "directories" property has been deprecated as a result. Closes gh-35179 --- .../boot/maven/RunIntegrationTests.java | 23 +++++++++ .../pom.xml | 27 +++++++++++ .../main/additional-elements/another/two.txt | 1 + .../src/main/additional-elements/one.txt | 1 + .../main/java/org/test/SampleApplication.java | 45 ++++++++++++++++++ .../run-additional-classpath-jar/pom.xml | 27 +++++++++++ .../main/additional-jar/resources-1.0.0.jar | Bin 0 -> 657 bytes .../main/java/org/test/SampleApplication.java | 45 ++++++++++++++++++ .../intTest/projects/run-directories/pom.xml | 27 +++++++++++ .../main/additional-elements/another/two.txt | 1 + .../src/main/additional-elements/one.txt | 1 + .../main/java/org/test/SampleApplication.java | 45 ++++++++++++++++++ .../boot/maven/AbstractRunMojo.java | 31 +++++++++--- 13 files changed, 268 insertions(+), 6 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-directory/pom.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-directory/src/main/additional-elements/another/two.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-directory/src/main/additional-elements/one.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-directory/src/main/java/org/test/SampleApplication.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-jar/pom.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-jar/src/main/additional-jar/resources-1.0.0.jar create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-jar/src/main/java/org/test/SampleApplication.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-directories/pom.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-directories/src/main/additional-elements/another/two.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-directories/src/main/additional-elements/one.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-directories/src/main/java/org/test/SampleApplication.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java index ec9f69901b2f..12382d58ef45 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java @@ -30,6 +30,7 @@ * Integration tests for the Maven plugin's run goal. * * @author Andy Wilkinson + * @author Stephane Nicoll */ @ExtendWith(MavenBuildExtension.class) class RunIntegrationTests { @@ -107,6 +108,28 @@ void whenAWorkingDirectoryIsConfiguredTheApplicationIsRunFromThatDirectory(Maven .execute((project) -> assertThat(buildLog(project)).containsPattern("I haz been run from.*src.main.java")); } + @TestTemplate + @Deprecated(since = "3.2.0", forRemoval = true) + void whenDirectoriesAreConfiguredTheyAreAvailableToTheApplication(MavenBuild mavenBuild) { + mavenBuild.project("run-directories") + .goals("spring-boot:run") + .execute((project) -> assertThat(buildLog(project)).contains("I haz been run")); + } + + @TestTemplate + void whenAdditionalClasspathDirectoryIsConfiguredItsResourcesAreAvailableToTheApplication(MavenBuild mavenBuild) { + mavenBuild.project("run-additional-classpath-directory") + .goals("spring-boot:run") + .execute((project) -> assertThat(buildLog(project)).contains("I haz been run")); + } + + @TestTemplate + void whenAdditionalClasspathFileIsConfiguredItsContentIsAvailableToTheApplication(MavenBuild mavenBuild) { + mavenBuild.project("run-additional-classpath-jar") + .goals("spring-boot:run") + .execute((project) -> assertThat(buildLog(project)).contains("I haz been run")); + } + @TestTemplate @DisabledOnOs(OS.WINDOWS) void whenAToolchainIsConfiguredItIsUsedToRunTheApplication(MavenBuild mavenBuild) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-directory/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-directory/pom.xml new file mode 100644 index 000000000000..a03170ba7d46 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-directory/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + org.springframework.boot.maven.it + run-additional-classpath-directory + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + src/main/additional-elements/ + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-directory/src/main/additional-elements/another/two.txt b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-directory/src/main/additional-elements/another/two.txt new file mode 100644 index 000000000000..d8263ee98605 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-directory/src/main/additional-elements/another/two.txt @@ -0,0 +1 @@ +2 \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-directory/src/main/additional-elements/one.txt b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-directory/src/main/additional-elements/one.txt new file mode 100644 index 000000000000..56a6051ca2b0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-directory/src/main/additional-elements/one.txt @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-directory/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-directory/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..cff7f6409bd8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-directory/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2020 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.test; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Scanner; + +public class SampleApplication { + + public static void main(String[] args) { + if (!readContent("one.txt").contains("1")) { + throw new IllegalArgumentException("Invalid content for one.txt"); + } + if (!readContent("another/two.txt").contains("2")) { + throw new IllegalArgumentException("Invalid content for another/two.txt"); + } + System.out.println("I haz been run"); + } + + private static String readContent(String location) { + InputStream in = SampleApplication.class.getClassLoader().getResourceAsStream(location); + if (in == null) { + throw new IllegalArgumentException("Not found: '" + location + "'"); + } + try (Scanner scanner = new Scanner(in, StandardCharsets.UTF_8)) { + return scanner.useDelimiter("\\A").next(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-jar/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-jar/pom.xml new file mode 100644 index 000000000000..7e1887b93fc4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-jar/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + org.springframework.boot.maven.it + run-additional-classpath-directory + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + src/main/additional-jar/resources-1.0.0.jar + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-jar/src/main/additional-jar/resources-1.0.0.jar b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-jar/src/main/additional-jar/resources-1.0.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..f6e05369c57dfc70cec1a3e7b758b9b31ba98c22 GIT binary patch literal 657 zcmWIWW@Zs#;Nak3_};)A&VU3s8CV#6T|*poJ^kGD|D9rBU}gyLX6FE@V1g9Z#+&zDcl_*C?oy!?`k z)FOR^1Iy}AeaDZas-!$$ucV^Hm)ZbR zPBTIuBa;XNYPiAz4HT}Z03J}FP!8}$)r#zCP^cn+EszP~2M)r79Y7`` zs1WvoJc}F*puj?adq5^k7c}UQ9Szcq92TIELI62nutLKvz?+o~B*6-VszBi|P>3@C E0D$9kLI3~& literal 0 HcmV?d00001 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-jar/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-jar/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..cff7f6409bd8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-jar/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2020 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.test; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Scanner; + +public class SampleApplication { + + public static void main(String[] args) { + if (!readContent("one.txt").contains("1")) { + throw new IllegalArgumentException("Invalid content for one.txt"); + } + if (!readContent("another/two.txt").contains("2")) { + throw new IllegalArgumentException("Invalid content for another/two.txt"); + } + System.out.println("I haz been run"); + } + + private static String readContent(String location) { + InputStream in = SampleApplication.class.getClassLoader().getResourceAsStream(location); + if (in == null) { + throw new IllegalArgumentException("Not found: '" + location + "'"); + } + try (Scanner scanner = new Scanner(in, StandardCharsets.UTF_8)) { + return scanner.useDelimiter("\\A").next(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-directories/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-directories/pom.xml new file mode 100644 index 000000000000..4029ed38e431 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-directories/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + org.springframework.boot.maven.it + run-directories + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + src/main/additional-elements/ + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-directories/src/main/additional-elements/another/two.txt b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-directories/src/main/additional-elements/another/two.txt new file mode 100644 index 000000000000..d8263ee98605 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-directories/src/main/additional-elements/another/two.txt @@ -0,0 +1 @@ +2 \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-directories/src/main/additional-elements/one.txt b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-directories/src/main/additional-elements/one.txt new file mode 100644 index 000000000000..56a6051ca2b0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-directories/src/main/additional-elements/one.txt @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-directories/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-directories/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..cff7f6409bd8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-directories/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2020 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.test; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Scanner; + +public class SampleApplication { + + public static void main(String[] args) { + if (!readContent("one.txt").contains("1")) { + throw new IllegalArgumentException("Invalid content for one.txt"); + } + if (!readContent("another/two.txt").contains("2")) { + throw new IllegalArgumentException("Invalid content for another/two.txt"); + } + System.out.println("I haz been run"); + } + + private static String readContent(String location) { + InputStream in = SampleApplication.class.getClassLoader().getResourceAsStream(location); + if (in == null) { + throw new IllegalArgumentException("Not found: '" + location + "'"); + } + try (Scanner scanner = new Scanner(in, StandardCharsets.UTF_8)) { + return scanner.useDelimiter("\\A").next(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java index c2c6a9147722..9ca140975702 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java @@ -39,6 +39,7 @@ import org.apache.maven.toolchain.ToolchainManager; import org.springframework.boot.loader.tools.FileUtils; +import org.springframework.util.ObjectUtils; /** * Base class to run a Spring Boot application. @@ -165,13 +166,24 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { private String mainClass; /** - * Additional directories besides the classes directory that should be added to the + * Additional directories containing classes or resources that should be added to the * classpath. * @since 1.0.0 + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of + * 'additionalClasspathElements' */ @Parameter(property = "spring-boot.run.directories") + @Deprecated(since = "3.2.0", forRemoval = true) private String[] directories; + /** + * Additional classpath elements that should be added to the classpath. An element can + * be a directory with classes and resources or a jar file. + * @since 3.2.0 + */ + @Parameter(property = "spring-boot.run.additional-classpath-elements") + private String[] additionalClasspathElements; + /** * Directory containing the classes and resource files that should be used to run the * application. @@ -348,7 +360,7 @@ private void addClasspath(List args) throws MojoExecutionException { protected URL[] getClassPathUrls() throws MojoExecutionException { try { List urls = new ArrayList<>(); - addUserDefinedDirectories(urls); + addAdditionalClasspathLocations(urls); addResources(urls); addProjectClasses(urls); addDependencies(urls); @@ -359,10 +371,17 @@ protected URL[] getClassPathUrls() throws MojoExecutionException { } } - private void addUserDefinedDirectories(List urls) throws MalformedURLException { - if (this.directories != null) { - for (String directory : this.directories) { - urls.add(new File(directory).toURI().toURL()); + @SuppressWarnings("removal") + private void addAdditionalClasspathLocations(List urls) throws MalformedURLException { + if (!ObjectUtils.isEmpty(this.directories) && !ObjectUtils.isEmpty(this.additionalClasspathElements)) { + throw new IllegalStateException( + "Either additionalClasspathElements or directories (deprecated) should be set, not both"); + } + String[] elements = !ObjectUtils.isEmpty(this.additionalClasspathElements) ? this.additionalClasspathElements + : this.directories; + if (elements != null) { + for (String element : elements) { + urls.add(new File(element).toURI().toURL()); } } } From d93d05ade2ad7523177ad5265ca0a240bd850c60 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 3 Aug 2023 17:24:56 +0200 Subject: [PATCH 0242/1215] Revert "Use virtual threads in BackgroundPreinitializer if enabled" This reverts commit 4bbc3363213fffa6a366ecbb603ddd38f330f6a6. --- .../BackgroundPreinitializer.java | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/BackgroundPreinitializer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/BackgroundPreinitializer.java index bacc9df0dda0..7fc2382303ef 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/BackgroundPreinitializer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/BackgroundPreinitializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2022 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. @@ -23,7 +23,6 @@ import jakarta.validation.Configuration; import jakarta.validation.Validation; -import org.springframework.boot.autoconfigure.thread.Threading; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.boot.context.event.ApplicationFailedEvent; import org.springframework.boot.context.event.ApplicationReadyEvent; @@ -32,9 +31,6 @@ import org.springframework.context.ApplicationListener; import org.springframework.core.NativeDetector; import org.springframework.core.Ordered; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.Environment; -import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; @@ -51,7 +47,6 @@ * @author Andy Wilkinson * @author Artsiom Yudovin * @author Sebastien Deleuze - * @author Moritz Halbritter * @since 1.3.0 */ public class BackgroundPreinitializer implements ApplicationListener, Ordered { @@ -84,8 +79,7 @@ public void onApplicationEvent(SpringApplicationEvent event) { } if (event instanceof ApplicationEnvironmentPreparedEvent && preinitializationStarted.compareAndSet(false, true)) { - ConfigurableEnvironment environment = ((ApplicationEnvironmentPreparedEvent) event).getEnvironment(); - performPreinitialization(environment); + performPreinitialization(); } if ((event instanceof ApplicationReadyEvent || event instanceof ApplicationFailedEvent) && preinitializationStarted.get()) { @@ -98,9 +92,9 @@ public void onApplicationEvent(SpringApplicationEvent event) { } } - private void performPreinitialization(Environment environment) { + private void performPreinitialization() { try { - Runnable runnable = new Runnable() { + Thread thread = new Thread(new Runnable() { @Override public void run() { @@ -108,7 +102,8 @@ public void run() { runSafely(new ValidationInitializer()); if (!runSafely(new MessageConverterInitializer())) { // If the MessageConverterInitializer fails to run, we still might - // be able to initialize Jackson + // be able to + // initialize Jackson runSafely(new JacksonInitializer()); } runSafely(new CharsetInitializer()); @@ -125,12 +120,8 @@ boolean runSafely(Runnable runnable) { } } - }; - SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("background-preinit-"); - if (Threading.VIRTUAL.isActive(environment)) { - taskExecutor.setVirtualThreads(true); - } - taskExecutor.execute(runnable); + }, "background-preinit"); + thread.start(); } catch (Exception ex) { // This will fail on GAE where creating threads is prohibited. We can safely From a843aca8213f49a2afd2b842b581b8986fd2016d Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 3 Aug 2023 17:25:15 +0200 Subject: [PATCH 0243/1215] Revert "Polish" This reverts commit 25eb3c8c18345e4b651a4fd1be44e72afa62ce90. --- .../jms/JmsHealthContributorAutoConfiguration.java | 2 +- .../springframework/boot/actuate/jms/JmsHealthIndicator.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfiguration.java index 70bface1ce01..64ba0e4568e7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfiguration.java @@ -64,7 +64,7 @@ public HealthContributor jmsHealthContributor(Map con } private static SimpleAsyncTaskExecutor getTaskExecutor(Environment environment) { - SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("jms-health-indicator-"); + SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("jms-health-indicator"); if (Threading.VIRTUAL.isActive(environment)) { taskExecutor.setVirtualThreads(true); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jms/JmsHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jms/JmsHealthIndicator.java index e31578520732..fd1322f59cce 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jms/JmsHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jms/JmsHealthIndicator.java @@ -59,7 +59,7 @@ public class JmsHealthIndicator extends AbstractHealthIndicator { */ @Deprecated(since = "3.2.0", forRemoval = true) public JmsHealthIndicator(ConnectionFactory connectionFactory) { - this(connectionFactory, new SimpleAsyncTaskExecutor("jms-health-indicator-"), Duration.ofSeconds(5)); + this(connectionFactory, new SimpleAsyncTaskExecutor("jms-health-indicator"), Duration.ofSeconds(5)); } /** From 02a7c22f40f7e4b992e277ae58c60421b6a49421 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 3 Aug 2023 17:25:28 +0200 Subject: [PATCH 0244/1215] Revert "Use virtual threads in JmsHealthIndicator if enabled" This reverts commit 6fc585c5d2e19ad1a07b65bbb6decddae78b854e. --- ...JmsHealthContributorAutoConfiguration.java | 19 +--------- ...althContributorAutoConfigurationTests.java | 18 --------- .../boot/actuate/jms/JmsHealthIndicator.java | 38 ++----------------- .../actuate/jms/JmsHealthIndicatorTests.java | 18 +++------ 4 files changed, 11 insertions(+), 82 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfiguration.java index 64ba0e4568e7..aa7d72ab27aa 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfiguration.java @@ -16,7 +16,6 @@ package org.springframework.boot.actuate.autoconfigure.jms; -import java.time.Duration; import java.util.Map; import jakarta.jms.ConnectionFactory; @@ -32,16 +31,12 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration; import org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration; -import org.springframework.boot.autoconfigure.thread.Threading; import org.springframework.context.annotation.Bean; -import org.springframework.core.env.Environment; -import org.springframework.core.task.SimpleAsyncTaskExecutor; /** * {@link EnableAutoConfiguration Auto-configuration} for {@link JmsHealthIndicator}. * * @author Stephane Nicoll - * @author Moritz Halbritter * @since 2.0.0 */ @AutoConfiguration(after = { ActiveMQAutoConfiguration.class, ArtemisAutoConfiguration.class }) @@ -51,10 +46,8 @@ public class JmsHealthContributorAutoConfiguration extends CompositeHealthContributorConfiguration { - private static final Duration TIMEOUT = Duration.ofSeconds(5); - - public JmsHealthContributorAutoConfiguration(Environment environment) { - super((connectionFactory) -> new JmsHealthIndicator(connectionFactory, getTaskExecutor(environment), TIMEOUT)); + public JmsHealthContributorAutoConfiguration() { + super(JmsHealthIndicator::new); } @Bean @@ -63,12 +56,4 @@ public HealthContributor jmsHealthContributor(Map con return createContributor(connectionFactories); } - private static SimpleAsyncTaskExecutor getTaskExecutor(Environment environment) { - SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor("jms-health-indicator"); - if (Threading.VIRTUAL.isActive(environment)) { - taskExecutor.setVirtualThreads(true); - } - return taskExecutor; - } - } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfigurationTests.java index e630b95c78eb..c8fa3c696200 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/jms/JmsHealthContributorAutoConfigurationTests.java @@ -17,8 +17,6 @@ package org.springframework.boot.actuate.autoconfigure.jms; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledForJreRange; -import org.junit.jupiter.api.condition.JRE; import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration; import org.springframework.boot.actuate.jms.JmsHealthIndicator; @@ -26,9 +24,6 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.boot.testsupport.assertj.SimpleAsyncTaskExecutorAssert; -import org.springframework.core.task.SimpleAsyncTaskExecutor; -import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -36,7 +31,6 @@ * Tests for {@link JmsHealthContributorAutoConfiguration}. * * @author Phillip Webb - * @author Moritz Halbritter */ class JmsHealthContributorAutoConfigurationTests { @@ -49,18 +43,6 @@ void runShouldCreateIndicator() { this.contextRunner.run((context) -> assertThat(context).hasSingleBean(JmsHealthIndicator.class)); } - @Test - @EnabledForJreRange(min = JRE.JAVA_21) - void shouldUseVirtualThreadsIfEnabled() { - this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> { - JmsHealthIndicator jmsHealthIndicator = context.getBean(JmsHealthIndicator.class); - assertThat(jmsHealthIndicator).isNotNull(); - Object taskExecutor = ReflectionTestUtils.getField(jmsHealthIndicator, "taskExecutor"); - assertThat(taskExecutor).isInstanceOf(SimpleAsyncTaskExecutor.class); - SimpleAsyncTaskExecutorAssert.assertThat((SimpleAsyncTaskExecutor) taskExecutor).usesVirtualThreads(); - }); - } - @Test void runWhenDisabledShouldNotCreateIndicator() { this.contextRunner.withPropertyValues("management.health.jms.enabled:false") diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jms/JmsHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jms/JmsHealthIndicator.java index fd1322f59cce..3c8f961d5372 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jms/JmsHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jms/JmsHealthIndicator.java @@ -16,7 +16,6 @@ package org.springframework.boot.actuate.jms; -import java.time.Duration; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -29,15 +28,11 @@ import org.springframework.boot.actuate.health.AbstractHealthIndicator; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; -import org.springframework.core.log.LogMessage; -import org.springframework.core.task.AsyncTaskExecutor; -import org.springframework.core.task.SimpleAsyncTaskExecutor; /** * {@link HealthIndicator} for a JMS {@link ConnectionFactory}. * * @author Stephane Nicoll - * @author Moritz Halbritter * @since 2.0.0 */ public class JmsHealthIndicator extends AbstractHealthIndicator { @@ -46,33 +41,9 @@ public class JmsHealthIndicator extends AbstractHealthIndicator { private final ConnectionFactory connectionFactory; - private final AsyncTaskExecutor taskExecutor; - - private final Duration timeout; - - /** - * Creates a new {@link JmsHealthIndicator}, using a {@link SimpleAsyncTaskExecutor} - * and a timeout of 5 seconds. - * @param connectionFactory the connection factory - * @deprecated since 3.2.0 for removal in 3.4.0 in favor of - * {@link #JmsHealthIndicator(ConnectionFactory, AsyncTaskExecutor, Duration)} - */ - @Deprecated(since = "3.2.0", forRemoval = true) public JmsHealthIndicator(ConnectionFactory connectionFactory) { - this(connectionFactory, new SimpleAsyncTaskExecutor("jms-health-indicator"), Duration.ofSeconds(5)); - } - - /** - * Creates a new {@link JmsHealthIndicator}. - * @param connectionFactory the connection factory - * @param taskExecutor the task executor used to run timeout checks - * @param timeout the connection timeout - */ - public JmsHealthIndicator(ConnectionFactory connectionFactory, AsyncTaskExecutor taskExecutor, Duration timeout) { super("JMS health check failed"); this.connectionFactory = connectionFactory; - this.taskExecutor = taskExecutor; - this.timeout = timeout; } @Override @@ -94,19 +65,18 @@ private final class MonitoredConnection { } void start() throws JMSException { - JmsHealthIndicator.this.taskExecutor.execute(() -> { + new Thread(() -> { try { - if (!this.latch.await(JmsHealthIndicator.this.timeout.toMillis(), TimeUnit.MILLISECONDS)) { + if (!this.latch.await(5, TimeUnit.SECONDS)) { JmsHealthIndicator.this.logger - .warn(LogMessage.format("Connection failed to start within %s and will be closed.", - JmsHealthIndicator.this.timeout)); + .warn("Connection failed to start within 5 seconds and will be closed."); closeConnection(); } } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } - }); + }, "jms-health-indicator").start(); this.connection.start(); this.latch.countDown(); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/jms/JmsHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/jms/JmsHealthIndicatorTests.java index f52082859df5..2e005d591d2e 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/jms/JmsHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/jms/JmsHealthIndicatorTests.java @@ -16,8 +16,6 @@ package org.springframework.boot.actuate.jms; -import java.time.Duration; - import jakarta.jms.Connection; import jakarta.jms.ConnectionFactory; import jakarta.jms.ConnectionMetaData; @@ -28,8 +26,6 @@ import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.Status; -import org.springframework.core.task.AsyncTaskExecutor; -import org.springframework.core.task.SimpleAsyncTaskExecutor; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -45,10 +41,6 @@ */ class JmsHealthIndicatorTests { - private static final Duration TIMEOUT = Duration.ofMillis(100); - - private final AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); - @Test void jmsBrokerIsUp() throws JMSException { ConnectionMetaData connectionMetaData = mock(ConnectionMetaData.class); @@ -57,7 +49,7 @@ void jmsBrokerIsUp() throws JMSException { given(connection.getMetaData()).willReturn(connectionMetaData); ConnectionFactory connectionFactory = mock(ConnectionFactory.class); given(connectionFactory.createConnection()).willReturn(connection); - JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory, this.taskExecutor, TIMEOUT); + JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory); Health health = indicator.health(); assertThat(health.getStatus()).isEqualTo(Status.UP); assertThat(health.getDetails()).containsEntry("provider", "JMS test provider"); @@ -68,7 +60,7 @@ void jmsBrokerIsUp() throws JMSException { void jmsBrokerIsDown() throws JMSException { ConnectionFactory connectionFactory = mock(ConnectionFactory.class); given(connectionFactory.createConnection()).willThrow(new JMSException("test", "123")); - JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory, this.taskExecutor, TIMEOUT); + JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory); Health health = indicator.health(); assertThat(health.getStatus()).isEqualTo(Status.DOWN); assertThat(health.getDetails()).doesNotContainKey("provider"); @@ -82,7 +74,7 @@ void jmsBrokerCouldNotRetrieveProviderMetadata() throws JMSException { given(connection.getMetaData()).willReturn(connectionMetaData); ConnectionFactory connectionFactory = mock(ConnectionFactory.class); given(connectionFactory.createConnection()).willReturn(connection); - JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory, this.taskExecutor, TIMEOUT); + JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory); Health health = indicator.health(); assertThat(health.getStatus()).isEqualTo(Status.DOWN); assertThat(health.getDetails()).doesNotContainKey("provider"); @@ -98,7 +90,7 @@ void jmsBrokerUsesFailover() throws JMSException { given(connection.getMetaData()).willReturn(connectionMetaData); willThrow(new JMSException("Could not start", "123")).given(connection).start(); given(connectionFactory.createConnection()).willReturn(connection); - JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory, this.taskExecutor, TIMEOUT); + JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory); Health health = indicator.health(); assertThat(health.getStatus()).isEqualTo(Status.DOWN); assertThat(health.getDetails()).doesNotContainKey("provider"); @@ -117,7 +109,7 @@ void whenConnectionStartIsUnresponsiveStatusIsDown() throws JMSException { }).given(connection).close(); ConnectionFactory connectionFactory = mock(ConnectionFactory.class); given(connectionFactory.createConnection()).willReturn(connection); - JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory, this.taskExecutor, TIMEOUT); + JmsHealthIndicator indicator = new JmsHealthIndicator(connectionFactory); Health health = indicator.health(); assertThat(health.getStatus()).isEqualTo(Status.DOWN); assertThat((String) health.getDetails().get("error")).contains("Connection closed"); From 1a8b8ce26e95e24458d3003c57debfd96dd6414b Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 3 Aug 2023 17:26:31 +0200 Subject: [PATCH 0245/1215] Revert "Revise synchronized blocks" This reverts commit 497bbf9c2d0fafa49e5e9e2688fcc8000d9f5675. --- .../TemplateAvailabilityProviders.java | 18 +--- .../devtools/filewatch/FileSystemWatcher.java | 6 +- .../devtools/livereload/LiveReloadServer.java | 59 +++---------- .../boot/devtools/restart/Restarter.java | 17 ++-- .../devtools/tunnel/client/TunnelClient.java | 30 ++----- .../payload/HttpTunnelPayloadForwarder.java | 12 +-- .../tunnel/server/HttpTunnelServer.java | 86 ++++--------------- .../web/servlet/WebDriverScope.java | 27 +----- .../web/SpringBootMockServletContext.java | 21 ++--- .../loader/data/RandomAccessDataFile.java | 24 ++---- .../builder/SpringApplicationBuilder.java | 8 +- .../boot/env/ConfigTreePropertySource.java | 13 +-- .../boot/logging/java/SimpleFormatter.java | 10 ++- ...cationContextServerWebExchangeMatcher.java | 12 +-- .../ApplicationContextRequestMatcher.java | 10 +-- .../boot/system/ApplicationTemp.java | 15 +--- .../web/embedded/jetty/JettyWebServer.java | 64 ++++++-------- .../web/embedded/tomcat/TomcatWebServer.java | 78 +++++++---------- .../embedded/undertow/UndertowWebServer.java | 16 +--- 19 files changed, 148 insertions(+), 378 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/TemplateAvailabilityProviders.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/TemplateAvailabilityProviders.java index de476fe9c2d7..b787dc413a09 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/TemplateAvailabilityProviders.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/TemplateAvailabilityProviders.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2022 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. @@ -22,8 +22,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import org.springframework.context.ApplicationContext; import org.springframework.core.env.Environment; @@ -54,13 +52,7 @@ public class TemplateAvailabilityProviders { private final Map resolved = new ConcurrentHashMap<>(CACHE_LIMIT); /** - * Guards access to {@link #cache}. - */ - private final Lock cacheLock = new ReentrantLock(); - - /** - * Map from view name resolve template view, protected by {@link #cacheLock} when - * accessed. + * Map from view name resolve template view, synchronized when accessed. */ private final Map cache = new LinkedHashMap<>(CACHE_LIMIT, 0.75f, true) { @@ -141,16 +133,12 @@ public TemplateAvailabilityProvider getProvider(String view, Environment environ } TemplateAvailabilityProvider provider = this.resolved.get(view); if (provider == null) { - this.cacheLock.lock(); - try { + synchronized (this.cache) { provider = findProvider(view, environment, classLoader, resourceLoader); provider = (provider != null) ? provider : NONE; this.resolved.put(view, provider); this.cache.put(view, provider); } - finally { - this.cacheLock.unlock(); - } } return (provider != NONE) ? provider : null; } diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcher.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcher.java index 35a950c6e3b1..99ef112ea3b5 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcher.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2022 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. @@ -158,7 +158,9 @@ public void setTriggerFilter(FileFilter triggerFilter) { } private void checkNotStarted() { - Assert.state(this.watchThread == null, "FileSystemWatcher already started"); + synchronized (this.monitor) { + Assert.state(this.watchThread == null, "FileSystemWatcher already started"); + } } /** diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/LiveReloadServer.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/LiveReloadServer.java index a22216be0948..773fcabc87fe 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/LiveReloadServer.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/LiveReloadServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2019 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. @@ -29,8 +29,6 @@ import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -59,12 +57,7 @@ public class LiveReloadServer { private final List connections = new ArrayList<>(); - /** - * Guards access to {@link #connections}. - */ - private final Lock connectionsLock = new ReentrantLock(); - - private final Lock lock = new ReentrantLock(); + private final Object monitor = new Object(); private final int port; @@ -115,8 +108,7 @@ public LiveReloadServer(int port, ThreadFactory threadFactory) { * @throws IOException in case of I/O errors */ public int start() throws IOException { - this.lock.lock(); - try { + synchronized (this.monitor) { Assert.state(!isStarted(), "Server already started"); logger.debug(LogMessage.format("Starting live reload server on port %s", this.port)); this.serverSocket = new ServerSocket(this.port); @@ -127,9 +119,6 @@ public int start() throws IOException { this.listenThread.start(); return localPort; } - finally { - this.lock.unlock(); - } } /** @@ -137,13 +126,9 @@ public int start() throws IOException { * @return {@code true} if the server is running */ public boolean isStarted() { - this.lock.lock(); - try { + synchronized (this.monitor) { return this.listenThread != null; } - finally { - this.lock.unlock(); - } } /** @@ -178,8 +163,7 @@ private void acceptConnections() { * @throws IOException in case of I/O errors */ public void stop() throws IOException { - this.lock.lock(); - try { + synchronized (this.monitor) { if (this.listenThread != null) { closeAllConnections(); try { @@ -200,31 +184,22 @@ public void stop() throws IOException { this.serverSocket = null; } } - finally { - this.lock.unlock(); - } } private void closeAllConnections() throws IOException { - this.connectionsLock.lock(); - try { + synchronized (this.connections) { for (Connection connection : this.connections) { connection.close(); } } - finally { - this.connectionsLock.unlock(); - } } /** * Trigger livereload of all connected clients. */ public void triggerReload() { - this.lock.lock(); - try { - this.connectionsLock.lock(); - try { + synchronized (this.monitor) { + synchronized (this.connections) { for (Connection connection : this.connections) { try { connection.triggerReload(); @@ -234,33 +209,19 @@ public void triggerReload() { } } } - finally { - this.connectionsLock.unlock(); - } - } - finally { - this.lock.unlock(); } } private void addConnection(Connection connection) { - this.connectionsLock.lock(); - try { + synchronized (this.connections) { this.connections.add(connection); } - finally { - this.connectionsLock.unlock(); - } } private void removeConnection(Connection connection) { - this.connectionsLock.lock(); - try { + synchronized (this.connections) { this.connections.remove(connection); } - finally { - this.connectionsLock.unlock(); - } } /** diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java index 410b269d1d66..bdc69f02d84d 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2022 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. @@ -22,6 +22,7 @@ import java.net.URL; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; @@ -29,7 +30,6 @@ import java.util.Set; import java.util.concurrent.BlockingDeque; import java.util.concurrent.Callable; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ThreadFactory; @@ -92,7 +92,7 @@ public class Restarter { private final ClassLoaderFiles classLoaderFiles = new ClassLoaderFiles(); - private final Map attributes = new ConcurrentHashMap<>(); + private final Map attributes = new HashMap<>(); private final BlockingDeque leakSafeThreads = new LinkedBlockingDeque<>(); @@ -440,11 +440,18 @@ private LeakSafeThread getLeakSafeThread() { } public Object getOrAddAttribute(String name, final ObjectFactory objectFactory) { - return this.attributes.computeIfAbsent(name, (ignore) -> objectFactory.getObject()); + synchronized (this.attributes) { + if (!this.attributes.containsKey(name)) { + this.attributes.put(name, objectFactory.getObject()); + } + return this.attributes.get(name); + } } public Object removeAttribute(String name) { - return this.attributes.remove(name); + synchronized (this.attributes) { + return this.attributes.remove(name); + } } /** diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/TunnelClient.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/TunnelClient.java index 3ab00f4ed3a5..e4a439492d3c 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/TunnelClient.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/client/TunnelClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2019 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. @@ -25,8 +25,6 @@ import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.channels.WritableByteChannel; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -51,7 +49,7 @@ public class TunnelClient implements SmartInitializingSingleton { private final TunnelClientListeners listeners = new TunnelClientListeners(); - private final Lock lock = new ReentrantLock(); + private final Object monitor = new Object(); private final int listenPort; @@ -68,8 +66,7 @@ public TunnelClient(int listenPort, TunnelConnection tunnelConnection) { @Override public void afterSingletonsInstantiated() { - this.lock.lock(); - try { + synchronized (this.monitor) { if (this.serverThread == null) { try { start(); @@ -79,9 +76,6 @@ public void afterSingletonsInstantiated() { } } } - finally { - this.lock.unlock(); - } } /** @@ -90,8 +84,7 @@ public void afterSingletonsInstantiated() { * @throws IOException in case of I/O errors */ public int start() throws IOException { - this.lock.lock(); - try { + synchronized (this.monitor) { Assert.state(this.serverThread == null, "Server already started"); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.socket().bind(new InetSocketAddress(this.listenPort)); @@ -101,9 +94,6 @@ public int start() throws IOException { this.serverThread.start(); return port; } - finally { - this.lock.unlock(); - } } /** @@ -111,8 +101,7 @@ public int start() throws IOException { * @throws IOException in case of I/O errors */ public void stop() throws IOException { - this.lock.lock(); - try { + synchronized (this.monitor) { if (this.serverThread != null) { this.serverThread.close(); try { @@ -124,19 +113,12 @@ public void stop() throws IOException { this.serverThread = null; } } - finally { - this.lock.unlock(); - } } protected final ServerThread getServerThread() { - this.lock.lock(); - try { + synchronized (this.monitor) { return this.serverThread; } - finally { - this.lock.unlock(); - } } public void addListener(TunnelClientListener listener) { diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/payload/HttpTunnelPayloadForwarder.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/payload/HttpTunnelPayloadForwarder.java index f1a0e40e71d1..0bf486fcaa2d 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/payload/HttpTunnelPayloadForwarder.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/payload/HttpTunnelPayloadForwarder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2019 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. @@ -20,8 +20,6 @@ import java.nio.channels.WritableByteChannel; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import org.springframework.util.Assert; @@ -38,7 +36,7 @@ public class HttpTunnelPayloadForwarder { private final Map queue = new HashMap<>(); - private final Lock lock = new ReentrantLock(); + private final Object monitor = new Object(); private final WritableByteChannel targetChannel; @@ -54,8 +52,7 @@ public HttpTunnelPayloadForwarder(WritableByteChannel targetChannel) { } public void forward(HttpTunnelPayload payload) throws IOException { - this.lock.lock(); - try { + synchronized (this.monitor) { long seq = payload.getSequence(); if (this.lastRequestSeq != seq - 1) { Assert.state(this.queue.size() < MAXIMUM_QUEUE_SIZE, "Too many messages queued"); @@ -70,9 +67,6 @@ public void forward(HttpTunnelPayload payload) throws IOException { forward(queuedItem); } } - finally { - this.lock.unlock(); - } } } diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/HttpTunnelServer.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/HttpTunnelServer.java index 4d16788699c1..b03e724dfbd8 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/HttpTunnelServer.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/tunnel/server/HttpTunnelServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2022 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. @@ -25,9 +25,6 @@ import java.util.Iterator; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -125,12 +122,7 @@ public class HttpTunnelServer { private long disconnectTimeout = DEFAULT_DISCONNECT_TIMEOUT; - /** - * Guards access to {@link #serverThread}. - */ - private final Lock serverThreadLock = new ReentrantLock(); - - private ServerThread serverThread; + private volatile ServerThread serverThread; /** * Creates a new {@link HttpTunnelServer} instance. @@ -172,8 +164,7 @@ protected void handle(HttpConnection httpConnection) throws IOException { * @throws IOException in case of I/O errors */ protected ServerThread getServerThread() throws IOException { - this.serverThreadLock.lock(); - try { + synchronized (this) { if (this.serverThread == null) { ByteChannel channel = this.serverConnection.open(this.longPollTimeout); this.serverThread = new ServerThread(channel); @@ -181,22 +172,15 @@ protected ServerThread getServerThread() throws IOException { } return this.serverThread; } - finally { - this.serverThreadLock.unlock(); - } } /** * Called when the server thread exits. */ void clearServerThread() { - this.serverThreadLock.lock(); - try { + synchronized (this) { this.serverThread = null; } - finally { - this.serverThreadLock.unlock(); - } } /** @@ -226,13 +210,6 @@ protected class ServerThread extends Thread { private final Deque httpConnections; - /** - * Guards access to {@link #httpConnections}. - */ - private final Lock httpConnectionsLock = new ReentrantLock(); - - private final Condition httpConnectionsCondition = this.httpConnectionsLock.newCondition(); - private final HttpTunnelPayloadForwarder payloadForwarder; private boolean closed; @@ -270,8 +247,7 @@ private void readAndForwardTargetServerData() throws IOException { while (this.targetServer.isOpen()) { closeStaleHttpConnections(); ByteBuffer data = HttpTunnelPayload.getPayloadData(this.targetServer); - this.httpConnectionsLock.lock(); - try { + synchronized (this.httpConnections) { if (data != null) { HttpTunnelPayload payload = new HttpTunnelPayload(this.responseSeq.incrementAndGet(), data); payload.logIncoming(); @@ -279,20 +255,15 @@ private void readAndForwardTargetServerData() throws IOException { connection.respond(payload); } } - finally { - this.httpConnectionsLock.unlock(); - } } } private HttpConnection getOrWaitForHttpConnection() { - this.httpConnectionsLock.lock(); - try { + synchronized (this.httpConnections) { HttpConnection httpConnection = this.httpConnections.pollFirst(); while (httpConnection == null) { try { - this.httpConnectionsCondition.await(HttpTunnelServer.this.longPollTimeout, - TimeUnit.MILLISECONDS); + this.httpConnections.wait(HttpTunnelServer.this.longPollTimeout); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); @@ -302,14 +273,10 @@ private HttpConnection getOrWaitForHttpConnection() { } return httpConnection; } - finally { - this.httpConnectionsLock.unlock(); - } } private void closeStaleHttpConnections() throws IOException { - this.httpConnectionsLock.lock(); - try { + synchronized (this.httpConnections) { checkNotDisconnected(); Iterator iterator = this.httpConnections.iterator(); while (iterator.hasNext()) { @@ -320,9 +287,6 @@ private void closeStaleHttpConnections() throws IOException { } } } - finally { - this.httpConnectionsLock.unlock(); - } } private void checkNotDisconnected() { @@ -334,8 +298,7 @@ private void checkNotDisconnected() { } private void closeHttpConnections() { - this.httpConnectionsLock.lock(); - try { + synchronized (this.httpConnections) { while (!this.httpConnections.isEmpty()) { try { this.httpConnections.removeFirst().respond(HttpStatus.GONE); @@ -345,9 +308,6 @@ private void closeHttpConnections() { } } } - finally { - this.httpConnectionsLock.unlock(); - } } private void closeTargetServer() { @@ -368,17 +328,13 @@ public void handleIncomingHttp(HttpConnection httpConnection) throws IOException if (this.closed) { httpConnection.respond(HttpStatus.GONE); } - this.httpConnectionsLock.lock(); - try { + synchronized (this.httpConnections) { while (this.httpConnections.size() > 1) { this.httpConnections.removeFirst().respond(HttpStatus.TOO_MANY_REQUESTS); } this.lastHttpRequestTime = System.currentTimeMillis(); this.httpConnections.addLast(httpConnection); - this.httpConnectionsCondition.signal(); - } - finally { - this.httpConnectionsLock.unlock(); + this.httpConnections.notify(); } forwardToTargetServer(httpConnection); } @@ -412,10 +368,6 @@ protected static class HttpConnection { private volatile boolean complete = false; - private final Lock lock = new ReentrantLock(); - - private final Condition lockCondition = this.lock.newCondition(); - public HttpConnection(ServerHttpRequest request, ServerHttpResponse response) { this.createTime = System.currentTimeMillis(); this.request = request; @@ -474,12 +426,8 @@ public void waitForResponse() { if (this.async == null) { while (!this.complete) { try { - this.lock.lock(); - try { - this.lockCondition.await(1, TimeUnit.SECONDS); - } - finally { - this.lock.unlock(); + synchronized (this) { + wait(1000); } } catch (InterruptedException ex) { @@ -528,13 +476,9 @@ protected void complete() { this.async.complete(); } else { - this.lock.lock(); - try { + synchronized (this) { this.complete = true; - this.lockCondition.signalAll(); - } - finally { - this.lock.unlock(); + notifyAll(); } } } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverScope.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverScope.java index f9338002e169..5b91ef38e44f 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverScope.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverScope.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2022 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. @@ -18,8 +18,6 @@ import java.util.HashMap; import java.util.Map; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import org.openqa.selenium.WebDriver; @@ -54,17 +52,11 @@ public class WebDriverScope implements Scope { private static final String[] BEAN_CLASSES = { WEB_DRIVER_CLASS, "org.springframework.test.web.servlet.htmlunit.webdriver.MockMvcHtmlUnitDriverBuilder" }; - /** - * Guards access to {@link #instances}. - */ - private final Lock instancesLock = new ReentrantLock(); - private final Map instances = new HashMap<>(); @Override public Object get(String name, ObjectFactory objectFactory) { - this.instancesLock.lock(); - try { + synchronized (this.instances) { Object instance = this.instances.get(name); if (instance == null) { instance = objectFactory.getObject(); @@ -72,20 +64,13 @@ public Object get(String name, ObjectFactory objectFactory) { } return instance; } - finally { - this.instancesLock.unlock(); - } } @Override public Object remove(String name) { - this.instancesLock.lock(); - try { + synchronized (this.instances) { return this.instances.remove(name); } - finally { - this.instancesLock.unlock(); - } } @Override @@ -108,8 +93,7 @@ public String getConversationId() { */ boolean reset() { boolean reset = false; - this.instancesLock.lock(); - try { + synchronized (this.instances) { for (Object instance : this.instances.values()) { reset = true; if (instance instanceof WebDriver webDriver) { @@ -118,9 +102,6 @@ boolean reset() { } this.instances.clear(); } - finally { - this.instancesLock.unlock(); - } return reset; } diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/web/SpringBootMockServletContext.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/web/SpringBootMockServletContext.java index 30d800a85dfc..ec810bac3fd5 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/web/SpringBootMockServletContext.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/web/SpringBootMockServletContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2020 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. @@ -21,8 +21,6 @@ import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Files; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import org.springframework.core.io.FileSystemResourceLoader; import org.springframework.core.io.Resource; @@ -46,11 +44,6 @@ public class SpringBootMockServletContext extends MockServletContext { private File emptyRootDirectory; - /** - * Guards access to {@link #emptyRootDirectory}. - */ - private final Lock emptyRootDirectoryLock = new ReentrantLock(); - public SpringBootMockServletContext(String resourceBasePath) { this(resourceBasePath, new FileSystemResourceLoader()); } @@ -98,21 +91,19 @@ public URL getResource(String path) throws MalformedURLException { if (resource == null && "/".equals(path)) { // Liquibase assumes that "/" always exists, if we don't have a directory // use a temporary location. - this.emptyRootDirectoryLock.lock(); try { if (this.emptyRootDirectory == null) { - File tempDirectory = Files.createTempDirectory("spr-servlet").toFile(); - tempDirectory.deleteOnExit(); - this.emptyRootDirectory = tempDirectory; + synchronized (this) { + File tempDirectory = Files.createTempDirectory("spr-servlet").toFile(); + tempDirectory.deleteOnExit(); + this.emptyRootDirectory = tempDirectory; + } } return this.emptyRootDirectory.toURI().toURL(); } catch (IOException ex) { // Ignore } - finally { - this.emptyRootDirectoryLock.unlock(); - } } return resource; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java index 91474edd42b7..06d9abcda51a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2022 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. @@ -22,8 +22,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; /** * {@link RandomAccessData} implementation backed by a {@link RandomAccessFile}. @@ -211,7 +209,7 @@ private long moveOn(int amount) { private static final class FileAccess { - private final Lock lock = new ReentrantLock(); + private final Object monitor = new Object(); private final File file; @@ -223,15 +221,11 @@ private FileAccess(File file) { } private int read(byte[] bytes, long position, int offset, int length) throws IOException { - this.lock.lock(); - try { + synchronized (this.monitor) { openIfNecessary(); this.randomAccessFile.seek(position); return this.randomAccessFile.read(bytes, offset, length); } - finally { - this.lock.unlock(); - } } private void openIfNecessary() { @@ -247,28 +241,20 @@ private void openIfNecessary() { } private void close() throws IOException { - this.lock.lock(); - try { + synchronized (this.monitor) { if (this.randomAccessFile != null) { this.randomAccessFile.close(); this.randomAccessFile = null; } } - finally { - this.lock.unlock(); - } } private int readByte(long position) throws IOException { - this.lock.lock(); - try { + synchronized (this.monitor) { openIfNecessary(); this.randomAccessFile.seek(position); return this.randomAccessFile.read(); } - finally { - this.lock.unlock(); - } } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java index b479568fcb53..712caa5e6f15 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java @@ -76,7 +76,7 @@ public class SpringApplicationBuilder { private final SpringApplication application; - private volatile ConfigurableApplicationContext context; + private ConfigurableApplicationContext context; private SpringApplicationBuilder parent; @@ -145,8 +145,10 @@ public ConfigurableApplicationContext run(String... args) { } configureAsChildIfNecessary(args); if (this.running.compareAndSet(false, true)) { - // If not already running copy the sources over and then run. - this.context = build().run(args); + synchronized (this.running) { + // If not already running copy the sources over and then run. + this.context = build().run(args); + } } return this.context; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/ConfigTreePropertySource.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/ConfigTreePropertySource.java index 1760b240f1ae..866404b46674 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/ConfigTreePropertySource.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/ConfigTreePropertySource.java @@ -29,8 +29,6 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Stream; import org.springframework.boot.convert.ApplicationConversionService; @@ -260,11 +258,6 @@ private static final class PropertyFileContent implements Value, OriginProvider private final Path path; - /** - * Guards access to {@link #resource}. - */ - private final Lock resourceLock = new ReentrantLock(); - private final Resource resource; private final Origin origin; @@ -348,15 +341,11 @@ private byte[] getBytes() { } if (this.content == null) { assertStillExists(); - this.resourceLock.lock(); - try { + synchronized (this.resource) { if (this.content == null) { this.content = FileCopyUtils.copyToByteArray(this.resource.getInputStream()); } } - finally { - this.resourceLock.unlock(); - } } return this.content; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/java/SimpleFormatter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/java/SimpleFormatter.java index fc9dbfbcaf29..1701a6a03a23 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/java/SimpleFormatter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/java/SimpleFormatter.java @@ -38,15 +38,17 @@ public class SimpleFormatter extends Formatter { private final String pid = getOrUseDefault(LoggingSystemProperty.PID.getEnvironmentVariableName(), "????"); + private final Date date = new Date(); + @Override - public String format(LogRecord record) { - Date date = new Date(record.getMillis()); + public synchronized String format(LogRecord record) { + this.date.setTime(record.getMillis()); String source = record.getLoggerName(); String message = formatMessage(record); String throwable = getThrowable(record); String thread = getThreadName(); - return String.format(this.format, date, source, record.getLoggerName(), record.getLevel().getLocalizedName(), - message, throwable, thread, this.pid); + return String.format(this.format, this.date, source, record.getLoggerName(), + record.getLevel().getLocalizedName(), message, throwable, thread, this.pid); } private String getThrowable(LogRecord record) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/security/reactive/ApplicationContextServerWebExchangeMatcher.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/security/reactive/ApplicationContextServerWebExchangeMatcher.java index e3732e219d59..59264b353ff2 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/security/reactive/ApplicationContextServerWebExchangeMatcher.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/security/reactive/ApplicationContextServerWebExchangeMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2020 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. @@ -16,8 +16,6 @@ package org.springframework.boot.security.reactive; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; import reactor.core.publisher.Mono; @@ -46,7 +44,7 @@ public abstract class ApplicationContextServerWebExchangeMatcher implements S private volatile Supplier context; - private final Lock contextLock = new ReentrantLock(); + private final Object contextLock = new Object(); public ApplicationContextServerWebExchangeMatcher(Class contextClass) { Assert.notNull(contextClass, "Context class must not be null"); @@ -83,17 +81,13 @@ protected boolean ignoreApplicationContext(ApplicationContext applicationContext protected Supplier getContext(ServerWebExchange exchange) { if (this.context == null) { - this.contextLock.lock(); - try { + synchronized (this.contextLock) { if (this.context == null) { Supplier createdContext = createContext(exchange); initialized(createdContext); this.context = createdContext; } } - finally { - this.contextLock.unlock(); - } } return this.context; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/security/servlet/ApplicationContextRequestMatcher.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/security/servlet/ApplicationContextRequestMatcher.java index 3c6542af3fca..0f2d9e3858ec 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/security/servlet/ApplicationContextRequestMatcher.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/security/servlet/ApplicationContextRequestMatcher.java @@ -16,8 +16,6 @@ package org.springframework.boot.security.servlet; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; import jakarta.servlet.http.HttpServletRequest; @@ -47,7 +45,7 @@ public abstract class ApplicationContextRequestMatcher implements RequestMatc private volatile boolean initialized; - private final Lock initializeLock = new ReentrantLock(); + private final Object initializeLock = new Object(); public ApplicationContextRequestMatcher(Class contextClass) { Assert.notNull(contextClass, "Context class must not be null"); @@ -63,16 +61,12 @@ public final boolean matches(HttpServletRequest request) { } Supplier context = () -> getContext(webApplicationContext); if (!this.initialized) { - this.initializeLock.lock(); - try { + synchronized (this.initializeLock) { if (!this.initialized) { initialized(context); this.initialized = true; } } - finally { - this.initializeLock.unlock(); - } } return matches(request, context); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/system/ApplicationTemp.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/system/ApplicationTemp.java index 852db487cd2b..9c413a0ab702 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/system/ApplicationTemp.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/system/ApplicationTemp.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2022 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. @@ -28,8 +28,6 @@ import java.security.MessageDigest; import java.util.EnumSet; import java.util.HexFormat; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -53,11 +51,6 @@ public class ApplicationTemp { private volatile Path path; - /** - * Guards access to {@link #path}. - */ - private final Lock pathLock = new ReentrantLock(); - /** * Create a new {@link ApplicationTemp} instance. */ @@ -97,14 +90,10 @@ public File getDir(String subDir) { private Path getPath() { if (this.path == null) { - this.pathLock.lock(); - try { + synchronized (this) { String hash = HexFormat.of().withUpperCase().formatHex(generateHash(this.sourceClass)); this.path = createDirectory(getTempDirectory().resolve(hash)); } - finally { - this.pathLock.unlock(); - } } return this.path; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java index c7661d2e1845..bbfa74b2f6b9 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java @@ -20,8 +20,6 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import org.apache.commons.logging.Log; @@ -59,7 +57,7 @@ public class JettyWebServer implements WebServer { private static final Log logger = LogFactory.getLog(JettyWebServer.class); - private final Lock lock = new ReentrantLock(); + private final Object monitor = new Object(); private final Server server; @@ -115,23 +113,21 @@ private StatisticsHandler findStatisticsHandler(Handler handler) { } private void initialize() { - this.lock.lock(); - try { - // Cache the connectors and then remove them to prevent requests being - // handled before the application context is ready. - this.connectors = this.server.getConnectors(); - JettyWebServer.this.server.setConnectors(null); - // Start the server so that the ServletContext is available - this.server.start(); - this.server.setStopAtShutdown(false); - } - catch (Throwable ex) { - // Ensure process isn't left running - stopSilently(); - throw new WebServerException("Unable to start embedded Jetty web server", ex); - } - finally { - this.lock.unlock(); + synchronized (this.monitor) { + try { + // Cache the connectors and then remove them to prevent requests being + // handled before the application context is ready. + this.connectors = this.server.getConnectors(); + JettyWebServer.this.server.setConnectors(null); + // Start the server so that the ServletContext is available + this.server.start(); + this.server.setStopAtShutdown(false); + } + catch (Throwable ex) { + // Ensure process isn't left running + stopSilently(); + throw new WebServerException("Unable to start embedded Jetty web server", ex); + } } } @@ -146,8 +142,7 @@ private void stopSilently() { @Override public void start() throws WebServerException { - this.lock.lock(); - try { + synchronized (this.monitor) { if (this.started) { return; } @@ -184,9 +179,6 @@ public void start() throws WebServerException { throw new WebServerException("Unable to start embedded Jetty server", ex); } } - finally { - this.lock.unlock(); - } } String getStartedLogMessage() { @@ -249,8 +241,7 @@ else if (handler instanceof HandlerCollection handlerCollection) { @Override public void stop() { - this.lock.lock(); - try { + synchronized (this.monitor) { this.started = false; if (this.gracefulShutdown != null) { this.gracefulShutdown.abort(); @@ -267,22 +258,17 @@ public void stop() { throw new WebServerException("Unable to stop embedded Jetty server", ex); } } - finally { - this.lock.unlock(); - } } @Override public void destroy() { - this.lock.lock(); - try { - this.server.stop(); - } - catch (Exception ex) { - throw new WebServerException("Unable to destroy embedded Jetty server", ex); - } - finally { - this.lock.unlock(); + synchronized (this.monitor) { + try { + this.server.stop(); + } + catch (Exception ex) { + throw new WebServerException("Unable to destroy embedded Jetty server", ex); + } } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java index 8eb9b17dd8e6..9290aeada33d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java @@ -20,8 +20,6 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import javax.naming.NamingException; @@ -62,7 +60,7 @@ public class TomcatWebServer implements WebServer { private static final AtomicInteger containerCounter = new AtomicInteger(-1); - private final Lock lock = new ReentrantLock(); + private final Object monitor = new Object(); private final Map serviceConnectors = new HashMap<>(); @@ -108,43 +106,41 @@ public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) { private void initialize() throws WebServerException { logger.info("Tomcat initialized with " + getPortsDescription(false)); - this.lock.lock(); - try { - addInstanceIdToEngineName(); - - Context context = findContext(); - context.addLifecycleListener((event) -> { - if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) { - // Remove service connectors so that protocol binding doesn't - // happen when the service is started. - removeServiceConnectors(); - } - }); + synchronized (this.monitor) { + try { + addInstanceIdToEngineName(); + + Context context = findContext(); + context.addLifecycleListener((event) -> { + if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) { + // Remove service connectors so that protocol binding doesn't + // happen when the service is started. + removeServiceConnectors(); + } + }); - // Start the server to trigger initialization listeners - this.tomcat.start(); + // Start the server to trigger initialization listeners + this.tomcat.start(); - // We can re-throw failure exception directly in the main thread - rethrowDeferredStartupExceptions(); + // We can re-throw failure exception directly in the main thread + rethrowDeferredStartupExceptions(); - try { - ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader()); + try { + ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader()); + } + catch (NamingException ex) { + // Naming is not enabled. Continue + } + + // Unlike Jetty, all Tomcat threads are daemon threads. We create a + // blocking non-daemon to stop immediate shutdown + startDaemonAwaitThread(); } - catch (NamingException ex) { - // Naming is not enabled. Continue + catch (Exception ex) { + stopSilently(); + destroySilently(); + throw new WebServerException("Unable to start embedded Tomcat", ex); } - - // Unlike Jetty, all Tomcat threads are daemon threads. We create a - // blocking non-daemon to stop immediate shutdown - startDaemonAwaitThread(); - } - catch (Exception ex) { - stopSilently(); - destroySilently(); - throw new WebServerException("Unable to start embedded Tomcat", ex); - } - finally { - this.lock.unlock(); } } @@ -209,8 +205,7 @@ public void run() { @Override public void start() throws WebServerException { - this.lock.lock(); - try { + synchronized (this.monitor) { if (this.started) { return; } @@ -238,9 +233,6 @@ public void start() throws WebServerException { ContextBindings.unbindClassLoader(context, context.getNamingToken(), getClass().getClassLoader()); } } - finally { - this.lock.unlock(); - } } String getStartedLogMessage() { @@ -332,8 +324,7 @@ Map getServiceConnectors() { @Override public void stop() throws WebServerException { - this.lock.lock(); - try { + synchronized (this.monitor) { boolean wasStarted = this.started; try { this.started = false; @@ -351,9 +342,6 @@ public void stop() throws WebServerException { } } } - finally { - this.lock.unlock(); - } } @Override diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java index 0ac3b76ff2fa..bce6152a7e68 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServer.java @@ -25,8 +25,6 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import io.undertow.Undertow; import io.undertow.server.HttpHandler; @@ -65,7 +63,7 @@ public class UndertowWebServer implements WebServer { private final AtomicReference gracefulShutdownCallback = new AtomicReference<>(); - private final Lock lock = new ReentrantLock(); + private final Object monitor = new Object(); private final Undertow.Builder builder; @@ -106,8 +104,7 @@ public UndertowWebServer(Undertow.Builder builder, Iterable @Override public void start() throws WebServerException { - this.lock.lock(); - try { + synchronized (this.monitor) { if (this.started) { return; } @@ -139,9 +136,6 @@ public void start() throws WebServerException { } } } - finally { - this.lock.unlock(); - } } private void destroySilently() { @@ -274,8 +268,7 @@ private UndertowWebServer.Port getPortFromListener(Object listener) { @Override public void stop() throws WebServerException { - this.lock.lock(); - try { + synchronized (this.monitor) { if (!this.started) { return; } @@ -293,9 +286,6 @@ public void stop() throws WebServerException { throw new WebServerException("Unable to stop Undertow", ex); } } - finally { - this.lock.unlock(); - } } @Override From 726d2e66786ad8463cfc54b3782aa58992b35475 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 3 Aug 2023 17:33:52 +0200 Subject: [PATCH 0246/1215] Remove unnecessary synchronization - on AtomicBoolean in SpringApplicationBuilder - on SimpleFormatter - in a private method in FileSystemWatcher which is always called in a synchronized block - Replaced synchronized guarded HashMap with ConcurrentHashMap --- .../devtools/filewatch/FileSystemWatcher.java | 6 ++---- .../boot/devtools/restart/Restarter.java | 17 +++++------------ .../boot/builder/SpringApplicationBuilder.java | 8 +++----- .../boot/logging/java/SimpleFormatter.java | 10 ++++------ 4 files changed, 14 insertions(+), 27 deletions(-) diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcher.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcher.java index 99ef112ea3b5..35a950c6e3b1 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcher.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/filewatch/FileSystemWatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -158,9 +158,7 @@ public void setTriggerFilter(FileFilter triggerFilter) { } private void checkNotStarted() { - synchronized (this.monitor) { - Assert.state(this.watchThread == null, "FileSystemWatcher already started"); - } + Assert.state(this.watchThread == null, "FileSystemWatcher already started"); } /** diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java index bdc69f02d84d..410b269d1d66 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -22,7 +22,6 @@ import java.net.URL; import java.util.Arrays; import java.util.Collection; -import java.util.HashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; @@ -30,6 +29,7 @@ import java.util.Set; import java.util.concurrent.BlockingDeque; import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ThreadFactory; @@ -92,7 +92,7 @@ public class Restarter { private final ClassLoaderFiles classLoaderFiles = new ClassLoaderFiles(); - private final Map attributes = new HashMap<>(); + private final Map attributes = new ConcurrentHashMap<>(); private final BlockingDeque leakSafeThreads = new LinkedBlockingDeque<>(); @@ -440,18 +440,11 @@ private LeakSafeThread getLeakSafeThread() { } public Object getOrAddAttribute(String name, final ObjectFactory objectFactory) { - synchronized (this.attributes) { - if (!this.attributes.containsKey(name)) { - this.attributes.put(name, objectFactory.getObject()); - } - return this.attributes.get(name); - } + return this.attributes.computeIfAbsent(name, (ignore) -> objectFactory.getObject()); } public Object removeAttribute(String name) { - synchronized (this.attributes) { - return this.attributes.remove(name); - } + return this.attributes.remove(name); } /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java index 712caa5e6f15..b479568fcb53 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java @@ -76,7 +76,7 @@ public class SpringApplicationBuilder { private final SpringApplication application; - private ConfigurableApplicationContext context; + private volatile ConfigurableApplicationContext context; private SpringApplicationBuilder parent; @@ -145,10 +145,8 @@ public ConfigurableApplicationContext run(String... args) { } configureAsChildIfNecessary(args); if (this.running.compareAndSet(false, true)) { - synchronized (this.running) { - // If not already running copy the sources over and then run. - this.context = build().run(args); - } + // If not already running copy the sources over and then run. + this.context = build().run(args); } return this.context; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/java/SimpleFormatter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/java/SimpleFormatter.java index 1701a6a03a23..fc9dbfbcaf29 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/java/SimpleFormatter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/java/SimpleFormatter.java @@ -38,17 +38,15 @@ public class SimpleFormatter extends Formatter { private final String pid = getOrUseDefault(LoggingSystemProperty.PID.getEnvironmentVariableName(), "????"); - private final Date date = new Date(); - @Override - public synchronized String format(LogRecord record) { - this.date.setTime(record.getMillis()); + public String format(LogRecord record) { + Date date = new Date(record.getMillis()); String source = record.getLoggerName(); String message = formatMessage(record); String throwable = getThrowable(record); String thread = getThreadName(); - return String.format(this.format, this.date, source, record.getLoggerName(), - record.getLevel().getLocalizedName(), message, throwable, thread, this.pid); + return String.format(this.format, date, source, record.getLoggerName(), record.getLevel().getLocalizedName(), + message, throwable, thread, this.pid); } private String getThrowable(LogRecord record) { From ecdb9f6b071927f3d47974fa7232783ce392621d Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Thu, 3 Aug 2023 15:57:36 -0500 Subject: [PATCH 0247/1215] Add support for CNB platform API 0.12 Closes gh-36712 --- .../boot/buildpack/platform/build/ApiVersions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersions.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersions.java index 10ceba196dba..2b1f474c06f7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersions.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersions.java @@ -31,7 +31,7 @@ final class ApiVersions { /** * The platform API versions supported by this release. */ - static final ApiVersions SUPPORTED_PLATFORMS = ApiVersions.of(0, IntStream.rangeClosed(3, 11)); + static final ApiVersions SUPPORTED_PLATFORMS = ApiVersions.of(0, IntStream.rangeClosed(3, 12)); private final ApiVersion[] apiVersions; From 3a6dbb4cc85e7895a73b4b67f5c44a10f86e5e59 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 4 Aug 2023 14:09:13 +0100 Subject: [PATCH 0248/1215] Upgrade to API Guardian 1.1.2 Closes gh-36742 --- spring-boot-project/spring-boot-parent/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-parent/build.gradle b/spring-boot-project/spring-boot-parent/build.gradle index f5b92ef67469..4924e97bf22f 100644 --- a/spring-boot-project/spring-boot-parent/build.gradle +++ b/spring-boot-project/spring-boot-parent/build.gradle @@ -20,7 +20,7 @@ bom { ] } } - library("API Guardian", "1.1.0") { + library("API Guardian", "1.1.2") { group("org.apiguardian") { modules = [ "apiguardian-api" From e63a30906c8bd189112f1d9f42a616f794dbfee8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 4 Aug 2023 14:09:18 +0100 Subject: [PATCH 0249/1215] Upgrade to Commons Compress 1.23.0 Closes gh-36743 --- spring-boot-project/spring-boot-parent/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-parent/build.gradle b/spring-boot-project/spring-boot-parent/build.gradle index 4924e97bf22f..4b79ac504981 100644 --- a/spring-boot-project/spring-boot-parent/build.gradle +++ b/spring-boot-project/spring-boot-parent/build.gradle @@ -34,7 +34,7 @@ bom { ] } } - library("Commons Compress", "1.21") { + library("Commons Compress", "1.23.0") { group("org.apache.commons") { modules = [ "commons-compress" From bc6bf24df8478db9f874d1b4aa2493b519558f78 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 4 Aug 2023 14:09:23 +0100 Subject: [PATCH 0250/1215] Upgrade to Commons FileUpload 1.5 Closes gh-36744 --- spring-boot-project/spring-boot-parent/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-parent/build.gradle b/spring-boot-project/spring-boot-parent/build.gradle index 4b79ac504981..39e3e2604a47 100644 --- a/spring-boot-project/spring-boot-parent/build.gradle +++ b/spring-boot-project/spring-boot-parent/build.gradle @@ -41,7 +41,7 @@ bom { ] } } - library("Commons FileUpload", "1.4") { + library("Commons FileUpload", "1.5") { group("commons-fileupload") { modules = [ "commons-fileupload" From faef25357fab4370ea78a7d98e073398b31d1cfa Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 4 Aug 2023 14:09:27 +0100 Subject: [PATCH 0251/1215] Upgrade to Janino 3.1.10 Closes gh-36745 --- spring-boot-project/spring-boot-parent/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-parent/build.gradle b/spring-boot-project/spring-boot-parent/build.gradle index 39e3e2604a47..6e31cde29bc3 100644 --- a/spring-boot-project/spring-boot-parent/build.gradle +++ b/spring-boot-project/spring-boot-parent/build.gradle @@ -55,7 +55,7 @@ bom { ] } } - library("Janino", "3.1.8") { + library("Janino", "3.1.10") { group("org.codehaus.janino") { imports = [ "janino" From f32e27f92e446f357e9f13b75e6a18b3f7989e0c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 4 Aug 2023 14:09:32 +0100 Subject: [PATCH 0252/1215] Upgrade to JNA 5.13.0 Closes gh-36746 --- spring-boot-project/spring-boot-parent/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-parent/build.gradle b/spring-boot-project/spring-boot-parent/build.gradle index 6e31cde29bc3..8311ef65d0d6 100644 --- a/spring-boot-project/spring-boot-parent/build.gradle +++ b/spring-boot-project/spring-boot-parent/build.gradle @@ -73,7 +73,7 @@ bom { ] } } - library("JNA", "5.7.0") { + library("JNA", "5.13.0") { group("net.java.dev.jna") { modules = [ "jna-platform" From 0c7c7ac8a923c727f6a8a379c37e60c76ad68c5a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 4 Aug 2023 14:09:37 +0100 Subject: [PATCH 0253/1215] Upgrade to Maven 3.9.4 Closes gh-36747 --- spring-boot-project/spring-boot-parent/build.gradle | 7 ++++--- .../spring-boot-maven-plugin/build.gradle | 7 +++++++ .../src/intTest/projects/settings.xml | 2 ++ .../org/springframework/boot/maven/ProcessTestAotMojo.java | 7 ++++--- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/spring-boot-project/spring-boot-parent/build.gradle b/spring-boot-project/spring-boot-parent/build.gradle index 8311ef65d0d6..e2920a4944fc 100644 --- a/spring-boot-project/spring-boot-parent/build.gradle +++ b/spring-boot-project/spring-boot-parent/build.gradle @@ -87,12 +87,13 @@ bom { ] } } - library("Maven", "3.6.3") { + library("Maven", "3.9.4") { group("org.apache.maven") { modules = [ + "maven-core", + "maven-model-builder", "maven-plugin-api", - "maven-resolver-provider", - "maven-settings-builder" + "maven-resolver-provider" ] } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle index fc4022c97070..e659d84d3148 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle @@ -16,10 +16,16 @@ dependencies { compileOnly("org.apache.maven.plugin-tools:maven-plugin-annotations") compileOnly("org.sonatype.plexus:plexus-build-api") compileOnly("org.apache.maven.shared:maven-common-artifact-filters") { + exclude(group: "javax.annotation", module: "javax.annotation-api") exclude(group: "javax.enterprise", module: "cdi-api") exclude(group: "javax.inject", module: "javax.inject") } + compileOnly("org.apache.maven:maven-core") { + exclude(group: "javax.annotation", module: "javax.annotation-api") + exclude(group: "javax.inject", module: "javax.inject") + } compileOnly("org.apache.maven:maven-plugin-api") { + exclude(group: "javax.annotation", module: "javax.annotation-api") exclude(group: "javax.enterprise", module: "cdi-api") exclude(group: "javax.inject", module: "javax.inject") } @@ -40,6 +46,7 @@ dependencies { intTestImplementation("org.testcontainers:junit-jupiter") mavenOptionalImplementation("org.apache.maven.plugins:maven-shade-plugin") { + exclude(group: "javax.annotation", module: "javax.annotation-api") exclude(group: "javax.enterprise", module: "cdi-api") exclude(group: "javax.inject", module: "javax.inject") } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/settings.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/settings.xml index d63e2d6b8d06..306a5fc6e054 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/settings.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/settings.xml @@ -17,6 +17,7 @@ true + ignore spring-milestones @@ -42,6 +43,7 @@ true + ignore diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ProcessTestAotMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ProcessTestAotMojo.java index a1b93ac3c259..9729c92bc550 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ProcessTestAotMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ProcessTestAotMojo.java @@ -30,7 +30,6 @@ import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.DefaultArtifact; import org.apache.maven.artifact.handler.DefaultArtifactHandler; -import org.apache.maven.artifact.repository.ArtifactRepository; import org.apache.maven.artifact.resolver.ArtifactResolutionRequest; import org.apache.maven.artifact.resolver.ArtifactResolutionResult; import org.apache.maven.artifact.resolver.ResolutionErrorHandler; @@ -101,14 +100,16 @@ public class ProcessTestAotMojo extends AbstractAotMojo { /** * Local artifact repository used to resolve JUnit platform launcher jars. */ + @SuppressWarnings("deprecation") @Parameter(defaultValue = "${localRepository}", required = true, readonly = true) - private ArtifactRepository localRepository; + private org.apache.maven.artifact.repository.ArtifactRepository localRepository; /** * Remote artifact repositories used to resolve JUnit platform launcher jars. */ + @SuppressWarnings("deprecation") @Parameter(defaultValue = "${project.remoteArtifactRepositories}", required = true, readonly = true) - private List remoteRepositories; + private List remoteRepositories; @Component private RepositorySystem repositorySystem; From b6db68bb9bcaeac268c7220ff3b6c43153a4ce21 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 4 Aug 2023 14:09:42 +0100 Subject: [PATCH 0254/1215] Upgrade to Maven Common Artifact Filters 3.3.2 Closes gh-36748 --- spring-boot-project/spring-boot-parent/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-parent/build.gradle b/spring-boot-project/spring-boot-parent/build.gradle index e2920a4944fc..ea6abb79c6d0 100644 --- a/spring-boot-project/spring-boot-parent/build.gradle +++ b/spring-boot-project/spring-boot-parent/build.gradle @@ -97,7 +97,7 @@ bom { ] } } - library("Maven Common Artifact Filters", "3.2.0") { + library("Maven Common Artifact Filters", "3.3.2") { group("org.apache.maven.shared") { modules = [ "maven-common-artifact-filters" From 7b70d991444b6df64016e22edfd1dc8b592d75fe Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 4 Aug 2023 14:09:47 +0100 Subject: [PATCH 0255/1215] Upgrade to Maven Invoker 3.2.0 Closes gh-36749 --- spring-boot-project/spring-boot-parent/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-parent/build.gradle b/spring-boot-project/spring-boot-parent/build.gradle index ea6abb79c6d0..bc45678286e1 100644 --- a/spring-boot-project/spring-boot-parent/build.gradle +++ b/spring-boot-project/spring-boot-parent/build.gradle @@ -104,7 +104,7 @@ bom { ] } } - library("Maven Invoker", "3.1.0") { + library("Maven Invoker", "3.2.0") { group("org.apache.maven.shared") { modules = [ "maven-invoker" From ef79d88acfabcd1907d8c746f3ac73f1af58fae3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 4 Aug 2023 14:09:52 +0100 Subject: [PATCH 0256/1215] Upgrade to Maven Plugin Tools 3.9.0 Closes gh-36750 --- spring-boot-project/spring-boot-parent/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-parent/build.gradle b/spring-boot-project/spring-boot-parent/build.gradle index bc45678286e1..dadf2242b7bf 100644 --- a/spring-boot-project/spring-boot-parent/build.gradle +++ b/spring-boot-project/spring-boot-parent/build.gradle @@ -111,7 +111,7 @@ bom { ] } } - library("Maven Plugin Tools", "3.6.0") { + library("Maven Plugin Tools", "3.9.0") { group("org.apache.maven.plugin-tools") { modules = [ "maven-plugin-annotations" From 1368593199abb5c836fa2481c23ada27853f90f5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 4 Aug 2023 14:09:57 +0100 Subject: [PATCH 0257/1215] Upgrade to Maven Resolver 1.9.14 Closes gh-36751 --- .../spring-boot-parent/build.gradle | 2 +- .../classpath/ModifiedClassPathClassLoader.java | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/spring-boot-project/spring-boot-parent/build.gradle b/spring-boot-project/spring-boot-parent/build.gradle index dadf2242b7bf..73fe311a14ce 100644 --- a/spring-boot-project/spring-boot-parent/build.gradle +++ b/spring-boot-project/spring-boot-parent/build.gradle @@ -118,7 +118,7 @@ bom { ] } } - library("Maven Resolver", "1.6.3") { + library("Maven Resolver", "1.9.14") { group("org.apache.maven.resolver") { modules = [ "maven-resolver-api", diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathClassLoader.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathClassLoader.java index 80438116f2d3..f05fa664d6f0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathClassLoader.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathClassLoader.java @@ -43,7 +43,6 @@ import org.eclipse.aether.collection.CollectRequest; import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory; import org.eclipse.aether.graph.Dependency; -import org.eclipse.aether.impl.DefaultServiceLocator; import org.eclipse.aether.repository.LocalRepository; import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.resolution.ArtifactResult; @@ -240,11 +239,9 @@ private static List getAdditionalUrls(List annotations) private static List resolveCoordinates(String[] coordinates) { Exception latestFailure = null; - DefaultServiceLocator serviceLocator = MavenRepositorySystemUtils.newServiceLocator(); - serviceLocator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class); - serviceLocator.addService(TransporterFactory.class, HttpTransporterFactory.class); - RepositorySystem repositorySystem = serviceLocator.getService(RepositorySystem.class); + RepositorySystem repositorySystem = createRepositorySystem(); DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession(); + session.setSystemProperties(System.getProperties()); LocalRepository localRepository = new LocalRepository(System.getProperty("user.home") + "/.m2/repository"); RemoteRepository remoteRepository = new RemoteRepository.Builder("central", "default", "https://repo.maven.apache.org/maven2") @@ -270,6 +267,15 @@ private static List resolveCoordinates(String[] coordinates) { latestFailure); } + @SuppressWarnings("deprecation") + private static RepositorySystem createRepositorySystem() { + org.eclipse.aether.impl.DefaultServiceLocator serviceLocator = MavenRepositorySystemUtils.newServiceLocator(); + serviceLocator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class); + serviceLocator.addService(TransporterFactory.class, HttpTransporterFactory.class); + RepositorySystem repositorySystem = serviceLocator.getService(RepositorySystem.class); + return repositorySystem; + } + private static List createDependencies(String[] allCoordinates) { List dependencies = new ArrayList<>(); for (String coordinate : allCoordinates) { From 579aac055dad073e11325a95828efe9f35ed2ff3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 4 Aug 2023 14:10:02 +0100 Subject: [PATCH 0258/1215] Upgrade to Maven Shade Plugin 3.5.0 Closes gh-36752 --- .../spring-boot-parent/build.gradle | 2 +- .../spring-boot-maven-plugin/build.gradle | 21 +++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/spring-boot-project/spring-boot-parent/build.gradle b/spring-boot-project/spring-boot-parent/build.gradle index 73fe311a14ce..b1b751432ec1 100644 --- a/spring-boot-project/spring-boot-parent/build.gradle +++ b/spring-boot-project/spring-boot-parent/build.gradle @@ -131,7 +131,7 @@ bom { ] } } - library("Maven Shade Plugin", "3.2.4") { + library("Maven Shade Plugin", "3.5.0") { group("org.apache.maven.plugins") { modules = [ "maven-shade-plugin" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle index e659d84d3148..e85537a99c0c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle @@ -15,11 +15,6 @@ configurations { dependencies { compileOnly("org.apache.maven.plugin-tools:maven-plugin-annotations") compileOnly("org.sonatype.plexus:plexus-build-api") - compileOnly("org.apache.maven.shared:maven-common-artifact-filters") { - exclude(group: "javax.annotation", module: "javax.annotation-api") - exclude(group: "javax.enterprise", module: "cdi-api") - exclude(group: "javax.inject", module: "javax.inject") - } compileOnly("org.apache.maven:maven-core") { exclude(group: "javax.annotation", module: "javax.annotation-api") exclude(group: "javax.inject", module: "javax.inject") @@ -30,9 +25,14 @@ dependencies { exclude(group: "javax.inject", module: "javax.inject") } - implementation("org.springframework:spring-context") implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-buildpack-platform")) implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-tools")) + implementation("org.apache.maven.shared:maven-common-artifact-filters") { + exclude(group: "javax.annotation", module: "javax.annotation-api") + exclude(group: "javax.enterprise", module: "cdi-api") + exclude(group: "javax.inject", module: "javax.inject") + } + implementation("org.springframework:spring-context") intTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-buildpack-platform")) intTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-tools")) @@ -58,6 +58,15 @@ dependencies { runtimeOnly("org.sonatype.plexus:plexus-build-api") + testImplementation("org.apache.maven:maven-core") { + exclude(group: "javax.annotation", module: "javax.annotation-api") + exclude(group: "javax.inject", module: "javax.inject") + } + testImplementation("org.apache.maven.shared:maven-common-artifact-filters") { + exclude(group: "javax.annotation", module: "javax.annotation-api") + exclude(group: "javax.enterprise", module: "cdi-api") + exclude(group: "javax.inject", module: "javax.inject") + } testImplementation("org.assertj:assertj-core") testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.mockito:mockito-core") From 0c36f1f26f19954f76dadb2c85de8759fd46d390 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 4 Aug 2023 14:10:07 +0100 Subject: [PATCH 0259/1215] Upgrade to MockK 1.13.5 Closes gh-36753 --- spring-boot-project/spring-boot-parent/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-parent/build.gradle b/spring-boot-project/spring-boot-parent/build.gradle index b1b751432ec1..e7e8f715236e 100644 --- a/spring-boot-project/spring-boot-parent/build.gradle +++ b/spring-boot-project/spring-boot-parent/build.gradle @@ -138,7 +138,7 @@ bom { ] } } - library("MockK", "1.10.6") { + library("MockK", "1.13.5") { group("io.mockk") { modules = [ "mockk" From 9d42cff472c7d67972036d60ed2b1e64b41ee253 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 4 Aug 2023 14:10:12 +0100 Subject: [PATCH 0260/1215] Upgrade to Spock Framework 2.3-groovy-4.0 Closes gh-36754 --- spring-boot-project/spring-boot-parent/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-parent/build.gradle b/spring-boot-project/spring-boot-parent/build.gradle index e7e8f715236e..a90f00a6960a 100644 --- a/spring-boot-project/spring-boot-parent/build.gradle +++ b/spring-boot-project/spring-boot-parent/build.gradle @@ -180,7 +180,7 @@ bom { ] } } - library("Spock Framework", "2.2-M1-groovy-4.0") { + library("Spock Framework", "2.3-groovy-4.0") { group("org.spockframework") { imports = [ "spock-bom" From b1d26fe9615240e1e762814303e2958b38b858db Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 7 Aug 2023 12:10:27 +0100 Subject: [PATCH 0261/1215] Revert "Upgrade default CNB builders to Paketo Jammy" This reverts commit 6506208d291e657e1ec5c4948d8a2054995b8b7f. The upgrade to the Jammy builder was causing failsWhenBuildImageIsInvokedOnMultiModuleProjectWithBuildImageGoal to hang on CI. See gh-36689 --- ci/pipeline.yml | 3 ++- .../native-image/developing-your-first-application.adoc | 2 +- .../spring-boot-starter-parent/build.gradle | 2 +- .../boot/buildpack/platform/build/BuildRequest.java | 2 +- .../src/docs/asciidoc/packaging-oci-image.adoc | 2 +- .../spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc | 2 +- .../boot/gradle/plugin/NativeImagePluginAction.java | 2 +- .../gradle/plugin/NativeImagePluginActionIntegrationTests.java | 3 +-- .../boot/gradle/tasks/bundling/BootBuildImageTests.java | 3 +-- .../src/docs/asciidoc/packaging-oci-image.adoc | 2 +- .../test/java/org/springframework/boot/maven/ImageTests.java | 2 +- 11 files changed, 12 insertions(+), 13 deletions(-) diff --git a/ci/pipeline.yml b/ci/pipeline.yml index 4cdfaf312378..ad367a8ad435 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -185,7 +185,8 @@ resources: type: registry-image icon: docker source: - repository: paketobuildpacks/builder-jammy-base + repository: paketobuildpacks/builder + tag: base - name: artifactory-repo type: artifactory-resource icon: package-variant diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/native-image/developing-your-first-application.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/native-image/developing-your-first-application.adoc index a1e47a36b742..b4e70ac86ce5 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/native-image/developing-your-first-application.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/native-image/developing-your-first-application.adoc @@ -32,7 +32,7 @@ This means you can just type a single command and quickly get a sensible image i The resulting image doesn't contain a JVM, instead the native image is compiled statically. This leads to smaller images. -NOTE: The builder used for the images is `paketobuildpacks/builder-jammy-tiny`. +NOTE: The builder used for the images is `paketobuildpacks/builder:tiny`. It has small footprint and reduced attack surface, but you can also use `paketobuildpacks/builder-jammy-base` or `paketobuildpacks/builder-jammy-full` to have more tools available in the image if required. diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle index 713f3ddd7826..2b2028ea2cad 100644 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle @@ -249,7 +249,7 @@ publishing.publications.withType(MavenPublication) { delegate.artifactId('spring-boot-maven-plugin') configuration { image { - delegate.builder("paketobuildpacks/builder-jammy-tiny"); + delegate.builder("paketobuildpacks/builder:tiny"); env { delegate.BP_NATIVE_IMAGE("true") } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java index 5596b0badba3..0bb75fe17e0e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java @@ -45,7 +45,7 @@ */ public class BuildRequest { - static final String DEFAULT_BUILDER_IMAGE_NAME = "paketobuildpacks/builder-jammy-base"; + static final String DEFAULT_BUILDER_IMAGE_NAME = "paketobuildpacks/builder:base"; private static final ImageReference DEFAULT_BUILDER = ImageReference.of(DEFAULT_BUILDER_IMAGE_NAME); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc index b5df977ed59e..824c79ee0049 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -115,7 +115,7 @@ The following table summarizes the available properties and their default values | `builder` | `--builder` | Name of the Builder image to use. -| `paketobuildpacks/builder-jammy-base` or `paketobuildpacks/builder-jammy-tiny` when {nbt-gradle-plugin}[GraalVM Native Image plugin] is applied. +| `paketobuildpacks/builder:base` or `paketobuildpacks/builder:tiny` when {nbt-gradle-plugin}[GraalVM Native Image plugin] is applied. | `runImage` | `--runImage` diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc index 468002d26c98..660404b1a426 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc @@ -81,6 +81,6 @@ When the {nbt-gradle-plugin}[GraalVM Native Image plugin] is applied to a projec . Configures the GraalVM extension to disable Toolchain detection. . Configures each GraalVM native binary to require GraalVM 22.3 or later. . Configures the `bootJar` task to include the reachability metadata produced by the `collectReachabilityMetadata` task in its jar. -. Configures the `bootBuildImage` task to use `paketobuildpacks/builder-jammy-tiny` as its builder and to set `BP_NATIVE_IMAGE` to `true` in its environment. +. Configures the `bootBuildImage` task to use `paketobuildpacks/builder:tiny` as its builder and to set `BP_NATIVE_IMAGE` to `true` in its environment. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java index fa0b967e6132..3cc2bf7b2b2d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java @@ -115,7 +115,7 @@ private void configureBootBuildImageToProduceANativeImage(Project project) { project.getTasks() .named(SpringBootPlugin.BOOT_BUILD_IMAGE_TASK_NAME, BootBuildImage.class) .configure((bootBuildImage) -> { - bootBuildImage.getBuilder().convention("paketobuildpacks/builder-jammy-tiny"); + bootBuildImage.getBuilder().convention("paketobuildpacks/builder:tiny"); bootBuildImage.getEnvironment().put("BP_NATIVE_IMAGE", "true"); }); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java index 4d15614a7057..08c094e2efb6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java @@ -93,8 +93,7 @@ void bootBuildImageIsConfiguredToBuildANativeImage() { writeDummySpringApplicationAotProcessorMainClass(); BuildResult result = this.gradleBuild.expectDeprecationWarningsWithAtLeastVersion("8.2-rc-1") .build("bootBuildImageConfiguration"); - assertThat(result.getOutput()).contains("paketobuildpacks/builder-jammy-tiny") - .contains("BP_NATIVE_IMAGE = true"); + assertThat(result.getOutput()).contains("paketobuildpacks/builder:tiny").contains("BP_NATIVE_IMAGE = true"); } @TestTemplate diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java index 33dac8a784e4..fdb69d2a532d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java @@ -171,8 +171,7 @@ void whenUsingDefaultConfigurationThenRequestHasPublishDisabled() { @Test void whenNoBuilderIsConfiguredThenRequestHasDefaultBuilder() { - assertThat(this.buildImage.createRequest().getBuilder().getName()) - .isEqualTo("paketobuildpacks/builder-jammy-base"); + assertThat(this.buildImage.createRequest().getBuilder().getName()).isEqualTo("paketobuildpacks/builder"); } @Test diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc index 7f76bfde2397..09e85abb8209 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -131,7 +131,7 @@ The following table summarizes the available parameters and their default values | `builder` + (`spring-boot.build-image.builder`) | Name of the Builder image to use. -| `paketobuildpacks/builder-jammy-base` +| `paketobuildpacks/builder:base` | `runImage` + (`spring-boot.build-image.runImage`) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java index 86625106bdbe..8f3558701c46 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java @@ -67,7 +67,7 @@ void getBuildRequestWhenNameIsSetUsesName() { void getBuildRequestWhenNoCustomizationsUsesDefaults() { BuildRequest request = new Image().getBuildRequest(createArtifact(), mockApplicationContent()); assertThat(request.getName()).hasToString("docker.io/library/my-app:0.0.1-SNAPSHOT"); - assertThat(request.getBuilder().toString()).contains("paketobuildpacks/builder-jammy-base"); + assertThat(request.getBuilder().toString()).contains("paketobuildpacks/builder"); assertThat(request.getRunImage()).isNull(); assertThat(request.getEnv()).isEmpty(); assertThat(request.isCleanCache()).isFalse(); From 3441833c5c503393f9f7e8e563f9477fb1ea8b39 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 7 Aug 2023 13:41:47 +0100 Subject: [PATCH 0262/1215] Add missing Details class --- .../io/restclient/restclient/ssl/Details.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/ssl/Details.java diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/ssl/Details.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/ssl/Details.java new file mode 100644 index 000000000000..eb853cba5e36 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/io/restclient/restclient/ssl/Details.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2023 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.docs.io.restclient.restclient.ssl; + +public class Details { + +} From ed9169501efb278942f7e6016cc235e1979f38c0 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 8 Aug 2023 09:27:45 +0200 Subject: [PATCH 0263/1215] Polish --- .../ReactiveCloudFoundrySecurityService.java | 5 +---- .../servlet/CloudFoundrySecurityInterceptor.java | 4 ++-- .../condition/OnAvailableEndpointCondition.java | 7 ++----- .../expose/IncludeExcludeEndpointFilter.java | 2 +- .../autoconfigure/web/ManagementContextFactory.java | 4 ++-- .../server/ChildManagementContextInitializer.java | 4 ++-- .../actuate/jdbc/DataSourceHealthIndicator.java | 4 ++-- .../boot/actuate/context/ShutdownEndpointTests.java | 2 +- .../autoconfigure/batch/BatchAutoConfiguration.java | 2 +- .../data/couchbase/CouchbaseDataConfiguration.java | 2 +- .../logging/ConditionEvaluationReportLogger.java | 4 ++-- .../remote/client/ClassPathChangeUploader.java | 7 +++---- .../boot/test/context/SpringBootContextLoader.java | 5 ++--- .../SimpleConfigurationMetadataRepository.java | 2 +- .../PropertyDescriptorResolver.java | 2 +- .../boot/gradle/plugin/NativeImagePluginAction.java | 3 +-- .../boot/jarmode/layertools/HelpCommand.java | 13 ++++++------- .../boot/jarmode/layertools/HelpCommandTests.java | 6 +++--- .../boot/maven/CustomLayersProvider.java | 2 +- .../boot/context/config/ConfigDataLoaders.java | 7 +++---- ...NotConstructorBoundInjectionFailureAnalyzer.java | 4 ++-- .../bind/DefaultBindConstructorProvider.java | 4 ++-- .../BeanDefinitionOverrideFailureAnalyzer.java | 6 +++--- ...usiveConfigurationPropertiesFailureAnalyzer.java | 2 +- 24 files changed, 46 insertions(+), 57 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityService.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityService.java index 4add4c094315..d75725e68d6f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityService.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityService.java @@ -53,8 +53,6 @@ class ReactiveCloudFoundrySecurityService { private final String cloudControllerUrl; - private Mono uaaUrl; - ReactiveCloudFoundrySecurityService(WebClient.Builder webClientBuilder, String cloudControllerUrl, boolean skipSslValidation) { Assert.notNull(webClientBuilder, "WebClient must not be null"); @@ -149,7 +147,7 @@ private Map extractTokenKeys(Map response) { * @return the UAA url Mono */ Mono getUaaUrl() { - this.uaaUrl = this.webClient.get() + return this.webClient.get() .uri(this.cloudControllerUrl + "/info") .retrieve() .bodyToMono(Map.class) @@ -157,7 +155,6 @@ Mono getUaaUrl() { .cache() .onErrorMap((ex) -> new CloudFoundryAuthorizationException(Reason.SERVICE_UNAVAILABLE, "Unable to fetch token keys from UAA.")); - return this.uaaUrl; } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityInterceptor.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityInterceptor.java index 01eafcf65125..c5c4b2c8e4d2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityInterceptor.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundrySecurityInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -86,7 +86,7 @@ SecurityResponse preHandle(HttpServletRequest request, EndpointId endpointId) { return SecurityResponse.success(); } - private void check(HttpServletRequest request, EndpointId endpointId) throws Exception { + private void check(HttpServletRequest request, EndpointId endpointId) { Token token = getToken(request); this.tokenValidator.validate(token); AccessLevel accessLevel = this.cloudFoundrySecurityService.getAccessLevel(token.toString(), this.applicationId); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnAvailableEndpointCondition.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnAvailableEndpointCondition.java index 4478b0ed9426..a485aa2a4a10 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnAvailableEndpointCondition.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/condition/OnAvailableEndpointCondition.java @@ -143,11 +143,8 @@ private ConditionOutcome getEnablementOutcome(Environment environment, } private Boolean isEnabledByDefault(Environment environment) { - Optional enabledByDefault = enabledByDefaultCache.get(environment); - if (enabledByDefault == null) { - enabledByDefault = Optional.ofNullable(environment.getProperty(ENABLED_BY_DEFAULT_KEY, Boolean.class)); - enabledByDefaultCache.put(environment, enabledByDefault); - } + Optional enabledByDefault = enabledByDefaultCache.computeIfAbsent(environment, + (ignore) -> Optional.ofNullable(environment.getProperty(ENABLED_BY_DEFAULT_KEY, Boolean.class))); return enabledByDefault.orElse(null); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/IncludeExcludeEndpointFilter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/IncludeExcludeEndpointFilter.java index d2c738b5e2ec..00affa4cfff3 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/IncludeExcludeEndpointFilter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/expose/IncludeExcludeEndpointFilter.java @@ -150,7 +150,7 @@ private static class EndpointPatterns { private final Set endpointIds; EndpointPatterns(String[] patterns) { - this((patterns != null) ? Arrays.asList(patterns) : (Collection) null); + this((patterns != null) ? Arrays.asList(patterns) : null); } EndpointPatterns(Collection patterns) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/ManagementContextFactory.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/ManagementContextFactory.java index 0871218563fb..4b98cdfc51a7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/ManagementContextFactory.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/ManagementContextFactory.java @@ -60,8 +60,8 @@ public ConfigurableApplicationContext createManagementContext(ApplicationContext Environment parentEnvironment = parentContext.getEnvironment(); ConfigurableEnvironment childEnvironment = ApplicationContextFactory.DEFAULT .createEnvironment(this.webApplicationType); - if (parentEnvironment instanceof ConfigurableEnvironment) { - childEnvironment.setConversionService(((ConfigurableEnvironment) parentEnvironment).getConversionService()); + if (parentEnvironment instanceof ConfigurableEnvironment configurableEnvironment) { + childEnvironment.setConversionService((configurableEnvironment).getConversionService()); } ConfigurableApplicationContext managementContext = ApplicationContextFactory.DEFAULT .create(this.webApplicationType); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ChildManagementContextInitializer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ChildManagementContextInitializer.java index a4d423dfce39..38a6a68f36e5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ChildManagementContextInitializer.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ChildManagementContextInitializer.java @@ -217,8 +217,8 @@ private void propagateCloseIfNecessary(ApplicationContext applicationContext) { } static void addIfPossible(ApplicationContext parentContext, ConfigurableApplicationContext childContext) { - if (parentContext instanceof ConfigurableApplicationContext) { - add((ConfigurableApplicationContext) parentContext, childContext); + if (parentContext instanceof ConfigurableApplicationContext configurableApplicationContext) { + add(configurableApplicationContext, childContext); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jdbc/DataSourceHealthIndicator.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jdbc/DataSourceHealthIndicator.java index b92c7398a67d..caef14b9bcbd 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jdbc/DataSourceHealthIndicator.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/jdbc/DataSourceHealthIndicator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. @@ -101,7 +101,7 @@ protected void doHealthCheck(Health.Builder builder) throws Exception { } } - private void doDataSourceHealthCheck(Health.Builder builder) throws Exception { + private void doDataSourceHealthCheck(Health.Builder builder) { builder.up().withDetail("database", getProduct()); String validationQuery = this.query; if (StringUtils.hasText(validationQuery)) { diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/ShutdownEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/ShutdownEndpointTests.java index 45b37c3e6fb3..018dd3a9059c 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/ShutdownEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/ShutdownEndpointTests.java @@ -60,7 +60,7 @@ void shutdown() { Thread.currentThread().setContextClassLoader(previousTccl); } assertThat(result.getMessage()).startsWith("Shutting down"); - assertThat(((ConfigurableApplicationContext) context).isActive()).isTrue(); + assertThat(context.isActive()).isTrue(); assertThat(config.latch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(config.threadContextClassLoader).isEqualTo(getClass().getClassLoader()); }); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java index 3c050a0856bc..0474d2d0afe3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java @@ -99,7 +99,7 @@ public JobExecutionExitCodeGenerator jobExecutionExitCodeGenerator() { @ConditionalOnMissingBean(JobOperator.class) public SimpleJobOperator jobOperator(ObjectProvider jobParametersConverter, JobExplorer jobExplorer, JobLauncher jobLauncher, ListableJobLocator jobRegistry, - JobRepository jobRepository) throws Exception { + JobRepository jobRepository) { SimpleJobOperator factory = new SimpleJobOperator(); factory.setJobExplorer(jobExplorer); factory.setJobLauncher(jobLauncher); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseDataConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseDataConfiguration.java index 20826c607ba9..1b408f876620 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseDataConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/couchbase/CouchbaseDataConfiguration.java @@ -61,7 +61,7 @@ TranslationService couchbaseTranslationService() { @ConditionalOnMissingBean(name = BeanNames.COUCHBASE_MAPPING_CONTEXT) CouchbaseMappingContext couchbaseMappingContext(CouchbaseDataProperties properties, ApplicationContext applicationContext, CouchbaseCustomConversions couchbaseCustomConversions) - throws Exception { + throws ClassNotFoundException { CouchbaseMappingContext mappingContext = new CouchbaseMappingContext(); mappingContext.setInitialEntitySet(new EntityScanner(applicationContext).scan(Document.class)); mappingContext.setSimpleTypeHolder(couchbaseCustomConversions.getSimpleTypeHolder()); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/logging/ConditionEvaluationReportLogger.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/logging/ConditionEvaluationReportLogger.java index 49a3036fd67d..1af820991ab5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/logging/ConditionEvaluationReportLogger.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/logging/ConditionEvaluationReportLogger.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -80,7 +80,7 @@ else if (isCrashReport) { private void logMessage(String logLevel) { this.logger.info(String.format("%n%nError starting ApplicationContext. To display the " - + "condition evaluation report re-run your application with '" + logLevel + "' enabled.")); + + "condition evaluation report re-run your application with '%s' enabled.", logLevel)); } } diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/ClassPathChangeUploader.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/ClassPathChangeUploader.java index 8cfe3144db9f..44bf7d24d58d 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/ClassPathChangeUploader.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/remote/client/ClassPathChangeUploader.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -92,15 +92,14 @@ public void onApplicationEvent(ClassPathChangedEvent event) { try { ClassLoaderFiles classLoaderFiles = getClassLoaderFiles(event); byte[] bytes = serialize(classLoaderFiles); - performUpload(classLoaderFiles, bytes, event); + performUpload(bytes, event); } catch (IOException ex) { throw new IllegalStateException(ex); } } - private void performUpload(ClassLoaderFiles classLoaderFiles, byte[] bytes, ClassPathChangedEvent event) - throws IOException { + private void performUpload(byte[] bytes, ClassPathChangedEvent event) throws IOException { try { while (true) { try { diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java index 1e673f8f2b49..a07e13d1c09e 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java @@ -187,7 +187,7 @@ private void configure(MergedContextConfiguration mergedConfig, SpringApplicatio if (mergedConfig instanceof WebMergedContextConfiguration) { application.setWebApplicationType(WebApplicationType.SERVLET); if (!isEmbeddedWebEnvironment(mergedConfig)) { - new WebConfigurer().configure(mergedConfig, application, initializers); + new WebConfigurer().configure(mergedConfig, initializers); } } else if (mergedConfig instanceof ReactiveWebMergedContextConfiguration) { @@ -374,8 +374,7 @@ private enum Mode { */ private static class WebConfigurer { - void configure(MergedContextConfiguration mergedConfig, SpringApplication application, - List> initializers) { + void configure(MergedContextConfiguration mergedConfig, List> initializers) { WebMergedContextConfiguration webMergedConfig = (WebMergedContextConfiguration) mergedConfig; addMockServletContext(initializers, webMergedConfig); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/SimpleConfigurationMetadataRepository.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/SimpleConfigurationMetadataRepository.java index bba6c2562787..bdfc91cb20d0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/SimpleConfigurationMetadataRepository.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/SimpleConfigurationMetadataRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java index e71fb074eb20..c4808a93a71f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java @@ -204,7 +204,7 @@ private static ExecutableElement deduceBindConstructor(TypeElement type, List 0 && !env.hasAutowiredAnnotation(candidate)) { + if (!candidate.getParameters().isEmpty() && !env.hasAutowiredAnnotation(candidate)) { if (type.getNestingKind() == NestingKind.MEMBER && candidate.getModifiers().contains(Modifier.PRIVATE)) { return null; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java index 3cc2bf7b2b2d..061d073e61e3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java @@ -47,8 +47,7 @@ class NativeImagePluginAction implements PluginApplicationAction { @Override - public Class> getPluginClass() - throws ClassNotFoundException, NoClassDefFoundError { + public Class> getPluginClass() { return NativeImagePlugin.class; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/HelpCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/HelpCommand.java index 70f5d385c3fb..4f25c80dc259 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/HelpCommand.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/HelpCommand.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -40,10 +40,10 @@ class HelpCommand extends Command { @Override protected void run(Map options, List parameters) { - run(System.out, options, parameters); + run(System.out, parameters); } - void run(PrintStream out, Map options, List parameters) { + void run(PrintStream out, List parameters) { Command command = (!parameters.isEmpty()) ? Command.find(this.commands, parameters.get(0)) : null; if (command != null) { printCommandHelp(out, command); @@ -66,8 +66,7 @@ private void printCommandHelp(PrintStream out, Command command) { } private void printOptionSummary(PrintStream out, Option option, int padding) { - out.println(String.format(" --%-" + padding + "s %s", option.getNameAndValueDescription(), - option.getDescription())); + out.printf(" --%-" + padding + "s %s%n", option.getNameAndValueDescription(), option.getDescription()); } private String getUsage(Command command) { @@ -76,7 +75,7 @@ private String getUsage(Command command) { if (!command.getOptions().isEmpty()) { usage.append(" [options]"); } - command.getParameters().getDescriptions().forEach((param) -> usage.append(" " + param)); + command.getParameters().getDescriptions().forEach((param) -> usage.append(" ").append(param)); return usage.toString(); } @@ -95,7 +94,7 @@ private int getMaxLength(int minimum, Stream strings) { } private void printCommandSummary(PrintStream out, Command command, int padding) { - out.println(String.format(" %-" + padding + "s %s", command.getName(), command.getDescription())); + out.printf(" %-" + padding + "s %s%n", command.getName(), command.getDescription()); } private String getJavaCommand() { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/HelpCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/HelpCommandTests.java index 4491d8798f18..9acb9c9c6ca8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/HelpCommandTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/HelpCommandTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -62,13 +62,13 @@ void setup() throws Exception { @Test void runWhenHasNoParametersPrintsUsage() { - this.command.run(this.out, Collections.emptyMap(), Collections.emptyList()); + this.command.run(this.out, Collections.emptyList()); assertThat(this.out).hasSameContentAsResource("help-output.txt"); } @Test void runWhenHasNoCommandParameterPrintsUsage() { - this.command.run(this.out, Collections.emptyMap(), Arrays.asList("extract")); + this.command.run(this.out, Arrays.asList("extract")); System.out.println(this.out); assertThat(this.out).hasSameContentAsResource("help-extract-output.txt"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CustomLayersProvider.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CustomLayersProvider.java index e78d817e34aa..5f3d6e6c87b6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CustomLayersProvider.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CustomLayersProvider.java @@ -123,7 +123,7 @@ private ContentSelector getSelector(Element element, Function(layer, includes, excludes, filterFactory); } - private ContentSelector getLibrarySelector(Element element, + private ContentSelector getLibrarySelector(Element element, Function> filterFactory) { Layer layer = new Layer(element.getAttribute("layer")); List includes = getChildNodeTextContent(element, "include"); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoaders.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoaders.java index 2857c534648f..2276d440b37e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoaders.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoaders.java @@ -100,15 +100,14 @@ ConfigData load(ConfigDataLoaderContext context, private ConfigDataLoader getLoader(ConfigDataLoaderContext context, R resource) { ConfigDataLoader result = null; for (int i = 0; i < this.loaders.size(); i++) { - ConfigDataLoader candidate = this.loaders.get(i); + ConfigDataLoader candidate = this.loaders.get(i); if (this.resourceTypes.get(i).isInstance(resource)) { - ConfigDataLoader loader = (ConfigDataLoader) candidate; - if (loader.isLoadable(context, resource)) { + if (candidate.isLoadable(context, resource)) { if (result != null) { throw new IllegalStateException("Multiple loaders found for resource '" + resource + "' [" + candidate.getClass().getName() + "," + result.getClass().getName() + "]"); } - result = loader; + result = candidate; } } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NotConstructorBoundInjectionFailureAnalyzer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NotConstructorBoundInjectionFailureAnalyzer.java index 2e4fc05a4885..228aa5ed48cc 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NotConstructorBoundInjectionFailureAnalyzer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NotConstructorBoundInjectionFailureAnalyzer.java @@ -49,9 +49,9 @@ protected FailureAnalysis analyze(Throwable rootFailure, NoSuchBeanDefinitionExc InjectionPoint injectionPoint = findInjectionPoint(rootFailure); if (isConstructorBindingConfigurationProperties(injectionPoint)) { String simpleName = injectionPoint.getMember().getDeclaringClass().getSimpleName(); - String action = String.format("Update your configuration so that " + simpleName + " is defined via @" + String action = "Update your configuration so that " + simpleName + " is defined via @" + ConfigurationPropertiesScan.class.getSimpleName() + " or @" - + EnableConfigurationProperties.class.getSimpleName() + ".", simpleName); + + EnableConfigurationProperties.class.getSimpleName() + "."; return new FailureAnalysis( simpleName + " is annotated with @" + ConstructorBinding.class.getSimpleName() + " but it is defined as a regular bean which caused dependency injection to fail.", diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProvider.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProvider.java index 54b454ebcafe..609b8ebccc3d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProvider.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProvider.java @@ -135,7 +135,7 @@ private static Constructor[] getCandidateConstructors(Class type) { return new Constructor[0]; } return Arrays.stream(type.getDeclaredConstructors()) - .filter((constructor) -> isNonSynthetic(constructor, type)) + .filter(Constructors::isNonSynthetic) .toArray(Constructor[]::new); } @@ -148,7 +148,7 @@ private static boolean isInnerClass(Class type) { } } - private static boolean isNonSynthetic(Constructor constructor, Class type) { + private static boolean isNonSynthetic(Constructor constructor) { return !constructor.isSynthetic(); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BeanDefinitionOverrideFailureAnalyzer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BeanDefinitionOverrideFailureAnalyzer.java index ca536fcc9a7b..439f9d23a5ed 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BeanDefinitionOverrideFailureAnalyzer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BeanDefinitionOverrideFailureAnalyzer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. @@ -46,11 +46,11 @@ private String getDescription(BeanDefinitionOverrideException ex) { if (ex.getBeanDefinition().getResourceDescription() != null) { printer.printf(", defined in %s,", ex.getBeanDefinition().getResourceDescription()); } - printer.printf(" could not be registered. A bean with that name has already been defined "); + printer.print(" could not be registered. A bean with that name has already been defined "); if (ex.getExistingDefinition().getResourceDescription() != null) { printer.printf("in %s ", ex.getExistingDefinition().getResourceDescription()); } - printer.printf("and overriding is disabled."); + printer.print("and overriding is disabled."); return description.toString(); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/MutuallyExclusiveConfigurationPropertiesFailureAnalyzer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/MutuallyExclusiveConfigurationPropertiesFailureAnalyzer.java index e3d95d3658f0..324c02083740 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/MutuallyExclusiveConfigurationPropertiesFailureAnalyzer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/MutuallyExclusiveConfigurationPropertiesFailureAnalyzer.java @@ -99,7 +99,7 @@ private void appendDetails(StringBuilder message, MutuallyExclusiveConfiguration configuredDescriptions.forEach(message::append); } - private Set sortedStrings(Collection input) { + private Set sortedStrings(Collection input) { return sortedStrings(input, Function.identity()); } From 4ea3c75331e5da6b493c7bc955bc72f123c81a6e Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 8 Aug 2023 09:32:01 +0200 Subject: [PATCH 0264/1215] Improve exception message if endpoint can't be found --- ...rseyWebEndpointManagementContextConfiguration.java | 3 ++- ...WebFluxEndpointManagementContextConfiguration.java | 11 ++++++----- .../WebMvcEndpointManagementContextConfiguration.java | 7 ++++--- ...althEndpointReactiveWebExtensionConfiguration.java | 3 ++- .../HealthEndpointWebExtensionConfiguration.java | 3 ++- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java index 24736e2647d0..dd7a8668ca4c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java @@ -103,7 +103,8 @@ JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar jerseyDifferentP ExposableWebEndpoint health = webEndpoints.stream() .filter((endpoint) -> endpoint.getEndpointId().equals(HEALTH_ENDPOINT_ID)) .findFirst() - .get(); + .orElseThrow( + () -> new IllegalStateException("No endpoint with id '%s' found".formatted(HEALTH_ENDPOINT_ID))); return new JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar(health, healthEndpointGroups); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java index 9e117dd3b7c9..94ea4766f50f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java @@ -120,7 +120,8 @@ public AdditionalHealthEndpointPathsWebFluxHandlerMapping managementHealthEndpoi ExposableWebEndpoint health = webEndpoints.stream() .filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID)) .findFirst() - .get(); + .orElseThrow( + () -> new IllegalStateException("No endpoint with id '%s' found".formatted(HealthEndpoint.ID))); return new AdditionalHealthEndpointPathsWebFluxHandlerMapping(new EndpointMapping(""), health, groups.getAllWithAdditionalPath(WebServerNamespace.MANAGEMENT)); } @@ -162,16 +163,16 @@ static class ServerCodecConfigurerEndpointObjectMapperBeanPostProcessor implemen @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof ServerCodecConfigurer) { - process((ServerCodecConfigurer) bean); + if (bean instanceof ServerCodecConfigurer serverCodecConfigurer) { + process(serverCodecConfigurer); } return bean; } private void process(ServerCodecConfigurer configurer) { for (HttpMessageWriter writer : configurer.getWriters()) { - if (writer instanceof EncoderHttpMessageWriter) { - process(((EncoderHttpMessageWriter) writer).getEncoder()); + if (writer instanceof EncoderHttpMessageWriter encoderHttpMessageWriter) { + process((encoderHttpMessageWriter).getEncoder()); } } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java index f271c663ab95..451e08b61396 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java @@ -115,7 +115,8 @@ public AdditionalHealthEndpointPathsWebMvcHandlerMapping managementHealthEndpoin ExposableWebEndpoint health = webEndpoints.stream() .filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID)) .findFirst() - .get(); + .orElseThrow( + () -> new IllegalStateException("No endpoint with id '%s' found".formatted(HealthEndpoint.ID))); return new AdditionalHealthEndpointPathsWebMvcHandlerMapping(health, groups.getAllWithAdditionalPath(WebServerNamespace.MANAGEMENT)); } @@ -157,8 +158,8 @@ static class EndpointObjectMapperWebMvcConfigurer implements WebMvcConfigurer { @Override public void configureMessageConverters(List> converters) { for (HttpMessageConverter converter : converters) { - if (converter instanceof MappingJackson2HttpMessageConverter) { - configure((MappingJackson2HttpMessageConverter) converter); + if (converter instanceof MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter) { + configure(mappingJackson2HttpMessageConverter); } } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointReactiveWebExtensionConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointReactiveWebExtensionConfiguration.java index 6e80745fa7e7..4a8d814ebd4e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointReactiveWebExtensionConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointReactiveWebExtensionConfiguration.java @@ -70,7 +70,8 @@ AdditionalHealthEndpointPathsWebFluxHandlerMapping healthEndpointWebFluxHandlerM ExposableWebEndpoint health = webEndpoints.stream() .filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID)) .findFirst() - .get(); + .orElseThrow( + () -> new IllegalStateException("No endpoint with id '%s' found".formatted(HealthEndpoint.ID))); return new AdditionalHealthEndpointPathsWebFluxHandlerMapping(new EndpointMapping(""), health, groups.getAllWithAdditionalPath(WebServerNamespace.SERVER)); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java index b0924d928018..a973b2f0fa4c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/HealthEndpointWebExtensionConfiguration.java @@ -81,7 +81,8 @@ private static ExposableWebEndpoint getHealthEndpoint(WebEndpointsSupplier webEn return webEndpoints.stream() .filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID)) .findFirst() - .get(); + .orElseThrow( + () -> new IllegalStateException("No endpoint with id '%s' found".formatted(HealthEndpoint.ID))); } @ConditionalOnBean(DispatcherServlet.class) From 7bb337aeb173c9c6c304d48648a35d51d39a51fd Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 8 Aug 2023 09:46:15 +0200 Subject: [PATCH 0265/1215] Polish tests --- .../sonatype/SonatypeServiceTests.java | 6 ++-- ...ctiveCloudFoundrySecurityServiceTests.java | 8 ++---- ...aSourceScriptDatabaseInitializerTests.java | 5 ++-- .../ConditionEvaluationReportTests.java | 2 +- .../ConditionalOnMissingBeanTests.java | 18 ++++++------ .../ProjectInfoAutoConfigurationTests.java | 2 +- ...rationCustomObjectMapperProviderTests.java | 4 +-- ...onfigurationObjectMapperProviderTests.java | 4 +-- .../jms/activemq/ActiveMQPropertiesTests.java | 2 +- .../servlet/WebMvcAutoConfigurationTests.java | 2 +- .../devtools/restart/ChangeableUrlsTests.java | 2 +- .../RestartApplicationListenerTests.java | 5 ++-- .../compose/core/DockerComposeFileTests.java | 2 +- ...nectionDetailsFactoryIntegrationTests.java | 2 +- .../type/ImageArchiveManifestTests.java | 2 +- .../PropertyDescriptorResolverTests.java | 13 +++++---- .../bundling/AbstractBootArchiveTests.java | 4 +-- .../boot/loader/jar/JarFileTests.java | 2 +- ...nfigDataEnvironmentPostProcessorTests.java | 2 +- .../ConfigurationPropertySourcesTests.java | 2 +- ...h2AuthorizationServerApplicationTests.java | 28 +++++++++---------- 21 files changed, 58 insertions(+), 59 deletions(-) diff --git a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/sonatype/SonatypeServiceTests.java b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/sonatype/SonatypeServiceTests.java index ef949ac8b0d4..80cf310e1094 100644 --- a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/sonatype/SonatypeServiceTests.java +++ b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/sonatype/SonatypeServiceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -132,7 +132,7 @@ void publishWithSuccessfulClose() throws IOException { .andRespond(withSuccess()); this.service.publish(getReleaseInfo(), artifactsRoot); this.server.verify(); - assertThat(uploadRequestsMatcher.candidates).hasSize(0); + assertThat(uploadRequestsMatcher.candidates).isEmpty(); } } @@ -184,7 +184,7 @@ void publishWithCloseFailureDueToRuleViolations() throws IOException { .isThrownBy(() -> this.service.publish(getReleaseInfo(), artifactsRoot)) .withMessage("Close failed"); this.server.verify(); - assertThat(uploadRequestsMatcher.candidates).hasSize(0); + assertThat(uploadRequestsMatcher.candidates).isEmpty(); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityServiceTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityServiceTests.java index 29258b9f20aa..ac73c0fc21ca 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityServiceTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundrySecurityServiceTests.java @@ -51,13 +51,11 @@ class ReactiveCloudFoundrySecurityServiceTests { private MockWebServer server; - private WebClient.Builder builder; - @BeforeEach void setup() { this.server = new MockWebServer(); - this.builder = WebClient.builder().baseUrl(this.server.url("/").toString()); - this.securityService = new ReactiveCloudFoundrySecurityService(this.builder, CLOUD_CONTROLLER, false); + WebClient.Builder builder = WebClient.builder().baseUrl(this.server.url("/").toString()); + this.securityService = new ReactiveCloudFoundrySecurityService(builder, CLOUD_CONTROLLER, false); } @AfterEach @@ -183,7 +181,7 @@ void fetchTokenKeysWhenNoKeysReturnedFromUAA() throws Exception { response.setHeader("Content-Type", "application/json"); }); StepVerifier.create(this.securityService.fetchTokenKeys()) - .consumeNextWith((tokenKeys) -> assertThat(tokenKeys).hasSize(0)) + .consumeNextWith((tokenKeys) -> assertThat(tokenKeys).isEmpty()) .expectComplete() .verify(); expectRequest((request) -> assertThat(request.getPath()).isEqualTo("/my-cloud-controller.com/info")); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchDataSourceScriptDatabaseInitializerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchDataSourceScriptDatabaseInitializerTests.java index 6b04e877c619..87a87f90c460 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchDataSourceScriptDatabaseInitializerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchDataSourceScriptDatabaseInitializerTests.java @@ -33,6 +33,7 @@ import org.springframework.boot.jdbc.DatabaseDriver; import org.springframework.boot.sql.init.DatabaseInitializationSettings; import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.test.util.ReflectionTestUtils; @@ -76,7 +77,7 @@ void batchSchemaCanBeLocated(DatabaseDriver driver) throws SQLException { DatabaseInitializationSettings settings = BatchDataSourceScriptDatabaseInitializer.getSettings(dataSource, properties.getJdbc()); List schemaLocations = settings.getSchemaLocations(); - assertThat(schemaLocations) + assertThat(schemaLocations).isNotEmpty() .allSatisfy((location) -> assertThat(resourceLoader.getResource(location).exists()).isTrue()); } @@ -85,7 +86,7 @@ void batchHasExpectedBuiltInSchemas() throws IOException { PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); List schemaNames = Stream .of(resolver.getResources("classpath:org/springframework/batch/core/schema-*.sql")) - .map((resource) -> resource.getFilename()) + .map(Resource::getFilename) .filter((resourceName) -> !resourceName.contains("-drop-")) .toList(); assertThat(schemaNames).containsExactlyInAnyOrder("schema-derby.sql", "schema-sqlserver.sql", diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionEvaluationReportTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionEvaluationReportTests.java index 2e130ff8c955..fc1df6ace290 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionEvaluationReportTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionEvaluationReportTests.java @@ -164,7 +164,7 @@ private void prepareMatches(boolean m1, boolean m2, boolean m3) { void springBootConditionPopulatesReport() { ConditionEvaluationReport report = ConditionEvaluationReport .get(new AnnotationConfigApplicationContext(Config.class).getBeanFactory()); - assertThat(report.getConditionAndOutcomesBySource().size()).isNotZero(); + assertThat(report.getConditionAndOutcomesBySource()).isNotEmpty(); } @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java index da64265c301d..9ca96314e9fa 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java @@ -155,7 +155,7 @@ void testOnMissingBeanConditionWithFactoryBean() { this.contextRunner .withUserConfiguration(FactoryBeanConfiguration.class, ConditionalOnFactoryBean.class, PropertyPlaceholderAutoConfiguration.class) - .run((context) -> assertThat(context.getBean(ExampleBean.class).toString()).isEqualTo("fromFactory")); + .run((context) -> assertThat(context.getBean(ExampleBean.class)).hasToString("fromFactory")); } @Test @@ -163,7 +163,7 @@ void testOnMissingBeanConditionWithComponentScannedFactoryBean() { this.contextRunner .withUserConfiguration(ComponentScannedFactoryBeanBeanMethodConfiguration.class, ConditionalOnFactoryBean.class, PropertyPlaceholderAutoConfiguration.class) - .run((context) -> assertThat(context.getBean(ScanBean.class).toString()).isEqualTo("fromFactory")); + .run((context) -> assertThat(context.getBean(ScanBean.class)).hasToString("fromFactory")); } @Test @@ -171,7 +171,7 @@ void testOnMissingBeanConditionWithComponentScannedFactoryBeanWithBeanMethodArgu this.contextRunner .withUserConfiguration(ComponentScannedFactoryBeanBeanMethodWithArgumentsConfiguration.class, ConditionalOnFactoryBean.class, PropertyPlaceholderAutoConfiguration.class) - .run((context) -> assertThat(context.getBean(ScanBean.class).toString()).isEqualTo("fromFactory")); + .run((context) -> assertThat(context.getBean(ScanBean.class)).hasToString("fromFactory")); } @Test @@ -180,7 +180,7 @@ void testOnMissingBeanConditionWithFactoryBeanWithBeanMethodArguments() { .withUserConfiguration(FactoryBeanWithBeanMethodArgumentsConfiguration.class, ConditionalOnFactoryBean.class, PropertyPlaceholderAutoConfiguration.class) .withPropertyValues("theValue=foo") - .run((context) -> assertThat(context.getBean(ExampleBean.class).toString()).isEqualTo("fromFactory")); + .run((context) -> assertThat(context.getBean(ExampleBean.class)).hasToString("fromFactory")); } @Test @@ -188,7 +188,7 @@ void testOnMissingBeanConditionWithConcreteFactoryBean() { this.contextRunner .withUserConfiguration(ConcreteFactoryBeanConfiguration.class, ConditionalOnFactoryBean.class, PropertyPlaceholderAutoConfiguration.class) - .run((context) -> assertThat(context.getBean(ExampleBean.class).toString()).isEqualTo("fromFactory")); + .run((context) -> assertThat(context.getBean(ExampleBean.class)).hasToString("fromFactory")); } @Test @@ -205,7 +205,7 @@ void testOnMissingBeanConditionWithRegisteredFactoryBean() { this.contextRunner .withUserConfiguration(RegisteredFactoryBeanConfiguration.class, ConditionalOnFactoryBean.class, PropertyPlaceholderAutoConfiguration.class) - .run((context) -> assertThat(context.getBean(ExampleBean.class).toString()).isEqualTo("fromFactory")); + .run((context) -> assertThat(context.getBean(ExampleBean.class)).hasToString("fromFactory")); } @Test @@ -213,7 +213,7 @@ void testOnMissingBeanConditionWithNonspecificFactoryBeanWithClassAttribute() { this.contextRunner .withUserConfiguration(NonspecificFactoryBeanClassAttributeConfiguration.class, ConditionalOnFactoryBean.class, PropertyPlaceholderAutoConfiguration.class) - .run((context) -> assertThat(context.getBean(ExampleBean.class).toString()).isEqualTo("fromFactory")); + .run((context) -> assertThat(context.getBean(ExampleBean.class)).hasToString("fromFactory")); } @Test @@ -221,7 +221,7 @@ void testOnMissingBeanConditionWithNonspecificFactoryBeanWithStringAttribute() { this.contextRunner .withUserConfiguration(NonspecificFactoryBeanStringAttributeConfiguration.class, ConditionalOnFactoryBean.class, PropertyPlaceholderAutoConfiguration.class) - .run((context) -> assertThat(context.getBean(ExampleBean.class).toString()).isEqualTo("fromFactory")); + .run((context) -> assertThat(context.getBean(ExampleBean.class)).hasToString("fromFactory")); } @Test @@ -229,7 +229,7 @@ void testOnMissingBeanConditionWithFactoryBeanInXml() { this.contextRunner .withUserConfiguration(FactoryBeanXmlConfiguration.class, ConditionalOnFactoryBean.class, PropertyPlaceholderAutoConfiguration.class) - .run((context) -> assertThat(context.getBean(ExampleBean.class).toString()).isEqualTo("fromFactory")); + .run((context) -> assertThat(context.getBean(ExampleBean.class)).hasToString("fromFactory")); } @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfigurationTests.java index 9d8680999811..7a65682b9306 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/info/ProjectInfoAutoConfigurationTests.java @@ -119,7 +119,7 @@ void buildPropertiesCustomLocation() { @Test void buildPropertiesCustomInvalidLocation() { this.contextRunner.withPropertyValues("spring.info.build.location=classpath:/org/acme/no-build-info.properties") - .run((context) -> assertThat(context.getBeansOfType(BuildProperties.class)).hasSize(0)); + .run((context) -> assertThat(context.getBeansOfType(BuildProperties.class)).isEmpty()); } @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomObjectMapperProviderTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomObjectMapperProviderTests.java index 7cddc14bfe79..fe823f095482 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomObjectMapperProviderTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationCustomObjectMapperProviderTests.java @@ -60,8 +60,8 @@ class JerseyAutoConfigurationCustomObjectMapperProviderTests { @Test void contextLoads() { ResponseEntity response = this.restTemplate.getForEntity("/rest/message", String.class); - assertThat(HttpStatus.OK).isEqualTo(response.getStatusCode()); - assertThat(response.getBody()).isEqualTo("{\"subject\":\"Jersey\"}"); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat("{\"subject\":\"Jersey\"}").isEqualTo(response.getBody()); } @MinimalWebConfiguration diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationObjectMapperProviderTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationObjectMapperProviderTests.java index e1336263463c..59cc5167b36e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationObjectMapperProviderTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jersey/JerseyAutoConfigurationObjectMapperProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -62,7 +62,7 @@ class JerseyAutoConfigurationObjectMapperProviderTests { @Test void responseIsSerializedUsingAutoConfiguredObjectMapper() { ResponseEntity response = this.restTemplate.getForEntity("/rest/message", String.class); - assertThat(HttpStatus.OK).isEqualTo(response.getStatusCode()); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).isEqualTo("{\"subject\":\"Jersey\"}"); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQPropertiesTests.java index 07d84e900e7a..a839b4d03d89 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQPropertiesTests.java @@ -62,7 +62,7 @@ void setTrustedPackages() { ActiveMQConnectionFactory factory = createFactory(this.properties) .createConnectionFactory(ActiveMQConnectionFactory.class); assertThat(factory.isTrustAllPackages()).isFalse(); - assertThat(factory.getTrustedPackages().size()).isEqualTo(1); + assertThat(factory.getTrustedPackages()).hasSize(1); assertThat(factory.getTrustedPackages().get(0)).isEqualTo("trusted.package"); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java index 1ecc2395be40..7ccc5e12bff4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfigurationTests.java @@ -235,7 +235,7 @@ void resourceHandlerMappingOverrideAll() { @Test void resourceHandlerMappingDisabled() { this.contextRunner.withPropertyValues("spring.web.resources.add-mappings:false") - .run((context) -> assertThat(getResourceMappingLocations(context)).hasSize(0)); + .run((context) -> assertThat(getResourceMappingLocations(context)).isEmpty()); } @Test diff --git a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/ChangeableUrlsTests.java b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/ChangeableUrlsTests.java index 354fd1d7e301..35504c892648 100644 --- a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/ChangeableUrlsTests.java +++ b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/ChangeableUrlsTests.java @@ -48,7 +48,7 @@ class ChangeableUrlsTests { @Test void directoryUrl() throws Exception { URL url = makeUrl("myproject"); - assertThat(ChangeableUrls.fromUrls(url).size()).isOne(); + assertThat(ChangeableUrls.fromUrls(url)).hasSize(1); } @Test diff --git a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/RestartApplicationListenerTests.java b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/RestartApplicationListenerTests.java index 308642994ca7..059bc27443cc 100644 --- a/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/RestartApplicationListenerTests.java +++ b/spring-boot-project/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/restart/RestartApplicationListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -36,7 +36,6 @@ import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.mock; /** @@ -103,7 +102,7 @@ private void testInitialize(boolean failed) { SpringApplication application = new SpringApplication(); ConfigurableApplicationContext context = mock(ConfigurableApplicationContext.class); listener.onApplicationEvent(new ApplicationStartingEvent(bootstrapContext, application, ARGS)); - assertThat(Restarter.getInstance()).isNotEqualTo(nullValue()); + assertThat(Restarter.getInstance()).isNotNull(); assertThat(Restarter.getInstance().isFinished()).isFalse(); listener.onApplicationEvent(new ApplicationPreparedEvent(application, ARGS, context)); if (failed) { diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerComposeFileTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerComposeFileTests.java index 4d1692e247fd..02bab15eb46c 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerComposeFileTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerComposeFileTests.java @@ -106,7 +106,7 @@ void ofReturnsDockerComposeFile() throws Exception { FileCopyUtils.copy(new byte[0], file); DockerComposeFile composeFile = DockerComposeFile.of(file); assertThat(composeFile).isNotNull(); - assertThat(composeFile.toString()).isEqualTo(file.getCanonicalPath()); + assertThat(composeFile).hasToString(file.getCanonicalPath()); } @Test diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraDockerComposeConnectionDetailsFactoryIntegrationTests.java index 5c7bd51379b3..8df562e23111 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -42,7 +42,7 @@ class CassandraDockerComposeConnectionDetailsFactoryIntegrationTests extends Abs void runCreatesConnectionDetails() { CassandraConnectionDetails connectionDetails = run(CassandraConnectionDetails.class); List contactPoints = connectionDetails.getContactPoints(); - assertThat(contactPoints.size()).isEqualTo(1); + assertThat(contactPoints).hasSize(1); Node node = contactPoints.get(0); assertThat(node.host()).isNotNull(); assertThat(node.port()).isGreaterThan(0); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveManifestTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveManifestTests.java index 31cd8a768cdf..8f0eaccd8453 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveManifestTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveManifestTests.java @@ -52,7 +52,7 @@ void getLayersWithNoLayersReturnsEmptyList() throws Exception { String content = "[{\"Layers\": []}]"; ImageArchiveManifest manifest = new ImageArchiveManifest(getObjectMapper().readTree(content)); assertThat(manifest.getEntries()).hasSize(1); - assertThat(manifest.getEntries().get(0).getLayers()).hasSize(0); + assertThat(manifest.getEntries().get(0).getLayers()).isEmpty(); } @Test diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolverTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolverTests.java index 4f45a734cb1c..9a2b1aeed873 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolverTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolverTests.java @@ -111,15 +111,16 @@ void propertiesWithLombokValueClass() { void propertiesWithDeducedConstructorBinding() { process(ImmutableDeducedConstructorBindingProperties.class, propertyNames((stream) -> assertThat(stream).containsExactly("theName", "flag"))); - process(ImmutableDeducedConstructorBindingProperties.class, properties((stream) -> assertThat(stream) - .allMatch((predicate) -> predicate instanceof ConstructorParameterPropertyDescriptor))); + process(ImmutableDeducedConstructorBindingProperties.class, + properties((stream) -> assertThat(stream).isNotEmpty() + .allMatch((predicate) -> predicate instanceof ConstructorParameterPropertyDescriptor))); } @Test void propertiesWithConstructorWithConstructorBinding() { process(ImmutableSimpleProperties.class, propertyNames( (stream) -> assertThat(stream).containsExactly("theName", "flag", "comparator", "counter"))); - process(ImmutableSimpleProperties.class, properties((stream) -> assertThat(stream) + process(ImmutableSimpleProperties.class, properties((stream) -> assertThat(stream).isNotEmpty() .allMatch((predicate) -> predicate instanceof ConstructorParameterPropertyDescriptor))); } @@ -127,14 +128,14 @@ void propertiesWithConstructorWithConstructorBinding() { void propertiesWithConstructorAndClassConstructorBinding() { process(ImmutableClassConstructorBindingProperties.class, propertyNames((stream) -> assertThat(stream).containsExactly("name", "description"))); - process(ImmutableClassConstructorBindingProperties.class, properties((stream) -> assertThat(stream) + process(ImmutableClassConstructorBindingProperties.class, properties((stream) -> assertThat(stream).isNotEmpty() .allMatch((predicate) -> predicate instanceof ConstructorParameterPropertyDescriptor))); } @Test void propertiesWithAutowiredConstructor() { process(AutowiredProperties.class, propertyNames((stream) -> assertThat(stream).containsExactly("theName"))); - process(AutowiredProperties.class, properties((stream) -> assertThat(stream) + process(AutowiredProperties.class, properties((stream) -> assertThat(stream).isNotEmpty() .allMatch((predicate) -> predicate instanceof JavaBeanPropertyDescriptor))); } @@ -142,7 +143,7 @@ void propertiesWithAutowiredConstructor() { void propertiesWithMultiConstructor() { process(ImmutableMultiConstructorProperties.class, propertyNames((stream) -> assertThat(stream).containsExactly("name", "description"))); - process(ImmutableMultiConstructorProperties.class, properties((stream) -> assertThat(stream) + process(ImmutableMultiConstructorProperties.class, properties((stream) -> assertThat(stream).isNotEmpty() .allMatch((predicate) -> predicate instanceof ConstructorParameterPropertyDescriptor))); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java index ec338420f1b9..f4eceac89a8f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java @@ -488,7 +488,7 @@ void archiveShouldBeLayeredByDefault() throws IOException { void jarWhenLayersDisabledShouldNotContainLayersIndex() throws IOException { List entryNames = getEntryNames( createLayeredJar((configuration) -> configuration.getEnabled().set(false))); - assertThat(entryNames).doesNotContain(this.indexPath + "layers.idx"); + assertThat(entryNames).isNotEmpty().doesNotContain(this.indexPath + "layers.idx"); } @Test @@ -605,7 +605,7 @@ void whenArchiveIsLayeredThenLayerToolsAreAddedToTheJar() throws IOException { void whenArchiveIsLayeredAndIncludeLayerToolsIsFalseThenLayerToolsAreNotAddedToTheJar() throws IOException { List entryNames = getEntryNames( createLayeredJar((configuration) -> configuration.getIncludeLayerTools().set(false))); - assertThat(entryNames) + assertThat(entryNames).isNotEmpty() .doesNotContain(this.indexPath + "layers/dependencies/lib/spring-boot-jarmode-layertools.jar"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java index 1188dd0ba81c..b37a99183a72 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java @@ -220,7 +220,7 @@ void getName() { @Test void size() throws Exception { try (ZipFile zip = new ZipFile(this.rootJarFile)) { - assertThat(this.jarFile.size()).isEqualTo(zip.size()); + assertThat(this.jarFile).hasSize(zip.size()); } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessorTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessorTests.java index a63c056fbb44..f7139ad750ad 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessorTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessorTests.java @@ -108,7 +108,7 @@ void applyToAppliesPostProcessing() { TestConfigDataEnvironmentUpdateListener listener = new TestConfigDataEnvironmentUpdateListener(); ConfigDataEnvironmentPostProcessor.applyTo(this.environment, null, null, Collections.singleton("dev"), listener); - assertThat(this.environment.getPropertySources().size()).isGreaterThan(before); + assertThat(this.environment.getPropertySources()).hasSizeGreaterThan(before); assertThat(this.environment.getActiveProfiles()).containsExactly("dev"); assertThat(listener.getAddedPropertySources()).isNotEmpty(); assertThat(listener.getProfiles().getActive()).containsExactly("dev"); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesTests.java index 552bbbc2effa..5907688999b9 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesTests.java @@ -54,7 +54,7 @@ void attachShouldAddAdapterAtBeginning() { sources.addLast(new MapPropertySource("config", Collections.singletonMap("server.port", "4568"))); int size = sources.size(); ConfigurationPropertySources.attach(environment); - assertThat(sources.size()).isEqualTo(size + 1); + assertThat(sources).hasSize(size + 1); PropertyResolver resolver = new PropertySourcesPropertyResolver(sources); assertThat(resolver.getProperty("server.port")).isEqualTo("1234"); } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java index 0ecf890c9f43..dbc0bff4e5b0 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java @@ -62,14 +62,14 @@ void openidConfigurationShouldAllowAccess() { assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); OidcProviderConfiguration config = OidcProviderConfiguration.withClaims(entity.getBody()).build(); - assertThat(config.getIssuer().toString()).isEqualTo("https://provider.com"); - assertThat(config.getAuthorizationEndpoint().toString()).isEqualTo("https://provider.com/authorize"); - assertThat(config.getTokenEndpoint().toString()).isEqualTo("https://provider.com/token"); - assertThat(config.getJwkSetUrl().toString()).isEqualTo("https://provider.com/jwks"); - assertThat(config.getTokenRevocationEndpoint().toString()).isEqualTo("https://provider.com/revoke"); - assertThat(config.getEndSessionEndpoint().toString()).isEqualTo("https://provider.com/logout"); - assertThat(config.getTokenIntrospectionEndpoint().toString()).isEqualTo("https://provider.com/introspect"); - assertThat(config.getUserInfoEndpoint().toString()).isEqualTo("https://provider.com/user"); + assertThat(config.getIssuer()).hasToString("https://provider.com"); + assertThat(config.getAuthorizationEndpoint()).hasToString("https://provider.com/authorize"); + assertThat(config.getTokenEndpoint()).hasToString("https://provider.com/token"); + assertThat(config.getJwkSetUrl()).hasToString("https://provider.com/jwks"); + assertThat(config.getTokenRevocationEndpoint()).hasToString("https://provider.com/revoke"); + assertThat(config.getEndSessionEndpoint()).hasToString("https://provider.com/logout"); + assertThat(config.getTokenIntrospectionEndpoint()).hasToString("https://provider.com/introspect"); + assertThat(config.getUserInfoEndpoint()).hasToString("https://provider.com/user"); // OIDC Client Registration is disabled by default assertThat(config.getClientRegistrationEndpoint()).isNull(); } @@ -82,12 +82,12 @@ void authServerMetadataShouldAllowAccess() { OAuth2AuthorizationServerMetadata config = OAuth2AuthorizationServerMetadata.withClaims(entity.getBody()) .build(); - assertThat(config.getIssuer().toString()).isEqualTo("https://provider.com"); - assertThat(config.getAuthorizationEndpoint().toString()).isEqualTo("https://provider.com/authorize"); - assertThat(config.getTokenEndpoint().toString()).isEqualTo("https://provider.com/token"); - assertThat(config.getJwkSetUrl().toString()).isEqualTo("https://provider.com/jwks"); - assertThat(config.getTokenRevocationEndpoint().toString()).isEqualTo("https://provider.com/revoke"); - assertThat(config.getTokenIntrospectionEndpoint().toString()).isEqualTo("https://provider.com/introspect"); + assertThat(config.getIssuer()).hasToString("https://provider.com"); + assertThat(config.getAuthorizationEndpoint()).hasToString("https://provider.com/authorize"); + assertThat(config.getTokenEndpoint()).hasToString("https://provider.com/token"); + assertThat(config.getJwkSetUrl()).hasToString("https://provider.com/jwks"); + assertThat(config.getTokenRevocationEndpoint()).hasToString("https://provider.com/revoke"); + assertThat(config.getTokenIntrospectionEndpoint()).hasToString("https://provider.com/introspect"); // OIDC Client Registration is disabled by default assertThat(config.getClientRegistrationEndpoint()).isNull(); } From 62fb45f75f62344139666bc5126cd77e0bd9edd0 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 8 Aug 2023 10:08:31 +0200 Subject: [PATCH 0266/1215] Replace contains/put/get pattern with computeIfAbsent --- .../metrics/jdbc/DataSourcePoolMetrics.java | 8 ++---- .../condition/ConditionEvaluationReport.java | 5 +--- .../restart/classloader/ClassLoaderFiles.java | 9 ++---- ...SimpleConfigurationMetadataRepository.java | 28 +++++-------------- 4 files changed, 12 insertions(+), 38 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jdbc/DataSourcePoolMetrics.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jdbc/DataSourcePoolMetrics.java index caf29f30ff68..f22023a1ef24 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jdbc/DataSourcePoolMetrics.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/jdbc/DataSourcePoolMetrics.java @@ -110,12 +110,8 @@ Function getValueFunction(Function this.metadataProvider.getDataSourcePoolMetadata(dataSource)); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionEvaluationReport.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionEvaluationReport.java index cb987b1af3a9..7712bc2847ed 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionEvaluationReport.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionEvaluationReport.java @@ -80,10 +80,7 @@ public void recordConditionEvaluation(String source, Condition condition, Condit Assert.notNull(condition, "Condition must not be null"); Assert.notNull(outcome, "Outcome must not be null"); this.unconditionalClasses.remove(source); - if (!this.outcomes.containsKey(source)) { - this.outcomes.put(source, new ConditionAndOutcomes()); - } - this.outcomes.get(source).add(condition, outcome); + this.outcomes.computeIfAbsent(source, (key) -> new ConditionAndOutcomes()).add(condition, outcome); this.addedAncestorOutcomes = false; } diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/classloader/ClassLoaderFiles.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/classloader/ClassLoaderFiles.java index e611e5f39827..06e7d8f3f97b 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/classloader/ClassLoaderFiles.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/classloader/ClassLoaderFiles.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. @@ -108,12 +108,7 @@ private void removeAll(String name) { * @return an existing or newly added {@link SourceDirectory} */ protected final SourceDirectory getOrCreateSourceDirectory(String name) { - SourceDirectory sourceDirectory = this.sourceDirectories.get(name); - if (sourceDirectory == null) { - sourceDirectory = new SourceDirectory(name); - this.sourceDirectories.put(name, sourceDirectory); - } - return sourceDirectory; + return this.sourceDirectories.computeIfAbsent(name, (key) -> new SourceDirectory(name)); } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/SimpleConfigurationMetadataRepository.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/SimpleConfigurationMetadataRepository.java index bdfc91cb20d0..c196afb5e2f2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/SimpleConfigurationMetadataRepository.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/SimpleConfigurationMetadataRepository.java @@ -54,11 +54,8 @@ public Map getAllProperties() { public void add(Collection sources) { for (ConfigurationMetadataSource source : sources) { String groupId = source.getGroupId(); - ConfigurationMetadataGroup group = this.allGroups.get(groupId); - if (group == null) { - group = new ConfigurationMetadataGroup(groupId); - this.allGroups.put(groupId, group); - } + ConfigurationMetadataGroup group = this.allGroups.computeIfAbsent(groupId, + (key) -> new ConfigurationMetadataGroup(groupId)); String sourceType = source.getType(); if (sourceType != null) { addOrMergeSource(group.getSources(), sourceType, source); @@ -74,9 +71,9 @@ public void add(Collection sources) { */ public void add(ConfigurationMetadataProperty property, ConfigurationMetadataSource source) { if (source != null) { - putIfAbsent(source.getProperties(), property.getId(), property); + source.getProperties().putIfAbsent(property.getId(), property); } - putIfAbsent(getGroup(source).getProperties(), property.getId(), property); + getGroup(source).getProperties().putIfAbsent(property.getId(), property); } /** @@ -91,7 +88,7 @@ public void include(ConfigurationMetadataRepository repository) { } else { // Merge properties - group.getProperties().forEach((name, value) -> putIfAbsent(existingGroup.getProperties(), name, value)); + group.getProperties().forEach((name, value) -> existingGroup.getProperties().putIfAbsent(name, value)); // Merge sources group.getSources().forEach((name, value) -> addOrMergeSource(existingGroup.getSources(), name, value)); } @@ -101,12 +98,7 @@ public void include(ConfigurationMetadataRepository repository) { private ConfigurationMetadataGroup getGroup(ConfigurationMetadataSource source) { if (source == null) { - ConfigurationMetadataGroup rootGroup = this.allGroups.get(ROOT_GROUP); - if (rootGroup == null) { - rootGroup = new ConfigurationMetadataGroup(ROOT_GROUP); - this.allGroups.put(ROOT_GROUP, rootGroup); - } - return rootGroup; + return this.allGroups.computeIfAbsent(ROOT_GROUP, (key) -> new ConfigurationMetadataGroup(ROOT_GROUP)); } return this.allGroups.get(source.getGroupId()); } @@ -118,13 +110,7 @@ private void addOrMergeSource(Map sources, sources.put(name, source); } else { - source.getProperties().forEach((k, v) -> putIfAbsent(existingSource.getProperties(), k, v)); - } - } - - private void putIfAbsent(Map map, String key, V value) { - if (!map.containsKey(key)) { - map.put(key, value); + source.getProperties().forEach((k, v) -> existingSource.getProperties().putIfAbsent(k, v)); } } From de57b5f4a49a70b84531c95fdf65a0681b023ef7 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 8 Aug 2023 10:29:31 +0200 Subject: [PATCH 0267/1215] Call remove() on ThreadLocal in SpringBootMockMvcBuilderCustomizer --- .../web/servlet/SpringBootMockMvcBuilderCustomizer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java index 269d3615009a..d739a2766b4a 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java @@ -253,7 +253,7 @@ static DeferredLinesWriter get(ApplicationContext applicationContext) { } void clear() { - this.lines.get().clear(); + this.lines.remove(); } } From b5a48e926d6bdd5805e08c3fdd1c008bd25ebe07 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 8 Aug 2023 10:35:11 +0200 Subject: [PATCH 0268/1215] Handle timeout of latch await in tests --- .../task/TaskExecutionAutoConfigurationTests.java | 5 +---- .../servlet/SpringBootMockMvcBuilderCustomizerTests.java | 8 +++----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java index 1a3149255ca8..0ae673011165 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java @@ -51,7 +51,6 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; @@ -366,9 +365,7 @@ private String virtualThreadName(SimpleAsyncTaskExecutor taskExecutor) throws In threadReference.set(currentThread); latch.countDown(); }); - if (!latch.await(30, TimeUnit.SECONDS)) { - fail("Timeout while waiting for latch"); - } + assertThat(latch.await(30, TimeUnit.SECONDS)).isTrue(); Thread thread = threadReference.get(); assertThat(thread).extracting("virtual").as("%s is virtual", thread).isEqualTo(true); return thread.getName(); diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java index 16c8126396cd..f4ddd7165428 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java @@ -50,8 +50,6 @@ */ class SpringBootMockMvcBuilderCustomizerTests { - private SpringBootMockMvcBuilderCustomizer customizer; - @Test @SuppressWarnings("unchecked") void customizeShouldAddFilters() { @@ -61,8 +59,8 @@ void customizeShouldAddFilters() { context.register(ServletConfiguration.class, FilterConfiguration.class); context.refresh(); DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(context); - this.customizer = new SpringBootMockMvcBuilderCustomizer(context); - this.customizer.customize(builder); + SpringBootMockMvcBuilderCustomizer customizer = new SpringBootMockMvcBuilderCustomizer(context); + customizer.customize(builder); FilterRegistrationBean registrationBean = (FilterRegistrationBean) context .getBean("filterRegistrationBean"); Filter testFilter = (Filter) context.getBean("testFilter"); @@ -94,7 +92,7 @@ void whenCalledInParallelDeferredLinesWriterSeparatesOutputByThread() throws Exc }); thread.start(); } - latch.await(60, TimeUnit.SECONDS); + assertThat(latch.await(60, TimeUnit.SECONDS)).isTrue(); assertThat(delegate.allWritten).hasSize(10000); assertThat(delegate.allWritten) From 9e85e3e3af5b8d998df8e532cbbddfd2a4b50329 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 8 Aug 2023 10:37:55 +0200 Subject: [PATCH 0269/1215] Disable nullability override inspections --- .idea/inspectionProfiles/Project_Default.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 7f1766dd1b68..139b9cd3647c 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -2,6 +2,16 @@ \ No newline at end of file From e4484c3db50710e44b06e402dbbdf7e743e486af Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 8 Aug 2023 10:49:03 +0200 Subject: [PATCH 0270/1215] Use constants in SpringBootAotPlugin --- .../boot/gradle/plugin/SpringBootAotPlugin.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootAotPlugin.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootAotPlugin.java index 00ba72184993..d24a3ce7e346 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootAotPlugin.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootAotPlugin.java @@ -78,9 +78,9 @@ public void apply(Project project) { JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class); SourceSetContainer sourceSets = javaPluginExtension.getSourceSets(); SourceSet mainSourceSet = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME); - SourceSet aotSourceSet = configureSourceSet(project, "aot", mainSourceSet); + SourceSet aotSourceSet = configureSourceSet(project, AOT_SOURCE_SET_NAME, mainSourceSet); SourceSet testSourceSet = sourceSets.getByName(SourceSet.TEST_SOURCE_SET_NAME); - SourceSet aotTestSourceSet = configureSourceSet(project, "aotTest", testSourceSet); + SourceSet aotTestSourceSet = configureSourceSet(project, AOT_TEST_SOURCE_SET_NAME, testSourceSet); plugins.withType(SpringBootPlugin.class).all((bootPlugin) -> { registerProcessAotTask(project, aotSourceSet, mainSourceSet); registerProcessTestAotTask(project, mainSourceSet, aotTestSourceSet, testSourceSet); @@ -126,7 +126,7 @@ private void registerProcessAotTask(Project project, SourceSet aotSourceSet, Sou .dir("generated/" + aotSourceSet.getName() + "Resources"); TaskProvider processAot = project.getTasks() .register(PROCESS_AOT_TASK_NAME, ProcessAot.class, (task) -> { - configureAotTask(project, aotSourceSet, task, mainSourceSet, resourcesOutput); + configureAotTask(project, aotSourceSet, task, resourcesOutput); task.getApplicationMainClass() .set(resolveMainClassName.flatMap(ResolveMainClassName::readMainClassName)); task.setClasspath(aotClasspath); @@ -139,7 +139,7 @@ private void registerProcessAotTask(Project project, SourceSet aotSourceSet, Sou configureDependsOn(project, aotSourceSet, processAot); } - private void configureAotTask(Project project, SourceSet sourceSet, AbstractAot task, SourceSet inputSourceSet, + private void configureAotTask(Project project, SourceSet sourceSet, AbstractAot task, Provider resourcesOutput) { task.getSourcesOutput() .set(project.getLayout().getBuildDirectory().dir("generated/" + sourceSet.getName() + "Sources")); @@ -154,7 +154,7 @@ private void configureAotTask(Project project, SourceSet sourceSet, AbstractAot private Configuration createAotProcessingClasspath(Project project, String taskName, SourceSet inputSourceSet) { Configuration base = project.getConfigurations() .getByName(inputSourceSet.getRuntimeClasspathConfigurationName()); - Configuration aotClasspath = project.getConfigurations().create(taskName + "Classpath", (classpath) -> { + return project.getConfigurations().create(taskName + "Classpath", (classpath) -> { classpath.setCanBeConsumed(false); classpath.setCanBeResolved(true); classpath.setDescription("Classpath of the " + taskName + " task."); @@ -166,7 +166,6 @@ private Configuration createAotProcessingClasspath(Project project, String taskN } }); }); - return aotClasspath; } private Stream removeDevelopmentOnly(Set configurations) { @@ -196,7 +195,7 @@ private void registerProcessTestAotTask(Project project, SourceSet mainSourceSet .dir("generated/" + aotTestSourceSet.getName() + "Resources"); TaskProvider processTestAot = project.getTasks() .register(PROCESS_TEST_AOT_TASK_NAME, ProcessTestAot.class, (task) -> { - configureAotTask(project, aotTestSourceSet, task, testSourceSet, resourcesOutput); + configureAotTask(project, aotTestSourceSet, task, resourcesOutput); task.setClasspath(aotClasspath); task.setClasspathRoots(testSourceSet.getOutput()); }); From 0588f9bf37a8aad87d595dab681068ab2ddeac01 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 8 Aug 2023 10:45:38 +0200 Subject: [PATCH 0271/1215] Use Deque instead of Stack --- .../ConfigurationMetadataAnnotationProcessor.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java index 134422b11aff..4c8b0fcb1bb4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java @@ -20,14 +20,15 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.time.Duration; +import java.util.ArrayDeque; import java.util.Arrays; import java.util.Collections; +import java.util.Deque; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.Stack; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.ProcessingEnvironment; @@ -214,10 +215,10 @@ private void processElement(Element element) { if (annotation != null) { String prefix = getPrefix(annotation); if (element instanceof TypeElement typeElement) { - processAnnotatedTypeElement(prefix, typeElement, new Stack<>()); + processAnnotatedTypeElement(prefix, typeElement, new ArrayDeque<>()); } else if (element instanceof ExecutableElement executableElement) { - processExecutableElement(prefix, executableElement, new Stack<>()); + processExecutableElement(prefix, executableElement, new ArrayDeque<>()); } } } @@ -226,13 +227,13 @@ else if (element instanceof ExecutableElement executableElement) { } } - private void processAnnotatedTypeElement(String prefix, TypeElement element, Stack seen) { + private void processAnnotatedTypeElement(String prefix, TypeElement element, Deque seen) { String type = this.metadataEnv.getTypeUtils().getQualifiedName(element); this.metadataCollector.add(ItemMetadata.newGroup(prefix, type, type, null)); processTypeElement(prefix, element, null, seen); } - private void processExecutableElement(String prefix, ExecutableElement element, Stack seen) { + private void processExecutableElement(String prefix, ExecutableElement element, Deque seen) { if ((!element.getModifiers().contains(Modifier.PRIVATE)) && (TypeKind.VOID != element.getReturnType().getKind())) { Element returns = this.processingEnv.getTypeUtils().asElement(element.getReturnType()); @@ -255,7 +256,7 @@ private void processExecutableElement(String prefix, ExecutableElement element, } private void processTypeElement(String prefix, TypeElement element, ExecutableElement source, - Stack seen) { + Deque seen) { if (!seen.contains(element)) { seen.push(element); new PropertyDescriptorResolver(this.metadataEnv).resolve(element, source).forEach((descriptor) -> { From 4aaca291dec2adc939965c0d334cd90f45ed3c7b Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 8 Aug 2023 11:07:30 +0200 Subject: [PATCH 0272/1215] Remove MD5 from RandomValuePropertySource Before that change, we get 32 random bytes, and then used MD5 on them to get a hex string. This removes the MD5, we now get 128 bits (output size of MD5) of random bytes directly. --- .../boot/env/RandomValuePropertySource.java | 9 +++++---- .../boot/env/RandomValuePropertySourceTests.java | 6 ++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/RandomValuePropertySource.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/RandomValuePropertySource.java index 7c2f23f63153..b01f16fb3d6f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/RandomValuePropertySource.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/RandomValuePropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -16,6 +16,7 @@ package org.springframework.boot.env; +import java.util.HexFormat; import java.util.OptionalInt; import java.util.OptionalLong; import java.util.Random; @@ -31,7 +32,6 @@ import org.springframework.core.env.StandardEnvironment; import org.springframework.core.log.LogMessage; import org.springframework.util.Assert; -import org.springframework.util.DigestUtils; import org.springframework.util.StringUtils; /** @@ -58,6 +58,7 @@ * @author Dave Syer * @author Matt Benson * @author Madhura Bhave + * @author Moritz Halbritter * @since 1.0.0 */ public class RandomValuePropertySource extends PropertySource { @@ -136,9 +137,9 @@ private void assertPresent(boolean present, Range range) { } private Object getRandomBytes() { - byte[] bytes = new byte[32]; + byte[] bytes = new byte[16]; getSource().nextBytes(bytes); - return DigestUtils.md5DigestAsHex(bytes); + return HexFormat.of().withLowerCase().formatHex(bytes); } public static void addToEnvironment(ConfigurableEnvironment environment) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/RandomValuePropertySourceTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/RandomValuePropertySourceTests.java index 77732be5dbd9..1fb9ad176d81 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/RandomValuePropertySourceTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/env/RandomValuePropertySourceTests.java @@ -37,6 +37,7 @@ * * @author Dave Syer * @author Matt Benson + * @author Moritz Halbritter */ class RandomValuePropertySourceTests { @@ -192,4 +193,9 @@ void addToEnvironmentAddsAfterSystemEnvironment() { RandomValuePropertySource.RANDOM_PROPERTY_SOURCE_NAME, "mockProperties"); } + @Test + void randomStringIs32CharsLong() { + assertThat(this.source.getProperty("random.string")).asString().hasSize(32); + } + } From e89e0c895657540b237088948738823a46115362 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Fri, 4 Aug 2023 13:44:45 +0200 Subject: [PATCH 0273/1215] Start build against Reactor Bom 2023.0.0-M2 snapshots See gh-36677 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 8c1784864035..869371d32cfc 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1206,7 +1206,7 @@ bom { ] } } - library("Reactor Bom", "2023.0.0-M1") { + library("Reactor Bom", "2023.0.0-SNAPSHOT") { group("io.projectreactor") { imports = [ "reactor-bom" From 7454368e1efdc284813d0cd2c1106c5c84eda63e Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Fri, 4 Aug 2023 13:44:01 +0200 Subject: [PATCH 0274/1215] Start build against Micrometer 1.12.0-M2 snapshots See gh-36675 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 869371d32cfc..7c0888bfd878 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -973,7 +973,7 @@ bom { ] } } - library("Micrometer", "1.12.0-M1") { + library("Micrometer", "1.12.0-SNAPSHOT") { group("io.micrometer") { modules = [ "micrometer-registry-stackdriver" { From 57eee66b8e4c972b4b1c8acc29ca345010e9e1b6 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Fri, 4 Aug 2023 13:44:23 +0200 Subject: [PATCH 0275/1215] Start build against Micrometer Tracing 1.12.0-M2 snapshots See gh-36676 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 7c0888bfd878..869aef50f826 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -985,7 +985,7 @@ bom { ] } } - library("Micrometer Tracing", "1.2.0-M1") { + library("Micrometer Tracing", "1.2.0-SNAPSHOT") { group("io.micrometer") { imports = [ "micrometer-tracing-bom" From b46e81c230595cd1b7cf345f9a31f5a137348276 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 1 Aug 2023 14:49:15 +0200 Subject: [PATCH 0276/1215] Start building against Spring Framework 6.1.0-M4 snapshots See gh-36678 --- gradle.properties | 2 +- .../boot/test/context/SpringBootContextLoader.java | 4 ++-- .../boot/test/context/SpringBootTestContextBootstrapper.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index c0cea14c2595..4a6916a666ae 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 kotlinVersion=1.9.0 nativeBuildToolsVersion=0.9.23 -springFrameworkVersion=6.1.0-M3 +springFrameworkVersion=6.1.0-SNAPSHOT tomcatVersion=10.1.11 kotlin.stdlib.default.dependency=false diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java index a07e13d1c09e..00d9c714eb94 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java @@ -230,8 +230,8 @@ private void prepareEnvironment(MergedContextConfiguration mergedConfig, SpringA setActiveProfiles(environment, mergedConfig.getActiveProfiles(), applicationEnvironment); ResourceLoader resourceLoader = (application.getResourceLoader() != null) ? application.getResourceLoader() : new DefaultResourceLoader(null); - TestPropertySourceUtils.addPropertiesFilesToEnvironment(environment, resourceLoader, - mergedConfig.getPropertySourceLocations()); + TestPropertySourceUtils.addPropertySourcesToEnvironment(environment, resourceLoader, + mergedConfig.getPropertySourceDescriptors()); TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment, getInlinedProperties(mergedConfig)); } diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java index ce07386b1a76..40970888546f 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java @@ -376,7 +376,7 @@ protected final MergedContextConfiguration createModifiedConfig(MergedContextCon contextCustomizers.add(new SpringBootTestAnnotation(mergedConfig.getTestClass())); return new MergedContextConfiguration(mergedConfig.getTestClass(), mergedConfig.getLocations(), classes, mergedConfig.getContextInitializerClasses(), mergedConfig.getActiveProfiles(), - mergedConfig.getPropertySourceLocations(), propertySourceProperties, contextCustomizers, + mergedConfig.getPropertySourceDescriptors(), propertySourceProperties, contextCustomizers, mergedConfig.getContextLoader(), getCacheAwareContextLoaderDelegate(), mergedConfig.getParent()); } From 421448233fcead63366db296e80cf2f9d50dd2bd Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 2 Aug 2023 12:41:17 +0200 Subject: [PATCH 0277/1215] Remove invalid check for String-based FactoryBean.OBJECT_TYPE_ATTRIBUTE Closes gh-36659 --- .../boot/test/mock/mockito/MockitoPostProcessor.java | 3 +-- .../test/mock/mockito/MockitoPostProcessorTests.java | 12 ------------ 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java index 6ccc03f142cd..334d90dcfb1a 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessor.java @@ -252,12 +252,11 @@ private Set getExistingBeans(ConfigurableListableBeanFactory beanFactory Set beans = new LinkedHashSet<>( Arrays.asList(beanFactory.getBeanNamesForType(resolvableType, true, false))); Class type = resolvableType.resolve(Object.class); - String typeName = type.getName(); for (String beanName : beanFactory.getBeanNamesForType(FactoryBean.class, true, false)) { beanName = BeanFactoryUtils.transformedBeanName(beanName); BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName); Object attribute = beanDefinition.getAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE); - if (resolvableType.equals(attribute) || type.equals(attribute) || typeName.equals(attribute)) { + if (resolvableType.equals(attribute) || type.equals(attribute)) { beans.add(beanName); } } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessorTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessorTests.java index 99969dadd9c9..5bb4bace7047 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessorTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/mock/mockito/MockitoPostProcessorTests.java @@ -73,18 +73,6 @@ void cannotMockMultipleQualifiedBeans() { + " expected a single matching bean to replace but found [example1, example3]"); } - @Test - void canMockBeanProducedByFactoryBeanWithStringObjectTypeAttribute() { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - MockitoPostProcessor.register(context); - RootBeanDefinition factoryBeanDefinition = new RootBeanDefinition(TestFactoryBean.class); - factoryBeanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, SomeInterface.class.getName()); - context.registerBeanDefinition("beanToBeMocked", factoryBeanDefinition); - context.register(MockedFactoryBean.class); - context.refresh(); - assertThat(Mockito.mockingDetails(context.getBean("beanToBeMocked")).isMock()).isTrue(); - } - @Test void canMockBeanProducedByFactoryBeanWithClassObjectTypeAttribute() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); From 309ea53ff3a47142ac73094cbbfea1bc6720eee2 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 2 Aug 2023 12:43:26 +0200 Subject: [PATCH 0278/1215] Start build against Spring Data 2023.1.0-M2 snapshots See gh-36680 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 869aef50f826..1504e15326ee 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1390,7 +1390,7 @@ bom { ] } } - library("Spring Data Bom", "2023.1.0-M1") { + library("Spring Data Bom", "2023.1.0-SNAPSHOT") { group("org.springframework.data") { imports = [ "spring-data-bom" From 5544ccdcbaf3d12516b82d7b284bbb19bd00cd96 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Fri, 4 Aug 2023 13:45:15 +0200 Subject: [PATCH 0279/1215] Start build against Spring LDAP 3.2.0-M2 snapshots See gh-36679 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 1504e15326ee..cea1d2a5ea19 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1434,7 +1434,7 @@ bom { ] } } - library("Spring LDAP", "3.2.0-M1") { + library("Spring LDAP", "3.2.0-SNAPSHOT") { group("org.springframework.ldap") { modules = [ "spring-ldap-core", From 38dbc644aeb32eb2434d71beeec23a5bfc8dbca7 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 1 Aug 2023 15:21:34 +0200 Subject: [PATCH 0280/1215] Add auto-configuration for JdbcClient Closes gh-36579 --- .../jdbc/JdbcClientAutoConfiguration.java | 46 ++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../JdbcClientAutoConfigurationTests.java | 100 ++++++++++++++++++ .../src/docs/asciidoc/data/sql.adoc | 13 ++- .../asciidoc/howto/data-initialization.adoc | 1 + .../boot/docs/data/sql/jdbcclient/MyBean.java | 35 ++++++ ...pendsOnDatabaseInitializationDetector.java | 7 +- 7 files changed, 198 insertions(+), 5 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/sql/jdbcclient/MyBean.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfiguration.java new file mode 100644 index 000000000000..9b78ee8e9d09 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfiguration.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2023 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.jdbc; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.JdbcClient; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for {@link JdbcClient}. + * + * @author Stephane Nicoll + * @since 3.2.0 + */ +@AutoConfiguration(after = JdbcTemplateAutoConfiguration.class) +@ConditionalOnSingleCandidate(NamedParameterJdbcTemplate.class) +@ConditionalOnMissingBean(JdbcClient.class) +@Import(DatabaseInitializationDependencyConfigurer.class) +public class JdbcClientAutoConfiguration { + + @Bean + JdbcClient jdbcClient(NamedParameterJdbcTemplate jdbcTemplate) { + return JdbcClient.create(jdbcTemplate); + } + +} 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 7bd25ab47168..972dad6815eb 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 @@ -68,6 +68,7 @@ org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration +org.springframework.boot.autoconfigure.jdbc.JdbcClientAutoConfiguration org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfigurationTests.java new file mode 100644 index 000000000000..4fa13d6922ee --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfigurationTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-2023 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.jdbc; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; +import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.JdbcClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link JdbcClientAutoConfiguration}. + * + * @author Stephane Nicoll + */ +class JdbcClientAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.datasource.generate-unique-name=true") + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, JdbcTemplateAutoConfiguration.class, + JdbcClientAutoConfiguration.class)); + + @Test + void jdbcClientWhenNoAvailableJdbcTemplateIsNotCreated() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .run((context) -> assertThat(context).doesNotHaveBean(JdbcClient.class)); + } + + @Test + void jdbcClientWhenExistingJdbcTemplateIsCreated() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(JdbcClient.class); + NamedParameterJdbcTemplate namedParameterJdbcTemplate = context.getBean(NamedParameterJdbcTemplate.class); + assertThat(namedParameterJdbcTemplate.getJdbcOperations()).isEqualTo(context.getBean(JdbcOperations.class)); + }); + } + + @Test + void jdbcClientWithCustomJdbcClientIsNotCreated() { + this.contextRunner.withBean("customJdbcClient", JdbcClient.class, () -> mock(JdbcClient.class)) + .run((context) -> { + assertThat(context).hasSingleBean(JdbcClient.class); + assertThat(context.getBean(JdbcClient.class)).isEqualTo(context.getBean("customJdbcClient")); + }); + } + + @Test + void jdbcClientIsOrderedAfterFlywayMigration() { + this.contextRunner.withUserConfiguration(JdbcClientDataSourceMigrationValidator.class) + .withPropertyValues("spring.flyway.locations:classpath:db/city") + .withConfiguration(AutoConfigurations.of(FlywayAutoConfiguration.class)) + .run((context) -> { + assertThat(context).hasNotFailed().hasSingleBean(JdbcClient.class); + assertThat(context.getBean(JdbcClientDataSourceMigrationValidator.class).count).isZero(); + }); + } + + @Test + void jdbcClientIsOrderedAfterLiquibaseMigration() { + this.contextRunner.withUserConfiguration(JdbcClientDataSourceMigrationValidator.class) + .withPropertyValues("spring.liquibase.changeLog:classpath:db/changelog/db.changelog-city.yaml") + .withConfiguration(AutoConfigurations.of(LiquibaseAutoConfiguration.class)) + .run((context) -> { + assertThat(context).hasNotFailed().hasSingleBean(JdbcClient.class); + assertThat(context.getBean(JdbcClientDataSourceMigrationValidator.class).count).isZero(); + }); + } + + static class JdbcClientDataSourceMigrationValidator { + + private final Integer count; + + JdbcClientDataSourceMigrationValidator(JdbcClient jdbcClient) { + this.count = jdbcClient.sql("SELECT COUNT(*) from CITY").query().singleValue(Integer.class); + } + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/sql.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/sql.adoc index 86fc5ac8016f..9bf0916b1156 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/sql.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/sql.adoc @@ -1,6 +1,6 @@ [[data.sql]] == SQL Databases -The {spring-framework}[Spring Framework] provides extensive support for working with SQL databases, from direct JDBC access using `JdbcTemplate` to complete "`object relational mapping`" technologies such as Hibernate. +The {spring-framework}[Spring Framework] provides extensive support for working with SQL databases, from direct JDBC access using `JdbcClient` or `JdbcTemplate` to complete "`object relational mapping`" technologies such as Hibernate. {spring-data}[Spring Data] provides an additional level of functionality: creating `Repository` implementations directly from interfaces and using conventions to generate queries from your method names. @@ -176,6 +176,17 @@ If more than one `JdbcTemplate` is defined and no primary candidate exists, the +[[data.sql.jdbc-client]] +=== Using JdbcClient +Spring's `JdbcClient` is auto-configured based on the presence of a `NamedParameterJdbcTemplate`. +You can inject it directly in your own beans as well, as shown in the following example: + +include::code:MyBean[] + +If you rely on auto-configuration to create the underlying `JdbcTemplate`, any customization using `spring.jdbc.template.*` properties are taken into account in the client as well. + + + [[data.sql.jpa-and-spring-data]] === JPA and Spring Data JPA The Java Persistence API is a standard technology that lets you "`map`" objects to relational databases. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc index 4b42e76843fb..ca6181f8da38 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc @@ -217,6 +217,7 @@ Spring Boot will automatically detect beans of the following types that depends - `AbstractEntityManagerFactoryBean` (unless configprop:spring.jpa.defer-datasource-initialization[] is set to `true`) - `DSLContext` (jOOQ) - `EntityManagerFactory` (unless configprop:spring.jpa.defer-datasource-initialization[] is set to `true`) +- `JdbcClient` - `JdbcOperations` - `NamedParameterJdbcOperations` diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/sql/jdbcclient/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/sql/jdbcclient/MyBean.java new file mode 100644 index 000000000000..4441070de213 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/sql/jdbcclient/MyBean.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2023 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.docs.data.sql.jdbcclient; + +import org.springframework.jdbc.core.simple.JdbcClient; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final JdbcClient jdbcClient; + + public MyBean(JdbcClient jdbcClient) { + this.jdbcClient = jdbcClient; + } + + public void doSomething() { + /* @chomp:line this.jdbcClient ... */ this.jdbcClient.sql("delete from customer").update(); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/SpringJdbcDependsOnDatabaseInitializationDetector.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/SpringJdbcDependsOnDatabaseInitializationDetector.java index 13ed613745a6..dedfa0004070 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/SpringJdbcDependsOnDatabaseInitializationDetector.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/SpringJdbcDependsOnDatabaseInitializationDetector.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -16,14 +16,13 @@ package org.springframework.boot.jdbc; -import java.util.Arrays; -import java.util.HashSet; import java.util.Set; import org.springframework.boot.sql.init.dependency.AbstractBeansOfTypeDependsOnDatabaseInitializationDetector; import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.core.simple.JdbcClient; /** * {@link DependsOnDatabaseInitializationDetector} for Spring Framework's JDBC support. @@ -35,7 +34,7 @@ class SpringJdbcDependsOnDatabaseInitializationDetector @Override protected Set> getDependsOnDatabaseInitializationBeanTypes() { - return new HashSet<>(Arrays.asList(JdbcOperations.class, NamedParameterJdbcOperations.class)); + return Set.of(JdbcClient.class, JdbcOperations.class, NamedParameterJdbcOperations.class); } } From 5b00d5f89b685bc66b471e9eb198867e07557496 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 1 Aug 2023 15:00:44 +0200 Subject: [PATCH 0281/1215] Auto-configure SimpleAsyncTaskScheduler when virtual threads are enabled This auto-configures a new SimpleAsyncTaskSchedulerBuilder bean in the context. This bean is configured to use virtual threads, if enabled. SimpleAsyncTaskSchedulerCustomizers can be used to customize the built SimpleAsyncTaskScheduler. If virtual threads are enabled, the application task scheduler is configured to be a SimpleAsyncTaskScheduler. Adds a new configuration property spring.task.scheduling.simple .concurrency-limit Closes gh-36609 --- .../task/TaskSchedulingAutoConfiguration.java | 3 +- .../task/TaskSchedulingConfigurations.java | 54 ++++- .../task/TaskSchedulingProperties.java | 26 ++- .../TaskSchedulingAutoConfigurationTests.java | 57 ++++++ .../task-execution-and-scheduling.adoc | 11 +- .../task/SimpleAsyncTaskSchedulerBuilder.java | 193 ++++++++++++++++++ .../SimpleAsyncTaskSchedulerCustomizer.java | 36 ++++ .../SimpleAsyncTaskSchedulerBuilderTests.java | 136 ++++++++++++ 8 files changed, 509 insertions(+), 7 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilder.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerCustomizer.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilderTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java index 5d14f7bcdc8a..5909153ee8e3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfiguration.java @@ -40,7 +40,8 @@ @EnableConfigurationProperties(TaskSchedulingProperties.class) @Import({ TaskSchedulingConfigurations.ThreadPoolTaskSchedulerBuilderConfiguration.class, TaskSchedulingConfigurations.TaskSchedulerBuilderConfiguration.class, - TaskSchedulingConfigurations.ThreadPoolTaskSchedulerConfiguration.class }) + TaskSchedulingConfigurations.SimpleAsyncTaskSchedulerBuilderConfiguration.class, + TaskSchedulingConfigurations.TaskSchedulerConfiguration.class }) public class TaskSchedulingAutoConfiguration { @Bean diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java index 5375620e3444..d05dc9a91ef7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java @@ -21,6 +21,10 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading; +import org.springframework.boot.autoconfigure.thread.Threading; +import org.springframework.boot.task.SimpleAsyncTaskSchedulerBuilder; +import org.springframework.boot.task.SimpleAsyncTaskSchedulerCustomizer; import org.springframework.boot.task.TaskSchedulerBuilder; import org.springframework.boot.task.TaskSchedulerCustomizer; import org.springframework.boot.task.ThreadPoolTaskSchedulerBuilder; @@ -28,6 +32,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.scheduling.config.TaskManagementConfigUtils; @@ -43,9 +48,16 @@ class TaskSchedulingConfigurations { @ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME) @ConditionalOnMissingBean({ TaskScheduler.class, ScheduledExecutorService.class }) @SuppressWarnings("removal") - static class ThreadPoolTaskSchedulerConfiguration { + static class TaskSchedulerConfiguration { + + @Bean(name = "taskScheduler") + @ConditionalOnThreading(Threading.VIRTUAL) + SimpleAsyncTaskScheduler taskSchedulerVirtualThreads(SimpleAsyncTaskSchedulerBuilder builder) { + return builder.build(); + } @Bean + @ConditionalOnThreading(Threading.PLATFORM) ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder taskSchedulerBuilder, ObjectProvider threadPoolTaskSchedulerBuilderProvider) { ThreadPoolTaskSchedulerBuilder threadPoolTaskSchedulerBuilder = threadPoolTaskSchedulerBuilderProvider @@ -105,4 +117,44 @@ private ThreadPoolTaskSchedulerCustomizer adapt(TaskSchedulerCustomizer customiz } + @Configuration(proxyBeanMethods = false) + static class SimpleAsyncTaskSchedulerBuilderConfiguration { + + private final TaskSchedulingProperties properties; + + private final ObjectProvider taskSchedulerCustomizers; + + SimpleAsyncTaskSchedulerBuilderConfiguration(TaskSchedulingProperties properties, + ObjectProvider taskSchedulerCustomizers) { + this.properties = properties; + this.taskSchedulerCustomizers = taskSchedulerCustomizers; + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnThreading(Threading.PLATFORM) + SimpleAsyncTaskSchedulerBuilder simpleAsyncTaskSchedulerBuilder() { + return builder(); + } + + @Bean(name = "simpleAsyncTaskSchedulerBuilder") + @ConditionalOnMissingBean + @ConditionalOnThreading(Threading.VIRTUAL) + SimpleAsyncTaskSchedulerBuilder simpleAsyncTaskSchedulerBuilderVirtualThreads() { + SimpleAsyncTaskSchedulerBuilder builder = builder(); + builder = builder.virtualThreads(true); + return builder; + } + + private SimpleAsyncTaskSchedulerBuilder builder() { + SimpleAsyncTaskSchedulerBuilder builder = new SimpleAsyncTaskSchedulerBuilder(); + builder = builder.threadNamePrefix(this.properties.getThreadNamePrefix()); + builder = builder.customizers(this.taskSchedulerCustomizers.orderedStream()::iterator); + TaskSchedulingProperties.Simple simple = this.properties.getSimple(); + builder = builder.concurrencyLimit(simple.getConcurrencyLimit()); + return builder; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingProperties.java index f9bc7beac2c1..ea26f3261039 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. @@ -31,6 +31,8 @@ public class TaskSchedulingProperties { private final Pool pool = new Pool(); + private final Simple simple = new Simple(); + private final Shutdown shutdown = new Shutdown(); /** @@ -42,6 +44,10 @@ public Pool getPool() { return this.pool; } + public Simple getSimple() { + return this.simple; + } + public Shutdown getShutdown() { return this.shutdown; } @@ -71,6 +77,24 @@ public void setSize(int size) { } + public static class Simple { + + /** + * Set the maximum number of parallel accesses allowed. -1 indicates no + * concurrency limit at all. + */ + private Integer concurrencyLimit; + + public Integer getConcurrencyLimit() { + return this.concurrencyLimit; + } + + public void setConcurrencyLimit(Integer concurrencyLimit) { + this.concurrencyLimit = concurrencyLimit; + } + + } + public static class Shutdown { /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java index 0898ec2beeb9..5d0cd515e889 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java @@ -28,9 +28,13 @@ import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import org.springframework.boot.LazyInitializationBeanFactoryPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.task.SimpleAsyncTaskSchedulerBuilder; +import org.springframework.boot.task.SimpleAsyncTaskSchedulerCustomizer; import org.springframework.boot.task.TaskSchedulerBuilder; import org.springframework.boot.task.TaskSchedulerCustomizer; import org.springframework.boot.task.ThreadPoolTaskSchedulerBuilder; @@ -45,6 +49,7 @@ import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -110,6 +115,58 @@ void enableSchedulingWithNoTaskExecutorAutoConfiguresOne() { }); } + @Test + void simpleAsyncTaskSchedulerBuilderShouldReadProperties() { + this.contextRunner + .withPropertyValues("spring.task.scheduling.simple.concurrency-limit=1", + "spring.task.scheduling.thread-name-prefix=scheduling-test-") + .withUserConfiguration(SchedulingConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(SimpleAsyncTaskSchedulerBuilder.class); + SimpleAsyncTaskSchedulerBuilder builder = context.getBean(SimpleAsyncTaskSchedulerBuilder.class); + assertThat(builder).hasFieldOrPropertyWithValue("threadNamePrefix", "scheduling-test-"); + assertThat(builder).hasFieldOrPropertyWithValue("concurrencyLimit", 1); + }); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void simpleAsyncTaskSchedulerBuilderShouldUseVirtualThreadsIfEnabled() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true") + .withUserConfiguration(SchedulingConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(SimpleAsyncTaskSchedulerBuilder.class); + SimpleAsyncTaskSchedulerBuilder builder = context.getBean(SimpleAsyncTaskSchedulerBuilder.class); + assertThat(builder).hasFieldOrPropertyWithValue("virtualThreads", true); + }); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void simpleAsyncTaskSchedulerBuilderShouldUsePlatformThreadsByDefault() { + this.contextRunner.withUserConfiguration(SchedulingConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(SimpleAsyncTaskSchedulerBuilder.class); + SimpleAsyncTaskSchedulerBuilder builder = context.getBean(SimpleAsyncTaskSchedulerBuilder.class); + assertThat(builder).hasFieldOrPropertyWithValue("virtualThreads", null); + }); + } + + @Test + @SuppressWarnings("unchecked") + void simpleAsyncTaskSchedulerBuilderShouldApplyCustomizers() { + SimpleAsyncTaskSchedulerCustomizer customizer = (scheduler) -> { + }; + this.contextRunner.withBean(SimpleAsyncTaskSchedulerCustomizer.class, () -> customizer) + .withUserConfiguration(SchedulingConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(SimpleAsyncTaskSchedulerBuilder.class); + SimpleAsyncTaskSchedulerBuilder builder = context.getBean(SimpleAsyncTaskSchedulerBuilder.class); + Set customizers = (Set) ReflectionTestUtils + .getField(builder, "customizers"); + assertThat(customizers).as("SimpleAsyncTaskSchedulerBuilder.customizers").contains(customizer); + }); + } + @Test void enableSchedulingWithNoTaskExecutorAppliesTaskSchedulerCustomizers() { this.contextRunner.withPropertyValues("spring.task.scheduling.thread-name-prefix=scheduling-test-") diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc index 5b3ed7f031df..1b5e8130d5d8 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc @@ -36,8 +36,11 @@ Those default settings can be fine-tuned using the `spring.task.execution` names This changes the thread pool to use a bounded queue so that when the queue is full (100 tasks), the thread pool increases to maximum 16 threads. Shrinking of the pool is more aggressive as threads are reclaimed when they are idle for 10 seconds (rather than 60 seconds by default). -A `ThreadPoolTaskScheduler` can also be auto-configured if need to be associated to scheduled task execution (using `@EnableScheduling` for instance). -The thread pool uses one thread by default and its settings can be fine-tuned using the `spring.task.scheduling` namespace, as shown in the following example: +A scheduler can also be auto-configured if need to be associated to scheduled task execution (using `@EnableScheduling` for instance). +When virtual threads are enabled (using Java 21+ and configprop:spring.threads.virtual.enabled[] set to `true`) this will be a `SimpleAsyncTaskScheduler` that uses virtual threads. +Otherwise, it will be a `ThreadPoolTaskScheduler` with sensible defaults. + +The `ThreadPoolTaskScheduler` uses one thread by default and its settings can be fine-tuned using the `spring.task.scheduling` namespace, as shown in the following example: [source,yaml,indent=0,subs="verbatim",configprops,configblocks] ---- @@ -49,5 +52,5 @@ The thread pool uses one thread by default and its settings can be fine-tuned us size: 2 ---- -A `ThreadPoolTaskExecutorBuilder` bean, a `SimpleAsyncTaskExecutorBuilder` bean and a `ThreadPoolTaskSchedulerBuilder` bean are made available in the context if a custom executor or scheduler needs to be created. -The `SimpleAsyncTaskExecutorBuilder` is auto-configured to use virtual threads if they are enabled (using Java 21+ and configprop:spring.threads.virtual.enabled[] set to `true`). +A `ThreadPoolTaskExecutorBuilder` bean, a `SimpleAsyncTaskExecutorBuilder` bean, a `ThreadPoolTaskSchedulerBuilder` bean and a `SimpleAsyncTaskSchedulerBuilder` are made available in the context if a custom executor or scheduler needs to be created. +The `SimpleAsyncTaskExecutorBuilder` and `SimpleAsyncTaskSchedulerBuilder` beans are auto-configured to use virtual threads if they are enabled (using Java 21+ and configprop:spring.threads.virtual.enabled[] set to `true`). diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilder.java new file mode 100644 index 000000000000..6c478250e1c8 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilder.java @@ -0,0 +1,193 @@ +/* + * Copyright 2012-2023 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.task; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Builder that can be used to configure and create a {@link SimpleAsyncTaskScheduler}. + * Provides convenience methods to set common {@link SimpleAsyncTaskScheduler} settings. + * For advanced configuration, consider using {@link SimpleAsyncTaskSchedulerCustomizer}. + *

    + * In a typical auto-configured Spring Boot application this builder is available as a + * bean and can be injected whenever a {@link SimpleAsyncTaskScheduler} is needed. + * + * @author Stephane Nicoll + * @author Moritz Halbritter + * @since 3.2.0 + */ +public class SimpleAsyncTaskSchedulerBuilder { + + private final String threadNamePrefix; + + private final Integer concurrencyLimit; + + private final Boolean virtualThreads; + + private final Set customizers; + + public SimpleAsyncTaskSchedulerBuilder() { + this.threadNamePrefix = null; + this.customizers = null; + this.concurrencyLimit = null; + this.virtualThreads = null; + } + + private SimpleAsyncTaskSchedulerBuilder(String threadNamePrefix, Integer concurrencyLimit, Boolean virtualThreads, + Set taskSchedulerCustomizers) { + this.threadNamePrefix = threadNamePrefix; + this.concurrencyLimit = concurrencyLimit; + this.virtualThreads = virtualThreads; + this.customizers = taskSchedulerCustomizers; + } + + /** + * Set the prefix to use for the names of newly created threads. + * @param threadNamePrefix the thread name prefix to set + * @return a new builder instance + */ + public SimpleAsyncTaskSchedulerBuilder threadNamePrefix(String threadNamePrefix) { + return new SimpleAsyncTaskSchedulerBuilder(threadNamePrefix, this.concurrencyLimit, this.virtualThreads, + this.customizers); + } + + /** + * Set the concurrency limit. + * @param concurrencyLimit the concurrency limit + * @return a new builder instance + */ + public SimpleAsyncTaskSchedulerBuilder concurrencyLimit(Integer concurrencyLimit) { + return new SimpleAsyncTaskSchedulerBuilder(this.threadNamePrefix, concurrencyLimit, this.virtualThreads, + this.customizers); + } + + /** + * Set whether to use virtual threads. + * @param virtualThreads whether to use virtual threads + * @return a new builder instance + */ + public SimpleAsyncTaskSchedulerBuilder virtualThreads(Boolean virtualThreads) { + return new SimpleAsyncTaskSchedulerBuilder(this.threadNamePrefix, this.concurrencyLimit, virtualThreads, + this.customizers); + } + + /** + * Set the {@link SimpleAsyncTaskSchedulerCustomizer + * threadPoolTaskSchedulerCustomizers} that should be applied to the + * {@link SimpleAsyncTaskScheduler}. Customizers are applied in the order that they + * were added after builder configuration has been applied. Setting this value will + * replace any previously configured customizers. + * @param customizers the customizers to set + * @return a new builder instance + * @see #additionalCustomizers(SimpleAsyncTaskSchedulerCustomizer...) + */ + public SimpleAsyncTaskSchedulerBuilder customizers(SimpleAsyncTaskSchedulerCustomizer... customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return customizers(Arrays.asList(customizers)); + } + + /** + * Set the {@link SimpleAsyncTaskSchedulerCustomizer + * threadPoolTaskSchedulerCustomizers} that should be applied to the + * {@link SimpleAsyncTaskScheduler}. Customizers are applied in the order that they + * were added after builder configuration has been applied. Setting this value will + * replace any previously configured customizers. + * @param customizers the customizers to set + * @return a new builder instance + * @see #additionalCustomizers(SimpleAsyncTaskSchedulerCustomizer...) + */ + public SimpleAsyncTaskSchedulerBuilder customizers( + Iterable customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return new SimpleAsyncTaskSchedulerBuilder(this.threadNamePrefix, this.concurrencyLimit, this.virtualThreads, + append(null, customizers)); + } + + /** + * Add {@link SimpleAsyncTaskSchedulerCustomizer threadPoolTaskSchedulerCustomizers} + * that should be applied to the {@link SimpleAsyncTaskScheduler}. Customizers are + * applied in the order that they were added after builder configuration has been + * applied. + * @param customizers the customizers to add + * @return a new builder instance + * @see #customizers(SimpleAsyncTaskSchedulerCustomizer...) + */ + public SimpleAsyncTaskSchedulerBuilder additionalCustomizers(SimpleAsyncTaskSchedulerCustomizer... customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return additionalCustomizers(Arrays.asList(customizers)); + } + + /** + * Add {@link SimpleAsyncTaskSchedulerCustomizer threadPoolTaskSchedulerCustomizers} + * that should be applied to the {@link SimpleAsyncTaskScheduler}. Customizers are + * applied in the order that they were added after builder configuration has been + * applied. + * @param customizers the customizers to add + * @return a new builder instance + * @see #customizers(SimpleAsyncTaskSchedulerCustomizer...) + */ + public SimpleAsyncTaskSchedulerBuilder additionalCustomizers( + Iterable customizers) { + Assert.notNull(customizers, "Customizers must not be null"); + return new SimpleAsyncTaskSchedulerBuilder(this.threadNamePrefix, this.concurrencyLimit, this.virtualThreads, + append(this.customizers, customizers)); + } + + /** + * Build a new {@link SimpleAsyncTaskScheduler} instance and configure it using this + * builder. + * @return a configured {@link SimpleAsyncTaskScheduler} instance. + * @see #configure(SimpleAsyncTaskScheduler) + */ + public SimpleAsyncTaskScheduler build() { + return configure(new SimpleAsyncTaskScheduler()); + } + + /** + * Configure the provided {@link SimpleAsyncTaskScheduler} instance using this + * builder. + * @param the type of task scheduler + * @param taskScheduler the {@link SimpleAsyncTaskScheduler} to configure + * @return the task scheduler instance + * @see #build() + */ + public T configure(T taskScheduler) { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(this.threadNamePrefix).to(taskScheduler::setThreadNamePrefix); + map.from(this.concurrencyLimit).to(taskScheduler::setConcurrencyLimit); + map.from(this.virtualThreads).to(taskScheduler::setVirtualThreads); + if (!CollectionUtils.isEmpty(this.customizers)) { + this.customizers.forEach((customizer) -> customizer.customize(taskScheduler)); + } + return taskScheduler; + } + + private Set append(Set set, Iterable additions) { + Set result = new LinkedHashSet<>((set != null) ? set : Collections.emptySet()); + additions.forEach(result::add); + return Collections.unmodifiableSet(result); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerCustomizer.java new file mode 100644 index 000000000000..e66c627327d4 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerCustomizer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2023 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.task; + +import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler; + +/** + * Callback interface that can be used to customize a {@link SimpleAsyncTaskScheduler}. + * + * @author Moritz Halbritter + * @since 3.2.0 + */ +@FunctionalInterface +public interface SimpleAsyncTaskSchedulerCustomizer { + + /** + * Callback to customize a {@link SimpleAsyncTaskScheduler} instance. + * @param taskScheduler the task scheduler to customize + */ + void customize(SimpleAsyncTaskScheduler taskScheduler); + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilderTests.java new file mode 100644 index 000000000000..440a42c0d6a4 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilderTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2012-2023 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.task; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; + +import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +/** + * Tests for {@link SimpleAsyncTaskSchedulerBuilder}. + * + * @author Stephane Nicoll + * @author Moritz Halbritter + */ +class SimpleAsyncTaskSchedulerBuilderTests { + + private final SimpleAsyncTaskSchedulerBuilder builder = new SimpleAsyncTaskSchedulerBuilder(); + + @Test + void threadNamePrefixShouldApply() { + SimpleAsyncTaskScheduler scheduler = this.builder.threadNamePrefix("test-").build(); + assertThat(scheduler.getThreadNamePrefix()).isEqualTo("test-"); + } + + @Test + void concurrencyLimitShouldApply() { + SimpleAsyncTaskScheduler scheduler = this.builder.concurrencyLimit(1).build(); + assertThat(scheduler.getConcurrencyLimit()).isEqualTo(1); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void virtualThreadsShouldApply() { + SimpleAsyncTaskScheduler scheduler = this.builder.virtualThreads(true).build(); + Field field = ReflectionUtils.findField(SimpleAsyncTaskScheduler.class, "virtualThreadDelegate"); + assertThat(field).as("SimpleAsyncTaskScheduler.virtualThreadDelegate").isNotNull(); + field.setAccessible(true); + Object virtualThreadDelegate = ReflectionUtils.getField(field, scheduler); + assertThat(virtualThreadDelegate).as("SimpleAsyncTaskScheduler.virtualThreadDelegate").isNotNull(); + } + + @Test + void customizersWhenCustomizersAreNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.builder.customizers((SimpleAsyncTaskSchedulerCustomizer[]) null)) + .withMessageContaining("Customizers must not be null"); + } + + @Test + void customizersCollectionWhenCustomizersAreNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.builder.customizers((Set) null)) + .withMessageContaining("Customizers must not be null"); + } + + @Test + void customizersShouldApply() { + SimpleAsyncTaskSchedulerCustomizer customizer = mock(SimpleAsyncTaskSchedulerCustomizer.class); + SimpleAsyncTaskScheduler scheduler = this.builder.customizers(customizer).build(); + then(customizer).should().customize(scheduler); + } + + @Test + void customizersShouldBeAppliedLast() { + SimpleAsyncTaskScheduler scheduler = spy(new SimpleAsyncTaskScheduler()); + this.builder.concurrencyLimit(1).threadNamePrefix("test-").additionalCustomizers((taskScheduler) -> { + then(taskScheduler).should().setConcurrencyLimit(1); + then(taskScheduler).should().setThreadNamePrefix("test-"); + }); + this.builder.configure(scheduler); + } + + @Test + void customizersShouldReplaceExisting() { + SimpleAsyncTaskSchedulerCustomizer customizer1 = mock(SimpleAsyncTaskSchedulerCustomizer.class); + SimpleAsyncTaskSchedulerCustomizer customizer2 = mock(SimpleAsyncTaskSchedulerCustomizer.class); + SimpleAsyncTaskScheduler scheduler = this.builder.customizers(customizer1) + .customizers(Collections.singleton(customizer2)) + .build(); + then(customizer1).shouldHaveNoInteractions(); + then(customizer2).should().customize(scheduler); + } + + @Test + void additionalCustomizersWhenCustomizersAreNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.builder.additionalCustomizers((SimpleAsyncTaskSchedulerCustomizer[]) null)) + .withMessageContaining("Customizers must not be null"); + } + + @Test + void additionalCustomizersCollectionWhenCustomizersAreNullShouldThrowException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.builder.additionalCustomizers((Set) null)) + .withMessageContaining("Customizers must not be null"); + } + + @Test + void additionalCustomizersShouldAddToExisting() { + SimpleAsyncTaskSchedulerCustomizer customizer1 = mock(SimpleAsyncTaskSchedulerCustomizer.class); + SimpleAsyncTaskSchedulerCustomizer customizer2 = mock(SimpleAsyncTaskSchedulerCustomizer.class); + SimpleAsyncTaskScheduler scheduler = this.builder.customizers(customizer1) + .additionalCustomizers(customizer2) + .build(); + then(customizer1).should().customize(scheduler); + then(customizer2).should().customize(scheduler); + } + +} From 9948fdf97590207f3daa93c75a4637a6116f61ed Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 16:54:31 +0100 Subject: [PATCH 0282/1215] Upgrade to MSSQL JDBC 12.4.0.jre11 Closes gh-36880 --- spring-boot-project/spring-boot-dependencies/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index cea1d2a5ea19..a16dce4dbf00 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1011,10 +1011,10 @@ bom { ] } } - library("MSSQL JDBC", "11.2.3.jre17") { + library("MSSQL JDBC", "12.4.0.jre11") { prohibit { - endsWith([".jre8", ".jre11", ".jre18"]) - because "we use the .jre17 version" + endsWith([".jre8", "-preview"]) + because "we use the non-preview .jre11 version" } group("com.microsoft.sqlserver") { modules = [ From 07a4646329a93b2fe2f1438fa9e65cb52cfa76d8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 17:06:13 +0100 Subject: [PATCH 0283/1215] Upgrade to Artemis 2.30.0 Closes gh-36881 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index a16dce4dbf00..7faa433364d6 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -65,7 +65,7 @@ bom { ] } } - library("Artemis", "2.29.0") { + library("Artemis", "2.30.0") { group("org.apache.activemq") { modules = [ "artemis-amqp-protocol", From d7ddf67700e0deeca857a9dda5ff553a842494ee Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 17:06:21 +0100 Subject: [PATCH 0284/1215] Upgrade to Caffeine 3.1.8 Closes gh-36882 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 7faa433364d6..5ac169889b46 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -143,7 +143,7 @@ bom { ] } } - library("Caffeine", "3.1.6") { + library("Caffeine", "3.1.8") { group("com.github.ben-manes.caffeine") { modules = [ "caffeine", From 33a6911f52e6acdc9c0af1c69ca6aa2d71b4cded Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 17:06:29 +0100 Subject: [PATCH 0285/1215] Upgrade to Cassandra Driver 4.17.0 Closes gh-36883 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5ac169889b46..0c90d37ee210 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -153,7 +153,7 @@ bom { ] } } - library("Cassandra Driver", "4.16.0") { + library("Cassandra Driver", "4.17.0") { group("com.datastax.oss") { imports = [ "java-driver-bom" From 2c3049f961339c0e9007832e8d14cb4e30af0abc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 17:06:36 +0100 Subject: [PATCH 0286/1215] Upgrade to Commons Lang3 3.13.0 Closes gh-36884 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 0c90d37ee210..eb879d95602f 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -188,7 +188,7 @@ bom { ] } } - library("Commons Lang3", "3.12.0") { + library("Commons Lang3", "3.13.0") { group("org.apache.commons") { modules = [ "commons-lang3" From 6c3c8398d019fc8031e140ee3ecbb94fb4856483 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 17:06:46 +0100 Subject: [PATCH 0287/1215] Upgrade to Elasticsearch Client 8.9.0 Closes gh-36886 --- .../elasticsearch/ElasticsearchClientConfigurations.java | 6 +++--- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- .../spring-boot-docs/src/docs/asciidoc/data/nosql.adoc | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchClientConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchClientConfigurations.java index 28197797567b..de1fd52b0833 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchClientConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchClientConfigurations.java @@ -22,7 +22,7 @@ import co.elastic.clients.json.jackson.JacksonJsonpMapper; import co.elastic.clients.json.jsonb.JsonbJsonpMapper; import co.elastic.clients.transport.ElasticsearchTransport; -import co.elastic.clients.transport.TransportOptions; +import co.elastic.clients.transport.rest_client.RestClientOptions; import co.elastic.clients.transport.rest_client.RestClientTransport; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.json.bind.Jsonb; @@ -90,8 +90,8 @@ static class ElasticsearchTransportConfiguration { @Bean RestClientTransport restClientTransport(RestClient restClient, JsonpMapper jsonMapper, - ObjectProvider transportOptions) { - return new RestClientTransport(restClient, jsonMapper, transportOptions.getIfAvailable()); + ObjectProvider restClientOptions) { + return new RestClientTransport(restClient, jsonMapper, restClientOptions.getIfAvailable()); } } diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index eb879d95602f..d0abfbd49fab 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -262,7 +262,7 @@ bom { ] } } - library("Elasticsearch Client", "8.8.2") { + library("Elasticsearch Client", "8.9.0") { group("org.elasticsearch.client") { modules = [ "elasticsearch-rest-client" { diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc index b2846fe49342..b49028f9e954 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc @@ -330,7 +330,7 @@ If you have `co.elastic.clients:elasticsearch-java` on the classpath, Spring Boo The `ElasticsearchClient` uses a transport that depends upon the previously described `RestClient`. Therefore, the properties described previously can be used to configure the `ElasticsearchClient`. -Furthermore, you can define a `TransportOptions` bean to take further control of the behavior of the transport. +Furthermore, you can define a `RestClientOptions` bean to take further control of the behavior of the transport. @@ -341,7 +341,7 @@ If you have Spring Data Elasticsearch and Reactor on the classpath, Spring Boot The `ReactiveElasticsearchclient` uses a transport that depends upon the previously described `RestClient`. Therefore, the properties described previously can be used to configure the `ReactiveElasticsearchClient`. -Furthermore, you can define a `TransportOptions` bean to take further control of the behavior of the transport. +Furthermore, you can define a `RestClientOptions` bean to take further control of the behavior of the transport. From b581ab0d3a42bbebd6dc981dce72ad3ff1ab3f6e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 17:06:51 +0100 Subject: [PATCH 0288/1215] Upgrade to Flyway 9.21.1 Closes gh-36887 --- .../boot/autoconfigure/flyway/FlywayPropertiesTests.java | 2 +- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java index 94c021b3f9b6..b2d3607dd843 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java @@ -119,7 +119,7 @@ void expectedPropertiesAreManaged() { "javaMigrationClassProvider", "pluginRegister", "resourceProvider", "resolvers"); // Properties we don't want to expose ignoreProperties(configuration, "resolversAsClassNames", "callbacksAsClassNames", "driver", "modernConfig", - "currentResolvedEnvironment", "reportFilename", "reportEnabled"); + "currentResolvedEnvironment", "reportFilename", "reportEnabled", "workingDirectory"); // Handled by the conversion service ignoreProperties(configuration, "baselineVersionAsString", "encodingAsString", "locationsAsStrings", "targetAsString"); diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d0abfbd49fab..4671371824c5 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -279,7 +279,7 @@ bom { ] } } - library("Flyway", "9.20.1") { + library("Flyway", "9.21.1") { group("org.flywaydb") { modules = [ "flyway-core", From 19a75475ae1abc6a78476cbdbcddf1df538e69a1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 17:06:56 +0100 Subject: [PATCH 0289/1215] Upgrade to Hibernate 6.2.7.Final Closes gh-36888 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 4671371824c5..f4c3356fa691 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -370,7 +370,7 @@ bom { ] } } - library("Hibernate", "6.2.6.Final") { + library("Hibernate", "6.2.7.Final") { group("org.hibernate.orm") { modules = [ "hibernate-agroal", From 4ae8949a3d24aa732f5ed2c49e19f3634fca66b1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 17:07:01 +0100 Subject: [PATCH 0290/1215] Upgrade to Jersey 3.1.3 Closes gh-36889 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f4c3356fa691..353e234b6c71 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -657,7 +657,7 @@ bom { ] } } - library("Jersey", "3.1.2") { + library("Jersey", "3.1.3") { group("org.glassfish.jersey") { imports = [ "jersey-bom" From 6cfabffa3d4fc7c6da3980085d06477184797af4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 17:07:07 +0100 Subject: [PATCH 0291/1215] Upgrade to JUnit Jupiter 5.10.0 Closes gh-36890 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 353e234b6c71..144ff0439659 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -734,7 +734,7 @@ bom { ] } } - library("JUnit Jupiter", "5.9.3") { + library("JUnit Jupiter", "5.10.0") { group("org.junit") { imports = [ "junit-bom" From 02921f9879721a7f9e5caf707085f310d5bdfbd5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 17:07:12 +0100 Subject: [PATCH 0292/1215] Upgrade to Kafka 3.5.1 Closes gh-36891 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 144ff0439659..0db26fa546e6 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -741,7 +741,7 @@ bom { ] } } - library("Kafka", "3.5.0") { + library("Kafka", "3.5.1") { group("org.apache.kafka") { modules = [ "connect", From d7875d601a2b7100881d0c742076bfc8a3474c5a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 17:07:17 +0100 Subject: [PATCH 0293/1215] Upgrade to Kotlin Coroutines 1.7.3 Closes gh-36892 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 0db26fa546e6..2f8f91881b93 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -792,7 +792,7 @@ bom { ] } } - library("Kotlin Coroutines", "1.7.2") { + library("Kotlin Coroutines", "1.7.3") { group("org.jetbrains.kotlinx") { imports = [ "kotlinx-coroutines-bom" From a854e4b0da96dd71228bf1d4d89bce8b4b35b360 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 17:07:28 +0100 Subject: [PATCH 0294/1215] Upgrade to Native Build Tools Plugin 0.9.24 Closes gh-36894 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 4a6916a666ae..71a2554b1781 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ org.gradle.parallel=true org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 kotlinVersion=1.9.0 -nativeBuildToolsVersion=0.9.23 +nativeBuildToolsVersion=0.9.24 springFrameworkVersion=6.1.0-SNAPSHOT tomcatVersion=10.1.11 From 3130dd97c712e9ab462ef70770a9beba162e2958 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 17:07:33 +0100 Subject: [PATCH 0295/1215] Upgrade to Neo4j Java Driver 5.11.0 Closes gh-36895 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 2f8f91881b93..c766ab6ed211 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1045,7 +1045,7 @@ bom { ] } } - library("Neo4j Java Driver", "5.10.0") { + library("Neo4j Java Driver", "5.11.0") { group("org.neo4j.driver") { modules = [ "neo4j-java-driver" From c72013c932336fecb8825eedc55ea263a99c2289 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 17:07:38 +0100 Subject: [PATCH 0296/1215] Upgrade to Netty 4.1.96.Final Closes gh-36896 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c766ab6ed211..a2a980248dea 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1052,7 +1052,7 @@ bom { ] } } - library("Netty", "4.1.94.Final") { + library("Netty", "4.1.96.Final") { group("io.netty") { imports = [ "netty-bom" From 1a2813e2176b7319a69932c6cf0b54937cf236f5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 17:07:43 +0100 Subject: [PATCH 0297/1215] Upgrade to Pooled JMS 3.1.1 Closes gh-36897 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index a2a980248dea..cd17a1b5ab97 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1090,7 +1090,7 @@ bom { ] } } - library("Pooled JMS", "3.1.0") { + library("Pooled JMS", "3.1.1") { group("org.messaginghub") { modules = [ "pooled-jms" From 61b735d1e900f04a7c6eddd8d6155de1a747c099 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 17:07:49 +0100 Subject: [PATCH 0298/1215] Upgrade to Rabbit Stream Client 0.12.0 Closes gh-36898 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index cd17a1b5ab97..d59dd985c612 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1192,7 +1192,7 @@ bom { ] } } - library("Rabbit Stream Client", "0.11.0") { + library("Rabbit Stream Client", "0.12.0") { group("com.rabbitmq") { modules = [ "stream-client" From 69baa03ee00169a5186a3a8c324f474d0234303f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 17:07:54 +0100 Subject: [PATCH 0299/1215] Upgrade to Selenium 4.11.0 Closes gh-36899 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d59dd985c612..36c0d6c08ed6 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1324,7 +1324,7 @@ bom { ] } } - library("Selenium", "4.10.0") { + library("Selenium", "4.11.0") { group("org.seleniumhq.selenium") { imports = [ "selenium-bom" From 616034e5900e7b05db05f981dbec707a75bb14a9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 17:07:59 +0100 Subject: [PATCH 0300/1215] Upgrade to Selenium HtmlUnit 4.11.0 Closes gh-36900 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 36c0d6c08ed6..c7eb0ca35713 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1331,7 +1331,7 @@ bom { ] } } - library("Selenium HtmlUnit", "4.10.0") { + library("Selenium HtmlUnit", "4.11.0") { group("org.seleniumhq.selenium") { modules = [ "htmlunit-driver" From 866e1ece47ca64f7868dc15ba8ac3de9fc645626 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 17:08:04 +0100 Subject: [PATCH 0301/1215] Upgrade to SnakeYAML 2.1 Closes gh-36901 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c7eb0ca35713..85bfd2cb78d3 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1362,7 +1362,7 @@ bom { ] } } - library("SnakeYAML", "2.0") { + library("SnakeYAML", "2.1") { group("org.yaml") { modules = [ "snakeyaml" From 6e6ffe644afbf2077bca7fd14c9dab4d5cc0fc21 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 17:08:09 +0100 Subject: [PATCH 0302/1215] Upgrade to Thymeleaf 3.1.2.RELEASE Closes gh-36902 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 85bfd2cb78d3..15d1e6ae2612 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1497,7 +1497,7 @@ bom { ] } } - library("Thymeleaf", "3.1.1.RELEASE") { + library("Thymeleaf", "3.1.2.RELEASE") { group("org.thymeleaf") { modules = [ "thymeleaf", From 0581d5a26f6aaabc308dfd574d1ac10896af10c1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 17:08:15 +0100 Subject: [PATCH 0303/1215] Upgrade to Thymeleaf Extras SpringSecurity 3.1.2.RELEASE Closes gh-36903 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 15d1e6ae2612..3d0ee7c2b9a4 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1512,7 +1512,7 @@ bom { ] } } - library("Thymeleaf Extras SpringSecurity", "3.1.1.RELEASE") { + library("Thymeleaf Extras SpringSecurity", "3.1.2.RELEASE") { group("org.thymeleaf.extras") { modules = [ "thymeleaf-extras-springsecurity6" From d1449fb97c7f502903a419a53c4b9dfea77a2e54 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Aug 2023 21:20:40 +0100 Subject: [PATCH 0304/1215] Make milestone plugin dependencies available to Maven integration tests --- .../src/intTest/projects/settings.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/settings.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/settings.xml index 306a5fc6e054..8c1aed58a6cb 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/settings.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/settings.xml @@ -45,6 +45,11 @@ ignore + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + From bcee354f544f817a087ce5be30df7affdb4493d4 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 10 Aug 2023 11:27:04 +0200 Subject: [PATCH 0305/1215] Refactor synchronized to Lock in ConfigTreePropertySource The synchronized guards an I/O operation. This code path can be triggered every time a user calls .getProperty() on the Environment. See gh-36670 --- .../boot/env/ConfigTreePropertySource.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/ConfigTreePropertySource.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/ConfigTreePropertySource.java index 866404b46674..ae3737c547b3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/ConfigTreePropertySource.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/ConfigTreePropertySource.java @@ -29,6 +29,8 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Stream; import org.springframework.boot.convert.ApplicationConversionService; @@ -258,6 +260,8 @@ private static final class PropertyFileContent implements Value, OriginProvider private final Path path; + private final Lock resourceLock = new ReentrantLock(); + private final Resource resource; private final Origin origin; @@ -341,11 +345,15 @@ private byte[] getBytes() { } if (this.content == null) { assertStillExists(); - synchronized (this.resource) { + this.resourceLock.lock(); + try { if (this.content == null) { this.content = FileCopyUtils.copyToByteArray(this.resource.getInputStream()); } } + finally { + this.resourceLock.unlock(); + } } return this.content; } From fbd3b65034637ab2ac1b3de264d9a5600e4129dd Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 10 Aug 2023 11:34:18 +0200 Subject: [PATCH 0306/1215] Refactor synchronized to Lock in ApplicationTemp The synchronized guards an I/O operation. Additionally, this adds double-checked locking to the path instance field. See gh-36670 --- .../boot/system/ApplicationTemp.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/system/ApplicationTemp.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/system/ApplicationTemp.java index 9c413a0ab702..2ab9f745f574 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/system/ApplicationTemp.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/system/ApplicationTemp.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -28,6 +28,8 @@ import java.security.MessageDigest; import java.util.EnumSet; import java.util.HexFormat; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -49,6 +51,8 @@ public class ApplicationTemp { private final Class sourceClass; + private final Lock pathLock = new ReentrantLock(); + private volatile Path path; /** @@ -90,9 +94,15 @@ public File getDir(String subDir) { private Path getPath() { if (this.path == null) { - synchronized (this) { - String hash = HexFormat.of().withUpperCase().formatHex(generateHash(this.sourceClass)); - this.path = createDirectory(getTempDirectory().resolve(hash)); + this.pathLock.lock(); + try { + if (this.path == null) { + String hash = HexFormat.of().withUpperCase().formatHex(generateHash(this.sourceClass)); + this.path = createDirectory(getTempDirectory().resolve(hash)); + } + } + finally { + this.pathLock.unlock(); } } return this.path; From a73adb53e84c954519f9f4ca9a77fc601bc0ddff Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 10 Aug 2023 15:03:31 +0200 Subject: [PATCH 0307/1215] Upgrade to Logback 1.4.11 Closes gh-36893 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5a1a92356168..f41fdb6a6ab2 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -828,7 +828,7 @@ bom { ] } } - library("Logback", "1.4.8") { + library("Logback", "1.4.11") { group("ch.qos.logback") { modules = [ "logback-access", From 732f51a61477b4eaf6dff5efb6be67644338d820 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 10 Aug 2023 16:09:26 +0200 Subject: [PATCH 0308/1215] Start building against Spring AMQP 3.0.8 snapshots See gh-36940 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f41fdb6a6ab2..d772ca21c970 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1373,7 +1373,7 @@ bom { ] } } - library("Spring AMQP", "3.0.7") { + library("Spring AMQP", "3.0.8-SNAPSHOT") { group("org.springframework.amqp") { imports = [ "spring-amqp-bom" From cbbf9334558fda4051575079007fd7e0497b5719 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 10 Aug 2023 16:09:32 +0200 Subject: [PATCH 0309/1215] Start building against Spring Authorization Server 1.1.2 snapshots See gh-36941 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d772ca21c970..13b0065a5c10 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1380,7 +1380,7 @@ bom { ] } } - library("Spring Authorization Server", "1.1.1") { + library("Spring Authorization Server", "1.1.2-SNAPSHOT") { group("org.springframework.security") { modules = [ "spring-security-oauth2-authorization-server" From 69bf66d5f7ef7f36f70fa8a3c85469fdf54393a8 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 10 Aug 2023 16:09:36 +0200 Subject: [PATCH 0310/1215] Start building against Spring Kafka 3.0.10 snapshots See gh-36942 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 13b0065a5c10..4442d964c5fa 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1430,7 +1430,7 @@ bom { ] } } - library("Spring Kafka", "3.0.9") { + library("Spring Kafka", "3.0.10-SNAPSHOT") { group("org.springframework.kafka") { modules = [ "spring-kafka", From 9274b1016fe00193746443e3282726fcb4419ae6 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 10 Aug 2023 16:10:40 +0200 Subject: [PATCH 0311/1215] Start building against Spring Integration 6.2.0-M2 snapshots See gh-36943 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 4442d964c5fa..dff0be52d542 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1423,7 +1423,7 @@ bom { ] } } - library("Spring Integration", "6.2.0-M1") { + library("Spring Integration", "6.2.0-SNAPSHOT") { group("org.springframework.integration") { imports = [ "spring-integration-bom" From 7fe0086ee6f562426313d965c94dab308e79d4a4 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 10 Aug 2023 16:13:54 +0200 Subject: [PATCH 0312/1215] Start building against Spring Batch 5.1.0-M2 snapshots See gh-36944 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index dff0be52d542..eb7e5e658949 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1387,7 +1387,7 @@ bom { ] } } - library("Spring Batch", "5.1.0-M1") { + library("Spring Batch", "5.1.0-SNAPSHOT") { group("org.springframework.batch") { imports = [ "spring-batch-bom" From 407fa780c8efd0b8aaeee0a76ae83731f55d21a6 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sun, 13 Aug 2023 20:40:57 -0700 Subject: [PATCH 0313/1215] Polish --- .../boot/autoconfigure/thread/Threading.java | 9 ++++++--- .../jms/activemq/ActiveMQAutoConfigurationTests.java | 2 ++ .../src/docs/asciidoc/actuator/observability.adoc | 2 ++ .../org/springframework/boot/maven/AbstractRunMojo.java | 7 +++---- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/Threading.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/Threading.java index 1f32fa117005..b82e29953fce 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/Threading.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/thread/Threading.java @@ -31,22 +31,25 @@ public enum Threading { * Platform threads. Active if virtual threads are not active. */ PLATFORM { + @Override public boolean isActive(Environment environment) { return !VIRTUAL.isActive(environment); } + }, /** * Virtual threads. Active if {@code spring.threads.virtual.enabled} is {@code true} * and running on Java 21 or later. */ VIRTUAL { + @Override public boolean isActive(Environment environment) { - boolean virtualThreadsEnabled = environment.getProperty("spring.threads.virtual.enabled", boolean.class, - false); - return virtualThreadsEnabled && JavaVersion.getJavaVersion().isEqualOrNewerThan(JavaVersion.TWENTY_ONE); + return environment.getProperty("spring.threads.virtual.enabled", boolean.class, false) + && JavaVersion.getJavaVersion().isEqualOrNewerThan(JavaVersion.TWENTY_ONE); } + }; /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQAutoConfigurationTests.java index 3e1b0980d88f..2815e3e2ff50 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/activemq/ActiveMQAutoConfigurationTests.java @@ -289,6 +289,7 @@ static class TestConnectionDetailsConfiguration { @Bean ActiveMQConnectionDetails activemqConnectionDetails() { return new ActiveMQConnectionDetails() { + @Override public String getBrokerUrl() { return "tcp://localhost:12345"; @@ -303,6 +304,7 @@ public String getUser() { public String getPassword() { return "spring"; } + }; } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc index 9d8cd3b0efad..2b8fcccdb419 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc @@ -67,6 +67,8 @@ include::code:MyObservationPredicate[] The preceding example will prevent all observations whose name contains "denied". + + [[actuator.observability.opentelemetry]] === OpenTelemetry Support Spring Boot's actuator module includes basic support for https://opentelemetry.io/[OpenTelemetry]. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java index 9ca140975702..18a4009620ad 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java @@ -39,6 +39,7 @@ import org.apache.maven.toolchain.ToolchainManager; import org.springframework.boot.loader.tools.FileUtils; +import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** @@ -373,10 +374,8 @@ protected URL[] getClassPathUrls() throws MojoExecutionException { @SuppressWarnings("removal") private void addAdditionalClasspathLocations(List urls) throws MalformedURLException { - if (!ObjectUtils.isEmpty(this.directories) && !ObjectUtils.isEmpty(this.additionalClasspathElements)) { - throw new IllegalStateException( - "Either additionalClasspathElements or directories (deprecated) should be set, not both"); - } + Assert.state(ObjectUtils.isEmpty(this.directories) || ObjectUtils.isEmpty(this.additionalClasspathElements), + "Either additionalClasspathElements or directories (deprecated) should be set, not both"); String[] elements = !ObjectUtils.isEmpty(this.additionalClasspathElements) ? this.additionalClasspathElements : this.directories; if (elements != null) { From 8edec21a6f47b3384c10e76c9389beaeae9596be Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sun, 13 Aug 2023 20:41:11 -0700 Subject: [PATCH 0314/1215] Update copyright year of changed files --- .../metrics/jersey/JerseyServerMetricsAutoConfiguration.java | 2 +- .../metrics/export/prometheus/PrometheusPushGatewayManager.java | 2 +- .../boot/autoconfigure/BackgroundPreinitializer.java | 2 +- .../boot/autoconfigure/liquibase/LiquibaseProperties.java | 2 +- .../autoconfigure/template/TemplateAvailabilityProviders.java | 2 +- .../autoconfigure/web/client/RestTemplateAutoConfiguration.java | 2 +- .../web/embedded/NettyWebServerFactoryCustomizerTests.java | 2 +- .../boot/devtools/livereload/LiveReloadServer.java | 2 +- .../boot/test/autoconfigure/web/servlet/WebDriverScope.java | 2 +- .../boot/test/mock/web/SpringBootMockServletContext.java | 2 +- .../SpringBootTestContextBootstrapperIntegrationTests.java | 2 +- .../configurationprocessor/metadata/ConfigurationMetadata.java | 2 +- .../boot/configurationprocessor/metadata/ItemDeprecation.java | 2 +- .../boot/configurationprocessor/metadata/JsonMarshaller.java | 2 +- .../configurationsample/DeprecatedConfigurationProperty.java | 2 +- .../configurationsample/simple/DeprecatedSingleProperty.java | 2 +- .../springframework/boot/loader/data/RandomAccessDataFile.java | 2 +- .../src/main/java/org/test/SampleApplication.java | 2 +- .../src/main/java/org/test/SampleApplication.java | 2 +- .../src/main/java/org/test/SampleApplication.java | 2 +- .../springframework/boot/testsupport/assertj/package-info.java | 2 +- .../context/properties/DeprecatedConfigurationProperty.java | 2 +- .../springframework/boot/rsocket/netty/NettyRSocketServer.java | 2 +- .../reactive/ApplicationContextServerWebExchangeMatcher.java | 2 +- .../web/servlet/server/AbstractServletWebServerFactory.java | 2 +- .../org/springframework/boot/web/servlet/server/Session.java | 2 +- 26 files changed, 26 insertions(+), 26 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfiguration.java index 654a87564e46..2fc5de48dd3f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManager.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManager.java index da459eb0c28b..dc3ebe4d31ac 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManager.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusPushGatewayManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/BackgroundPreinitializer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/BackgroundPreinitializer.java index 7fc2382303ef..06a0f557df5c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/BackgroundPreinitializer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/BackgroundPreinitializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java index c14779f3305e..74588c2b8124 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/TemplateAvailabilityProviders.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/TemplateAvailabilityProviders.java index b787dc413a09..a6d032875e43 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/TemplateAvailabilityProviders.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/TemplateAvailabilityProviders.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java index fab1a2c4a475..ce89524c277c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java index 51945e666a5a..1505dac2c4d2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/LiveReloadServer.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/LiveReloadServer.java index 773fcabc87fe..d656bdc3f854 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/LiveReloadServer.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/livereload/LiveReloadServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverScope.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverScope.java index 5b91ef38e44f..4f9f1ae548fc 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverScope.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverScope.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/web/SpringBootMockServletContext.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/web/SpringBootMockServletContext.java index ec810bac3fd5..d1c005c78d46 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/web/SpringBootMockServletContext.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/web/SpringBootMockServletContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/SpringBootTestContextBootstrapperIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/SpringBootTestContextBootstrapperIntegrationTests.java index fb275a756e0c..69f87d827ff0 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/SpringBootTestContextBootstrapperIntegrationTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/bootstrap/SpringBootTestContextBootstrapperIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java index a1fff60dffad..79c62bd3ed62 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemDeprecation.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemDeprecation.java index 21ac303b394a..e684edf73c6f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemDeprecation.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemDeprecation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java index 3243adb17853..9f049bb72b23 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/DeprecatedConfigurationProperty.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/DeprecatedConfigurationProperty.java index 29116a508503..3aaf2a6540de 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/DeprecatedConfigurationProperty.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/DeprecatedConfigurationProperty.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/DeprecatedSingleProperty.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/DeprecatedSingleProperty.java index 904d3100af22..984c98764386 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/DeprecatedSingleProperty.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/DeprecatedSingleProperty.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java index 06d9abcda51a..4bd5d205418c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-directory/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-directory/src/main/java/org/test/SampleApplication.java index cff7f6409bd8..944441df246d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-directory/src/main/java/org/test/SampleApplication.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-directory/src/main/java/org/test/SampleApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-jar/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-jar/src/main/java/org/test/SampleApplication.java index cff7f6409bd8..944441df246d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-jar/src/main/java/org/test/SampleApplication.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-additional-classpath-jar/src/main/java/org/test/SampleApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-directories/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-directories/src/main/java/org/test/SampleApplication.java index cff7f6409bd8..944441df246d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-directories/src/main/java/org/test/SampleApplication.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/run-directories/src/main/java/org/test/SampleApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/package-info.java index 51fb5eaf4ef2..9eb3d15f8081 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/assertj/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/DeprecatedConfigurationProperty.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/DeprecatedConfigurationProperty.java index 7ac7bc98592d..f3962b551515 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/DeprecatedConfigurationProperty.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/DeprecatedConfigurationProperty.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServer.java index f50f847346dd..6c21b070367a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/security/reactive/ApplicationContextServerWebExchangeMatcher.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/security/reactive/ApplicationContextServerWebExchangeMatcher.java index 59264b353ff2..88ea38ca5614 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/security/reactive/ApplicationContextServerWebExchangeMatcher.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/security/reactive/ApplicationContextServerWebExchangeMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java index 04f622135008..d34dabce3326 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/Session.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/Session.java index df95dc474795..815f84abda2c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/Session.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/Session.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. From 3835e25a18687ae9e4b00ad92a9537d01e5b2a97 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 14 Aug 2023 12:18:34 +0200 Subject: [PATCH 0315/1215] Add missing Kotlin example See gh-36579 --- .../boot/docs/data/sql/jdbcclient/MyBean.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/data/sql/jdbcclient/MyBean.kt diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/data/sql/jdbcclient/MyBean.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/data/sql/jdbcclient/MyBean.kt new file mode 100644 index 000000000000..02cd7243bd7d --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/data/sql/jdbcclient/MyBean.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2023 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.docs.data.sql.jdbcclient + +import org.springframework.jdbc.core.simple.JdbcClient +import org.springframework.stereotype.Component + +@Component +class MyBean(private val jdbcClient: JdbcClient) { + + fun doSomething() { + jdbcClient.sql("delete from customer").update() + } + +} From 2ef2529c9343b07f8de1d9a2f8e7b8403037508f Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 14 Aug 2023 09:25:58 -0700 Subject: [PATCH 0316/1215] Refine Flyway extension mapping Change `ConfigurationExtensionMapper` to a helper class that can create a `Consumer` to use with the `PropertyMapper`. See gh-36364 --- .../flyway/FlywayAutoConfiguration.java | 87 +++++++++---------- 1 file changed, 40 insertions(+), 47 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java index 1f499261c910..6f4733298b2b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java @@ -26,7 +26,6 @@ import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Consumer; -import java.util.function.Supplier; import javax.sql.DataSource; @@ -53,6 +52,8 @@ import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayAutoConfigurationRuntimeHints; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration.FlywayDataSourceCondition; import org.springframework.boot.autoconfigure.flyway.FlywayProperties.Oracle; +import org.springframework.boot.autoconfigure.flyway.FlywayProperties.Postgresql; +import org.springframework.boot.autoconfigure.flyway.FlywayProperties.Sqlserver; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; @@ -471,18 +472,15 @@ static final class OracleFlywayConfigurationCustomizer implements FlywayConfigur @Override public void customize(FluentConfiguration configuration) { - ConfigurationExtensionMapper map = new ConfigurationExtensionMapper<>( - PropertyMapper.get().alwaysApplyingWhenNonNull(), () -> { - OracleConfigurationExtension extension = configuration.getPluginRegister() - .getPlugin(OracleConfigurationExtension.class); - Assert.notNull(extension, "Flyway Oracle extension missing"); - return extension; - }); - Oracle oracle = this.properties.getOracle(); - map.apply(oracle.getSqlplus(), OracleConfigurationExtension::setSqlplus); - map.apply(oracle.getSqlplusWarn(), OracleConfigurationExtension::setSqlplusWarn); - map.apply(oracle.getWalletLocation(), OracleConfigurationExtension::setWalletLocation); - map.apply(oracle.getKerberosCacheFile(), OracleConfigurationExtension::setKerberosCacheFile); + Extension extension = new Extension<>(configuration, + OracleConfigurationExtension.class, "Oracle"); + Oracle properties = this.properties.getOracle(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getSqlplus).to(extension.via(OracleConfigurationExtension::setSqlplus)); + map.from(properties::getSqlplusWarn).to(extension.via(OracleConfigurationExtension::setSqlplusWarn)); + map.from(properties::getWalletLocation).to(extension.via(OracleConfigurationExtension::setWalletLocation)); + map.from(properties::getKerberosCacheFile) + .to(extension.via(OracleConfigurationExtension::setKerberosCacheFile)); } } @@ -498,15 +496,12 @@ static final class PostgresqlFlywayConfigurationCustomizer implements FlywayConf @Override public void customize(FluentConfiguration configuration) { - ConfigurationExtensionMapper map = new ConfigurationExtensionMapper<>( - PropertyMapper.get().alwaysApplyingWhenNonNull(), () -> { - PostgreSQLConfigurationExtension extension = configuration.getPluginRegister() - .getPlugin(PostgreSQLConfigurationExtension.class); - Assert.notNull(extension, "PostgreSQL extension missing"); - return extension; - }); - map.apply(this.properties.getPostgresql().getTransactionalLock(), - PostgreSQLConfigurationExtension::setTransactionalLock); + Extension extension = new Extension<>(configuration, + PostgreSQLConfigurationExtension.class, "PostgreSQL"); + Postgresql properties = this.properties.getPostgresql(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getTransactionalLock) + .to(extension.via(PostgreSQLConfigurationExtension::setTransactionalLock)); } } @@ -522,40 +517,38 @@ static final class SqlServerFlywayConfigurationCustomizer implements FlywayConfi @Override public void customize(FluentConfiguration configuration) { - ConfigurationExtensionMapper map = new ConfigurationExtensionMapper<>( - PropertyMapper.get().alwaysApplyingWhenNonNull(), () -> { - SQLServerConfigurationExtension extension = configuration.getPluginRegister() - .getPlugin(SQLServerConfigurationExtension.class); - Assert.notNull(extension, "Flyway SQL Server extension missing"); - return extension; - }); + Extension extension = new Extension<>(configuration, + SQLServerConfigurationExtension.class, "SQL Server"); + Sqlserver properties = this.properties.getSqlserver(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getKerberosLoginFile).to(extension.via(this::setKerberosLoginFile)); + } - map.apply(this.properties.getSqlserver().getKerberosLoginFile(), - (extension, file) -> extension.getKerberos().getLogin().setFile(file)); + private void setKerberosLoginFile(SQLServerConfigurationExtension configuration, String file) { + configuration.getKerberos().getLogin().setFile(file); } } - static class ConfigurationExtensionMapper { - - private final PropertyMapper map; - - private final Supplier extensionProvider; + /** + * Helper class used to map properties to a {@link ConfigurationExtension}. + * + * @param the extension type + */ + static class Extension { - ConfigurationExtensionMapper(PropertyMapper map, Supplier extensionProvider) { - this.map = map; - this.extensionProvider = SingletonSupplier.of(extensionProvider); - } + private SingletonSupplier extension; - void apply(V value, BiConsumer mapper) { - this.map.from(value).to(withExtension(mapper)); + Extension(FluentConfiguration configuration, Class type, String name) { + this.extension = SingletonSupplier.of(() -> { + E extension = configuration.getPluginRegister().getPlugin(type); + Assert.notNull(extension, () -> "Flyway %s extension missing".formatted(name)); + return extension; + }); } - private Consumer withExtension(BiConsumer mapper) { - return (value) -> { - T extension = this.extensionProvider.get(); - mapper.accept(extension, value); - }; + Consumer via(BiConsumer action) { + return (value) -> action.accept(this.extension.get(), value); } } From eb45aab7120d6cc8d8085b4a4cb71d141066a704 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Mon, 7 Aug 2023 13:19:24 -0500 Subject: [PATCH 0317/1215] Upgrade default CNB builders to Paketo Jammy Closes gh-36689 --- ci/pipeline.yml | 4 ++-- .../native-image/developing-your-first-application.adoc | 4 ++-- .../spring-boot-starter-parent/build.gradle | 2 +- .../boot/buildpack/platform/build/BuildRequest.java | 2 +- .../src/docs/asciidoc/packaging-oci-image.adoc | 2 +- .../spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc | 2 +- .../boot/gradle/plugin/NativeImagePluginAction.java | 2 +- .../plugin/NativeImagePluginActionIntegrationTests.java | 3 ++- .../boot/gradle/tasks/bundling/BootBuildImageTests.java | 3 ++- .../src/docs/asciidoc/packaging-oci-image.adoc | 2 +- .../test/java/org/springframework/boot/maven/ImageTests.java | 2 +- 11 files changed, 15 insertions(+), 13 deletions(-) diff --git a/ci/pipeline.yml b/ci/pipeline.yml index ad367a8ad435..77cc81ecbc10 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -185,8 +185,8 @@ resources: type: registry-image icon: docker source: - repository: paketobuildpacks/builder - tag: base + repository: paketobuildpacks/builder-jammy-base + tag: latest - name: artifactory-repo type: artifactory-resource icon: package-variant diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/native-image/developing-your-first-application.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/native-image/developing-your-first-application.adoc index b4e70ac86ce5..fe5a0d568380 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/native-image/developing-your-first-application.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/native-image/developing-your-first-application.adoc @@ -32,8 +32,8 @@ This means you can just type a single command and quickly get a sensible image i The resulting image doesn't contain a JVM, instead the native image is compiled statically. This leads to smaller images. -NOTE: The builder used for the images is `paketobuildpacks/builder:tiny`. -It has small footprint and reduced attack surface, but you can also use `paketobuildpacks/builder-jammy-base` or `paketobuildpacks/builder-jammy-full` to have more tools available in the image if required. +NOTE: The builder used for the images is `paketobuildpacks/builder-jammy-tiny:latest`. +It has small footprint and reduced attack surface, but you can also use `paketobuildpacks/builder-jammy-base:latest` or `paketobuildpacks/builder-jammy-full:latest` to have more tools available in the image if required. diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle index 2b2028ea2cad..32d88db76916 100644 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle @@ -249,7 +249,7 @@ publishing.publications.withType(MavenPublication) { delegate.artifactId('spring-boot-maven-plugin') configuration { image { - delegate.builder("paketobuildpacks/builder:tiny"); + delegate.builder("paketobuildpacks/builder-jammy-tiny:latest"); env { delegate.BP_NATIVE_IMAGE("true") } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java index 0bb75fe17e0e..f409fcc875ff 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java @@ -45,7 +45,7 @@ */ public class BuildRequest { - static final String DEFAULT_BUILDER_IMAGE_NAME = "paketobuildpacks/builder:base"; + static final String DEFAULT_BUILDER_IMAGE_NAME = "paketobuildpacks/builder-jammy-base:latest"; private static final ImageReference DEFAULT_BUILDER = ImageReference.of(DEFAULT_BUILDER_IMAGE_NAME); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc index 824c79ee0049..501d4f12d4f9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -115,7 +115,7 @@ The following table summarizes the available properties and their default values | `builder` | `--builder` | Name of the Builder image to use. -| `paketobuildpacks/builder:base` or `paketobuildpacks/builder:tiny` when {nbt-gradle-plugin}[GraalVM Native Image plugin] is applied. +| `paketobuildpacks/builder-jammy-base:latest` or `paketobuildpacks/builder-jammy-tiny:latest` when {nbt-gradle-plugin}[GraalVM Native Image plugin] is applied. | `runImage` | `--runImage` diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc index 660404b1a426..bfb245f2d427 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc @@ -81,6 +81,6 @@ When the {nbt-gradle-plugin}[GraalVM Native Image plugin] is applied to a projec . Configures the GraalVM extension to disable Toolchain detection. . Configures each GraalVM native binary to require GraalVM 22.3 or later. . Configures the `bootJar` task to include the reachability metadata produced by the `collectReachabilityMetadata` task in its jar. -. Configures the `bootBuildImage` task to use `paketobuildpacks/builder:tiny` as its builder and to set `BP_NATIVE_IMAGE` to `true` in its environment. +. Configures the `bootBuildImage` task to use `paketobuildpacks/builder-jammy-tiny:latest` as its builder and to set `BP_NATIVE_IMAGE` to `true` in its environment. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java index 061d073e61e3..7c3ead680b39 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java @@ -114,7 +114,7 @@ private void configureBootBuildImageToProduceANativeImage(Project project) { project.getTasks() .named(SpringBootPlugin.BOOT_BUILD_IMAGE_TASK_NAME, BootBuildImage.class) .configure((bootBuildImage) -> { - bootBuildImage.getBuilder().convention("paketobuildpacks/builder:tiny"); + bootBuildImage.getBuilder().convention("paketobuildpacks/builder-jammy-tiny:latest"); bootBuildImage.getEnvironment().put("BP_NATIVE_IMAGE", "true"); }); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java index 08c094e2efb6..4d15614a7057 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java @@ -93,7 +93,8 @@ void bootBuildImageIsConfiguredToBuildANativeImage() { writeDummySpringApplicationAotProcessorMainClass(); BuildResult result = this.gradleBuild.expectDeprecationWarningsWithAtLeastVersion("8.2-rc-1") .build("bootBuildImageConfiguration"); - assertThat(result.getOutput()).contains("paketobuildpacks/builder:tiny").contains("BP_NATIVE_IMAGE = true"); + assertThat(result.getOutput()).contains("paketobuildpacks/builder-jammy-tiny") + .contains("BP_NATIVE_IMAGE = true"); } @TestTemplate diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java index fdb69d2a532d..33dac8a784e4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java @@ -171,7 +171,8 @@ void whenUsingDefaultConfigurationThenRequestHasPublishDisabled() { @Test void whenNoBuilderIsConfiguredThenRequestHasDefaultBuilder() { - assertThat(this.buildImage.createRequest().getBuilder().getName()).isEqualTo("paketobuildpacks/builder"); + assertThat(this.buildImage.createRequest().getBuilder().getName()) + .isEqualTo("paketobuildpacks/builder-jammy-base"); } @Test diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc index 09e85abb8209..0abcd2303d44 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -131,7 +131,7 @@ The following table summarizes the available parameters and their default values | `builder` + (`spring-boot.build-image.builder`) | Name of the Builder image to use. -| `paketobuildpacks/builder:base` +| `paketobuildpacks/builder-jammy-base:latest` | `runImage` + (`spring-boot.build-image.runImage`) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java index 8f3558701c46..86625106bdbe 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java @@ -67,7 +67,7 @@ void getBuildRequestWhenNameIsSetUsesName() { void getBuildRequestWhenNoCustomizationsUsesDefaults() { BuildRequest request = new Image().getBuildRequest(createArtifact(), mockApplicationContent()); assertThat(request.getName()).hasToString("docker.io/library/my-app:0.0.1-SNAPSHOT"); - assertThat(request.getBuilder().toString()).contains("paketobuildpacks/builder"); + assertThat(request.getBuilder().toString()).contains("paketobuildpacks/builder-jammy-base"); assertThat(request.getRunImage()).isNull(); assertThat(request.getEnv()).isEmpty(); assertThat(request.isCleanCache()).isFalse(); From 85b4362ec6217300958607f54a8aeb816fe21949 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 15 Aug 2023 17:08:29 +0200 Subject: [PATCH 0318/1215] Adapt to change in Spring Framework snapshots --- .../autoconfigure/jdbc/JdbcClientAutoConfigurationTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfigurationTests.java index 4fa13d6922ee..0430b4f8f747 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfigurationTests.java @@ -89,10 +89,10 @@ void jdbcClientIsOrderedAfterLiquibaseMigration() { static class JdbcClientDataSourceMigrationValidator { - private final Integer count; + private final Long count; JdbcClientDataSourceMigrationValidator(JdbcClient jdbcClient) { - this.count = jdbcClient.sql("SELECT COUNT(*) from CITY").query().singleValue(Integer.class); + this.count = jdbcClient.sql("SELECT COUNT(*) from CITY").query().singleValue(); } } From 73874911ad820cba5c5422675bd3542fd40b1512 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Tue, 15 Aug 2023 14:30:16 -0500 Subject: [PATCH 0319/1215] Adapt to changes in Spring Data snapshots See gh-36680 --- .../data/jdbc/JdbcRepositoriesAutoConfigurationTests.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/jdbc/JdbcRepositoriesAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/jdbc/JdbcRepositoriesAutoConfigurationTests.java index 0ed97c82ab28..064d5d34c5aa 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/jdbc/JdbcRepositoriesAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/jdbc/JdbcRepositoriesAutoConfigurationTests.java @@ -232,7 +232,7 @@ static class JdbcMappingContextConfiguration { @Bean JdbcMappingContext customJdbcMappingContext() { - return mock(JdbcMappingContext.class); + return mock(JdbcMappingContext.class, Answers.RETURNS_MOCKS); } } @@ -242,7 +242,7 @@ static class JdbcConverterConfiguration { @Bean JdbcConverter customJdbcConverter() { - return mock(JdbcConverter.class); + return mock(JdbcConverter.class, Answers.RETURNS_MOCKS); } } @@ -262,7 +262,7 @@ static class JdbcAggregateTemplateConfiguration { @Bean JdbcAggregateTemplate customJdbcAggregateTemplate() { - return mock(JdbcAggregateTemplate.class); + return mock(JdbcAggregateTemplate.class, Answers.RETURNS_MOCKS); } } @@ -272,7 +272,7 @@ static class DataAccessStrategyConfiguration { @Bean DataAccessStrategy customDataAccessStrategy() { - return mock(DataAccessStrategy.class); + return mock(DataAccessStrategy.class, Answers.RETURNS_MOCKS); } } From a3f37089db09179b2f13b8df87933f6e89d077b3 Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Tue, 15 Aug 2023 10:30:21 -0700 Subject: [PATCH 0320/1215] Fix failing tests due to extended exemplars Micrometer introduced extended exemplars functionality that adds exemplars to _count too not only to histogram buckets, see: https://github.com/micrometer-metrics/micrometer/pull/3996 Because of this, some verifications should be changed. --- ...etheusExemplarsAutoConfigurationTests.java | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java index 3bbec4be49f3..8f1a8ba5d98e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/prometheus/PrometheusExemplarsAutoConfigurationTests.java @@ -16,6 +16,10 @@ package org.springframework.boot.actuate.autoconfigure.tracing.prometheus; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import io.micrometer.prometheus.PrometheusMeterRegistry; @@ -33,6 +37,7 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -44,6 +49,12 @@ */ class PrometheusExemplarsAutoConfigurationTests { + private static final Pattern BUCKET_TRACE_INFO_PATTERN = Pattern.compile( + "^test_observation_seconds_bucket\\{error=\"none\",le=\".+\"} 1.0 # \\{span_id=\"(\\p{XDigit}+)\",trace_id=\"(\\p{XDigit}+)\"} .+$"); + + private static final Pattern COUNTER_TRACE_INFO_PATTERN = Pattern.compile( + "^test_observation_seconds_count\\{error=\"none\"} 1.0 # \\{span_id=\"(\\p{XDigit}+)\",trace_id=\"(\\p{XDigit}+)\"} .+$"); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("management.tracing.sampling.probability=1.0", "management.metrics.distribution.percentiles-histogram.all=true") @@ -80,9 +91,27 @@ void prometheusOpenMetricsOutputShouldContainExemplars() { Observation.start("test.observation", observationRegistry).stop(); PrometheusMeterRegistry prometheusMeterRegistry = context.getBean(PrometheusMeterRegistry.class); String openMetricsOutput = prometheusMeterRegistry.scrape(TextFormat.CONTENT_TYPE_OPENMETRICS_100); - assertThat(openMetricsOutput).contains("test_observation_seconds_bucket") - .containsOnlyOnce("trace_id=") - .containsOnlyOnce("span_id="); + + assertThat(openMetricsOutput).contains("test_observation_seconds_bucket"); + assertThat(openMetricsOutput).containsOnlyOnce("test_observation_seconds_count"); + assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "span_id")).isEqualTo(2); + assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "trace_id")).isEqualTo(2); + + Optional bucketTraceInfo = openMetricsOutput.lines() + .filter((line) -> line.contains("test_observation_seconds_bucket") && line.contains("span_id")) + .map(BUCKET_TRACE_INFO_PATTERN::matcher) + .flatMap(Matcher::results) + .map((matchResult) -> new TraceInfo(matchResult.group(2), matchResult.group(1))) + .findFirst(); + + Optional counterTraceInfo = openMetricsOutput.lines() + .filter((line) -> line.contains("test_observation_seconds_count") && line.contains("span_id")) + .map(COUNTER_TRACE_INFO_PATTERN::matcher) + .flatMap(Matcher::results) + .map((matchResult) -> new TraceInfo(matchResult.group(2), matchResult.group(1))) + .findFirst(); + + assertThat(bucketTraceInfo).isNotEmpty().contains(counterTraceInfo.orElse(null)); }); } @@ -98,4 +127,7 @@ SpanContextSupplier customSpanContextSupplier() { } + private record TraceInfo(String traceId, String spanId) { + } + } From a3361e1c9da809a49634f395f60813a03e8eb5e1 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 15 Aug 2023 17:18:49 +0200 Subject: [PATCH 0321/1215] Upgrade to Infinispan 14.0.14.Final Closes gh-36982 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index eb7e5e658949..fe7df3f59291 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -462,7 +462,7 @@ bom { ] } } - library("Infinispan", "14.0.12.Final") { + library("Infinispan", "14.0.14.Final") { group("org.infinispan") { imports = [ "infinispan-bom" From 48b0ff9d4bc842358f64b1f50caa1653486db725 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 15 Aug 2023 17:19:22 +0200 Subject: [PATCH 0322/1215] Upgrade to Micrometer 1.12.0-M2 Closes gh-36675 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index fe7df3f59291..f02ba2fc8f4a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -977,7 +977,7 @@ bom { ] } } - library("Micrometer", "1.12.0-SNAPSHOT") { + library("Micrometer", "1.12.0-M2") { group("io.micrometer") { modules = [ "micrometer-registry-stackdriver" { From 494daa6bafe2726ed99eccccd736636142dc7d8d Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 15 Aug 2023 17:19:44 +0200 Subject: [PATCH 0323/1215] Upgrade to Micrometer Tracing 1.2.0-M2 Closes gh-36676 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f02ba2fc8f4a..1d9204fec3e5 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -989,7 +989,7 @@ bom { ] } } - library("Micrometer Tracing", "1.2.0-SNAPSHOT") { + library("Micrometer Tracing", "1.2.0-M2") { group("io.micrometer") { imports = [ "micrometer-tracing-bom" From a690ce68579645d799c87b44bd45f93272636d0f Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 15 Aug 2023 17:20:06 +0200 Subject: [PATCH 0324/1215] Upgrade to Reactor Bom 2023.0.0-M2 Closes gh-36677 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 1d9204fec3e5..5109a575df13 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1210,7 +1210,7 @@ bom { ] } } - library("Reactor Bom", "2023.0.0-SNAPSHOT") { + library("Reactor Bom", "2023.0.0-M2") { group("io.projectreactor") { imports = [ "reactor-bom" From da0b8104231d40f5a48df65328c610c0217fd166 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 14 Aug 2023 17:18:56 +0200 Subject: [PATCH 0325/1215] Upgrade to Dependency Management Plugin 1.1.3 Closes gh-36981 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5109a575df13..8d4c6929c136 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -227,7 +227,7 @@ bom { ] } } - library("Dependency Management Plugin", "1.1.2") { + library("Dependency Management Plugin", "1.1.3") { group("io.spring.gradle") { modules = [ "dependency-management-plugin" From a83fa3e055041e65cc1be781c6926883afe7fa66 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 14 Aug 2023 17:19:05 +0200 Subject: [PATCH 0326/1215] Upgrade to jOOQ 3.18.6 Closes gh-36983 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 8d4c6929c136..905d1ba60d97 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -689,7 +689,7 @@ bom { ] } } - library("jOOQ", "3.18.5") { + library("jOOQ", "3.18.6") { group("org.jooq") { modules = [ "jooq", From 9b8b39db2ab6ecdefaf6746fec4a8ff32ce3f8d3 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 14 Aug 2023 17:19:14 +0200 Subject: [PATCH 0327/1215] Upgrade to OpenTelemetry 1.29.0 Closes gh-36985 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 905d1ba60d97..8fea83a4221c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1070,7 +1070,7 @@ bom { ] } } - library("OpenTelemetry", "1.28.0") { + library("OpenTelemetry", "1.29.0") { group("io.opentelemetry") { imports = [ "opentelemetry-bom" From 983d060030bc1c9831499c401ca3dd5f74077117 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 14 Aug 2023 17:19:19 +0200 Subject: [PATCH 0328/1215] Upgrade to R2DBC Pool 1.0.1.RELEASE Closes gh-36986 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 8fea83a4221c..ad650213f45b 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1161,7 +1161,7 @@ bom { ] } } - library("R2DBC Pool", "1.0.0.RELEASE") { + library("R2DBC Pool", "1.0.1.RELEASE") { group("io.r2dbc") { modules = [ "r2dbc-pool" From 1bfcdbb576316716f47f671d45616d01e7c68e57 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 14 Aug 2023 17:19:23 +0200 Subject: [PATCH 0329/1215] Upgrade to Tomcat 10.1.12 Closes gh-36987 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 71a2554b1781..2f7cc57659b6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,6 @@ org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 kotlinVersion=1.9.0 nativeBuildToolsVersion=0.9.24 springFrameworkVersion=6.1.0-SNAPSHOT -tomcatVersion=10.1.11 +tomcatVersion=10.1.12 kotlin.stdlib.default.dependency=false From 968328ead392d0ab218a60a5407f64cf39e5780c Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 14 Aug 2023 17:20:24 +0200 Subject: [PATCH 0330/1215] Upgrade to Couchbase Client 3.4.8 Closes gh-36988 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index ad650213f45b..3e75cc87e5e6 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -209,7 +209,7 @@ bom { ] } } - library("Couchbase Client", "3.4.7") { + library("Couchbase Client", "3.4.8") { prohibit { versionRange "3.4.9" because "it contains unshaded io.opentelemetry classes that break our Otel integration" From 7d00ff4adf88164620377171346292ef0b014fa0 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 16 Aug 2023 15:17:16 +0200 Subject: [PATCH 0331/1215] Upgrade to Byte Buddy 1.14.6 Closes gh-36996 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 3e75cc87e5e6..e7b5b12805e2 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -123,7 +123,7 @@ bom { ] } } - library("Byte Buddy", "1.14.5") { + library("Byte Buddy", "1.14.6") { group("net.bytebuddy") { modules = [ "byte-buddy", From 7af57299eb249a1bda654f85d7e9965f5c236950 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 16 Aug 2023 15:44:04 +0200 Subject: [PATCH 0332/1215] Upgrade to AspectJ 1.9.20 Closes gh-37005 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e7b5b12805e2..e94266b319b9 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -83,7 +83,7 @@ bom { ] } } - library("AspectJ", "1.9.19") { + library("AspectJ", "1.9.20") { group("org.aspectj") { modules = [ "aspectjrt", From 16b00b5ef7ccb6defc1215b0fa89637845b6c006 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 16 Aug 2023 15:44:09 +0200 Subject: [PATCH 0333/1215] Upgrade to Lettuce 6.2.6.RELEASE Closes gh-37006 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e94266b319b9..485bdef157fb 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -803,7 +803,7 @@ bom { ] } } - library("Lettuce", "6.2.5.RELEASE") { + library("Lettuce", "6.2.6.RELEASE") { group("io.lettuce") { modules = [ "lettuce-core" From 36b5500ad0cdb528de41dba220db33aefc380113 Mon Sep 17 00:00:00 2001 From: Ramil Sayetov Date: Tue, 15 Aug 2023 09:29:39 +0300 Subject: [PATCH 0334/1215] Reuse JOOQ helper to determine the dialect to use See gh-36991 --- .../boot/autoconfigure/jooq/SqlDialectLookup.java | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookup.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookup.java index b91b72b68258..cc49388b5bbc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookup.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookup.java @@ -16,8 +16,7 @@ package org.springframework.boot.autoconfigure.jooq; -import java.sql.DatabaseMetaData; - +import java.sql.SQLException; import javax.sql.DataSource; import org.apache.commons.logging.Log; @@ -25,14 +24,12 @@ import org.jooq.SQLDialect; import org.jooq.tools.jdbc.JDBCUtils; -import org.springframework.jdbc.support.JdbcUtils; -import org.springframework.jdbc.support.MetaDataAccessException; - /** * Utility to lookup well known {@link SQLDialect SQLDialects} from a {@link DataSource}. * * @author Michael Simons * @author Lukas Eder + * @author Ramil Saetov */ final class SqlDialectLookup { @@ -51,13 +48,9 @@ static SQLDialect getDialect(DataSource dataSource) { return SQLDialect.DEFAULT; } try { - String url = JdbcUtils.extractDatabaseMetaData(dataSource, DatabaseMetaData::getURL); - SQLDialect sqlDialect = JDBCUtils.dialect(url); - if (sqlDialect != null) { - return sqlDialect; - } + return JDBCUtils.dialect(dataSource.getConnection()); } - catch (MetaDataAccessException ex) { + catch (SQLException ex) { logger.warn("Unable to determine jdbc url from datasource", ex); } return SQLDialect.DEFAULT; From 37467c79d0642375ba7d26c1b5a823ea8735fc33 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 16 Aug 2023 15:56:16 +0200 Subject: [PATCH 0335/1215] Polish "Reuse JOOQ helper to determine the dialect to use" See gh-36991 --- .../boot/autoconfigure/jooq/SqlDialectLookup.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookup.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookup.java index cc49388b5bbc..4213e39c50bb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookup.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookup.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -16,7 +16,9 @@ package org.springframework.boot.autoconfigure.jooq; +import java.sql.Connection; import java.sql.SQLException; + import javax.sql.DataSource; import org.apache.commons.logging.Log; @@ -44,14 +46,12 @@ private SqlDialectLookup() { * @return the most suitable {@link SQLDialect} */ static SQLDialect getDialect(DataSource dataSource) { - if (dataSource == null) { - return SQLDialect.DEFAULT; - } try { - return JDBCUtils.dialect(dataSource.getConnection()); + Connection connection = (dataSource != null) ? dataSource.getConnection() : null; + return JDBCUtils.dialect(connection); } catch (SQLException ex) { - logger.warn("Unable to determine jdbc url from datasource", ex); + logger.warn("Unable to determine dialect from datasource", ex); } return SQLDialect.DEFAULT; } From e1af526129457769116c5925dbdca8776aa0af76 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 16 Aug 2023 17:53:45 +0200 Subject: [PATCH 0336/1215] Prohibit upgrades to Liquibase 4.23.1 Closes gh-36984 --- spring-boot-project/spring-boot-dependencies/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 485bdef157fb..0becacd3c19d 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -811,6 +811,10 @@ bom { } } library("Liquibase", "4.23.0") { + prohibit { + versionRange "4.23.1" + because "it contains a regression (https://github.com/liquibase/liquibase/issues/4684)" + } group("org.liquibase") { modules = [ "liquibase-cdi", From 06d0e45383f10b206fdfcd402a422f1ab1778c19 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 17 Aug 2023 15:46:52 +0200 Subject: [PATCH 0337/1215] Upgrade to Spring Framework 6.1.0-M4 Closes gh-36678 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2f7cc57659b6..cef268201872 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 kotlinVersion=1.9.0 nativeBuildToolsVersion=0.9.24 -springFrameworkVersion=6.1.0-SNAPSHOT +springFrameworkVersion=6.1.0-M4 tomcatVersion=10.1.12 kotlin.stdlib.default.dependency=false From e995ef6830112c15a6452f093fe8a98c0a1a37f9 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Fri, 18 Aug 2023 16:49:36 +0200 Subject: [PATCH 0338/1215] Upgrade to Undertow 2.3.8.Final Closes gh-37032 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 74cd699ef3e9..fdbadffd1478 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1558,7 +1558,7 @@ bom { ] } } - library("Undertow", "2.3.7.Final") { + library("Undertow", "2.3.8.Final") { group("io.undertow") { modules = [ "undertow-core", From 48ea1ed8fb252ff9fcafa387fa9ac86367d36eae Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Fri, 18 Aug 2023 16:50:11 +0200 Subject: [PATCH 0339/1215] Upgrade to Spring Data Bom 2023.1.0-M2 Closes gh-36680 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index fdbadffd1478..5d64b851730a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1398,7 +1398,7 @@ bom { ] } } - library("Spring Data Bom", "2023.1.0-SNAPSHOT") { + library("Spring Data Bom", "2023.1.0-M2") { group("org.springframework.data") { imports = [ "spring-data-bom" From 5e41d910f02e25fa282a260b3ba2c1142a953576 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Sat, 19 Aug 2023 18:29:31 +0200 Subject: [PATCH 0340/1215] Upgrade to Spring LDAP 3.2.0-M2 Closes gh-36679 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5d64b851730a..9b65c90857ae 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1442,7 +1442,7 @@ bom { ] } } - library("Spring LDAP", "3.2.0-SNAPSHOT") { + library("Spring LDAP", "3.2.0-M2") { group("org.springframework.ldap") { modules = [ "spring-ldap-core", From d46a58f0f6f9f5f01a4203a712a54364b4b08cd5 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 21 Aug 2023 16:43:41 +0200 Subject: [PATCH 0341/1215] Configure Virtual Threads support in Reactor This commit configures the virtual threads support in Reactor Core for Schedulers if: * the current JDK is 21 or higher * the current environment enables this globally with the `"spring.threads.virtual.enabled"` property. This needs to happen early in the application startup process, as this feature is detected statically when the first schedulers call is made. As a result, this is being done with an environment post processor and not with an auto-configuration class. Closes gh-36302 --- ...a => ReactorEnvironmentPostProcessor.java} | 29 ++++++++++------- ...itional-spring-configuration-metadata.json | 2 +- .../main/resources/META-INF/spring.factories | 2 +- .../reactor/InstrumentedFluxProvider.java | 2 +- ...ReactorEnvironmentPostProcessorTests.java} | 32 +++++++++++++++---- 5 files changed, 47 insertions(+), 20 deletions(-) rename spring-boot-project/spring-boot/src/main/java/org/springframework/boot/reactor/{DebugAgentEnvironmentPostProcessor.java => ReactorEnvironmentPostProcessor.java} (58%) rename spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/{DebugAgentEnvironmentPostProcessorTests.java => ReactorEnvironmentPostProcessorTests.java} (59%) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/reactor/DebugAgentEnvironmentPostProcessor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/reactor/ReactorEnvironmentPostProcessor.java similarity index 58% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/reactor/DebugAgentEnvironmentPostProcessor.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/reactor/ReactorEnvironmentPostProcessor.java index 6268e8b67c93..4b3e1c143fb7 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/reactor/DebugAgentEnvironmentPostProcessor.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/reactor/ReactorEnvironmentPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. @@ -18,31 +18,34 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.boot.system.JavaVersion; import org.springframework.core.Ordered; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.util.ClassUtils; /** - * {@link EnvironmentPostProcessor} to enable the Reactor Debug Agent if available. + * {@link EnvironmentPostProcessor} to enable the Reactor global features as early as + * possible in the startup process. *

    - * The debug agent is enabled by default, unless the - * {@code "spring.reactor.debug-agent.enabled"} configuration property is set to false. We - * are using here an {@link EnvironmentPostProcessor} instead of an auto-configuration - * class to enable the agent as soon as possible during the startup process. + * If the "reactor-tools" dependency is available, the debug agent is enabled by default, + * unless the {@code "spring.reactor.debug-agent.enabled"} configuration property is set + * to false. + *

    + * If the {@code "spring.threads.virtual.enabled"} property is enabled and the current JVM + * is 21 or later, then the Reactor System property is set to configure the Bounded + * Elastic Scheduler to use Virtual Threads globally. * * @author Brian Clozel - * @since 2.2.0 + * @since 3.2.0 */ -public class DebugAgentEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { +public class ReactorEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { private static final String REACTOR_DEBUGAGENT_CLASS = "reactor.tools.agent.ReactorDebugAgent"; - private static final String DEBUGAGENT_ENABLED_CONFIG_KEY = "spring.reactor.debug-agent.enabled"; - @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { if (ClassUtils.isPresent(REACTOR_DEBUGAGENT_CLASS, null)) { - Boolean agentEnabled = environment.getProperty(DEBUGAGENT_ENABLED_CONFIG_KEY, Boolean.class); + Boolean agentEnabled = environment.getProperty("spring.reactor.debug-agent.enabled", Boolean.class); if (agentEnabled != Boolean.FALSE) { try { Class debugAgent = Class.forName(REACTOR_DEBUGAGENT_CLASS); @@ -53,6 +56,10 @@ public void postProcessEnvironment(ConfigurableEnvironment environment, SpringAp } } } + if (environment.getProperty("spring.threads.virtual.enabled", boolean.class, false) + && JavaVersion.getJavaVersion().isEqualOrNewerThan(JavaVersion.TWENTY_ONE)) { + System.setProperty("reactor.schedulers.defaultBoundedElasticOnVirtualThreads", "true"); + } } @Override diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 5bc1a19d991f..12f70410dd73 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -491,7 +491,7 @@ { "name": "spring.reactor.debug-agent.enabled", "type": "java.lang.Boolean", - "sourceType": "org.springframework.boot.reactor.DebugAgentEnvironmentPostProcessor", + "sourceType": "org.springframework.boot.reactor.ReactorEnvironmentPostProcessor", "description": "Whether the Reactor Debug Agent should be enabled when reactor-tools is present.", "defaultValue": true }, diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories index 4641c7b8dd61..e6775b7491f1 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories @@ -57,7 +57,7 @@ org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor,\ org.springframework.boot.env.RandomValuePropertySourceEnvironmentPostProcessor,\ org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\ org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor,\ -org.springframework.boot.reactor.DebugAgentEnvironmentPostProcessor +org.springframework.boot.reactor.ReactorEnvironmentPostProcessor # Failure Analyzers org.springframework.boot.diagnostics.FailureAnalyzer=\ diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/InstrumentedFluxProvider.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/InstrumentedFluxProvider.java index 2ec6e0c03b02..d9382b364b41 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/InstrumentedFluxProvider.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/InstrumentedFluxProvider.java @@ -22,7 +22,7 @@ * Utility class that should be instrumented by the reactor debug agent. * * @author Brian Clozel - * @see DebugAgentEnvironmentPostProcessorTests + * @see ReactorEnvironmentPostProcessorTests */ class InstrumentedFluxProvider { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/DebugAgentEnvironmentPostProcessorTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/ReactorEnvironmentPostProcessorTests.java similarity index 59% rename from spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/DebugAgentEnvironmentPostProcessorTests.java rename to spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/ReactorEnvironmentPostProcessorTests.java index 87efbcc782f4..b7aeae287780 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/DebugAgentEnvironmentPostProcessorTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/ReactorEnvironmentPostProcessorTests.java @@ -16,8 +16,10 @@ package org.springframework.boot.reactor; -import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import reactor.core.Scannable; import reactor.core.publisher.Flux; @@ -27,17 +29,18 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link DebugAgentEnvironmentPostProcessor}. + * Tests for {@link ReactorEnvironmentPostProcessor}. * * @author Brian Clozel */ -@Disabled("We need the not-yet-released reactor-tools 3.4.11 for JDK 17 compatibility") -@ClassPathOverrides("io.projectreactor:reactor-tools:3.4.11") -class DebugAgentEnvironmentPostProcessorTests { + +@ClassPathOverrides("io.projectreactor:reactor-tools:3.5.9") +class ReactorEnvironmentPostProcessorTests { static { MockEnvironment environment = new MockEnvironment(); - DebugAgentEnvironmentPostProcessor postProcessor = new DebugAgentEnvironmentPostProcessor(); + environment.setProperty("spring.threads.virtual.enabled", "true"); + ReactorEnvironmentPostProcessor postProcessor = new ReactorEnvironmentPostProcessor(); postProcessor.postProcessEnvironment(environment, null); } @@ -49,4 +52,21 @@ void enablesReactorDebugAgent() { .startsWith("Flux.just ⇢ at org.springframework.boot.reactor.InstrumentedFluxProvider.newFluxJust"); } + @Test + @EnabledForJreRange(max = JRE.JAVA_20) + void shouldNotEnableVirtualThreads() { + assertThat(System.getProperty("reactor.schedulers.defaultBoundedElasticOnVirtualThreads")).isNotEqualTo("true"); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void shouldEnableVirtualThreads() { + assertThat(System.getProperty("reactor.schedulers.defaultBoundedElasticOnVirtualThreads")).isEqualTo("true"); + } + + @AfterEach + void cleanup() { + System.setProperty("reactor.schedulers.defaultBoundedElasticOnVirtualThreads", "false"); + } + } From c17ecf0f0bd8a7375835d0217406d5ffb90207e9 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Mon, 21 Aug 2023 15:03:39 -0500 Subject: [PATCH 0342/1215] Add support for caching to bind mounts when building images When building an image using the Maven `spring-boot:build-image` goal or the Gradle `bootBuildImage` task, the build and launch caches can be configured to use a bind mount as an alternative to using a named volume. Closes gh-28387 --- .../platform/build/AbstractBuildLog.java | 8 +- .../buildpack/platform/build/BuildLog.java | 10 +- .../boot/buildpack/platform/build/Cache.java | 106 +++++++++++++++++- .../buildpack/platform/build/Lifecycle.java | 60 ++++++---- .../platform/build/BuildRequestTests.java | 16 +++ .../platform/build/LifecycleTests.java | 12 ++ .../build/PrintStreamBuildLogTests.java | 4 +- .../lifecycle-creator-cache-bind-mounts.json | 39 +++++++ .../docs/asciidoc/packaging-oci-image.adoc | 14 +++ .../boot-build-image-bind-caches.gradle | 30 +++++ .../boot-build-image-bind-caches.gradle.kts | 28 +++++ .../boot/gradle/tasks/bundling/CacheSpec.java | 29 ++++- .../docs/PackagingDocumentationTests.java | 8 ++ .../BootBuildImageIntegrationTests.java | 21 ++++ ...tionTests-buildsImageWithBindCaches.gradle | 24 ++++ ...s-failsWhenCachesAreConfiguredTwice.gradle | 6 +- .../docs/asciidoc/packaging-oci-image.adoc | 7 ++ .../packaging-oci-image/bind-caches-pom.xml | 27 +++++ .../boot/maven/BuildImageTests.java | 29 ++++- .../projects/build-image-bind-caches/pom.xml | 44 ++++++++ .../main/java/org/test/SampleApplication.java | 2 +- .../pom.xml | 2 +- .../main/java/org/test/SampleApplication.java | 28 +++++ .../springframework/boot/maven/CacheInfo.java | 43 ++++++- .../boot/maven/ImageTests.java | 25 ++++- 25 files changed, 575 insertions(+), 47 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-cache-bind-mounts.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle.kts create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBindCaches.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/bind-caches-pom.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bind-caches/pom.xml rename spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/{build-image-caches => build-image-bind-caches}/src/main/java/org/test/SampleApplication.java (93%) rename spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/{build-image-caches => build-image-volume-caches}/pom.xml (96%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-volume-caches/src/main/java/org/test/SampleApplication.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/AbstractBuildLog.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/AbstractBuildLog.java index 7154d4ad43bf..b870d30f9f64 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/AbstractBuildLog.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/AbstractBuildLog.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -68,6 +68,12 @@ public void executingLifecycle(BuildRequest request, LifecycleVersion version, V log(" > Using build cache volume '" + buildCacheVolume + "'"); } + @Override + public void executingLifecycle(BuildRequest request, LifecycleVersion version, Cache buildCache) { + log(" > Executing lifecycle version " + version); + log(" > Using build cache " + buildCache); + } + @Override public Consumer runningPhase(BuildRequest request, String name) { log(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildLog.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildLog.java index 0acbbabd224f..e84054773f40 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildLog.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildLog.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -79,6 +79,14 @@ public interface BuildLog { */ void executingLifecycle(BuildRequest request, LifecycleVersion version, VolumeName buildCacheVolume); + /** + * Log that the lifecycle is executing. + * @param request the build request + * @param version the lifecycle version + * @param buildCache the build cache in use + */ + void executingLifecycle(BuildRequest request, LifecycleVersion version, Cache buildCache); + /** * Log that a specific phase is running. * @param request the build request diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java index 9f3087f94c31..704a3418d397 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -18,6 +18,7 @@ import java.util.Objects; +import org.springframework.boot.buildpack.platform.docker.type.VolumeName; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -37,7 +38,22 @@ public enum Format { /** * A cache stored as a volume in the Docker daemon. */ - VOLUME; + VOLUME("volume"), + + /** + * A cache stored as a bind mount. + */ + BIND("bind mount"); + + private final String description; + + Format(String description) { + this.description = description; + } + + public String getDescription() { + return this.description; + } } @@ -55,16 +71,44 @@ public Volume getVolume() { return (this.format.equals(Format.VOLUME)) ? (Volume) this : null; } + /** + * Return the details of the cache if it is a bind cache. + * @return the cache, or {@code null} if it is not a bind cache + */ + public Bind getBind() { + return (this.format.equals(Format.BIND)) ? (Bind) this : null; + } + /** * Create a new {@code Cache} that uses a volume with the provided name. * @param name the cache volume name * @return a new cache instance */ public static Cache volume(String name) { + Assert.notNull(name, "Name must not be null"); + return new Volume(VolumeName.of(name)); + } + + /** + * Create a new {@code Cache} that uses a volume with the provided name. + * @param name the cache volume name + * @return a new cache instance + */ + public static Cache volume(VolumeName name) { Assert.notNull(name, "Name must not be null"); return new Volume(name); } + /** + * Create a new {@code Cache} that uses a bind mount with the provided source. + * @param source the cache bind mount source + * @return a new cache instance + */ + public static Cache bind(String source) { + Assert.notNull(source, "Source must not be null"); + return new Bind(source); + } + @Override public boolean equals(Object obj) { if (this == obj) { @@ -87,14 +131,18 @@ public int hashCode() { */ public static class Volume extends Cache { - private final String name; + private final VolumeName name; - Volume(String name) { + Volume(VolumeName name) { super(Format.VOLUME); this.name = name; } public String getName() { + return this.name.toString(); + } + + public VolumeName getVolumeName() { return this.name; } @@ -120,6 +168,56 @@ public int hashCode() { return result; } + @Override + public String toString() { + return this.format.getDescription() + " '" + this.name + "'"; + } + + } + + /** + * Details of a cache stored in a bind mount. + */ + public static class Bind extends Cache { + + private final String source; + + Bind(String source) { + super(Format.BIND); + this.source = source; + } + + public String getSource() { + return this.source; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + if (!super.equals(obj)) { + return false; + } + Bind other = (Bind) obj; + return Objects.equals(this.source, other.source); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.source); + return result; + } + + @Override + public String toString() { + return this.format.getDescription() + " '" + this.source + "'"; + } + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java index a9bf57caee18..4d12105764a3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java @@ -18,6 +18,7 @@ import java.io.Closeable; import java.io.IOException; +import java.nio.file.Path; import java.util.function.Consumer; import com.sun.jna.Platform; @@ -34,6 +35,7 @@ import org.springframework.boot.buildpack.platform.docker.type.VolumeName; import org.springframework.boot.buildpack.platform.io.TarArchive; import org.springframework.util.Assert; +import org.springframework.util.FileSystemUtils; /** * A buildpack lifecycle used to run the build {@link Phase phases} needed to package an @@ -72,9 +74,9 @@ class Lifecycle implements Closeable { private final VolumeName applicationVolume; - private final VolumeName buildCacheVolume; + private final Cache buildCache; - private final VolumeName launchCacheVolume; + private final Cache launchCache; private final String applicationDirectory; @@ -101,8 +103,8 @@ class Lifecycle implements Closeable { this.platformVersion = getPlatformVersion(builder.getBuilderMetadata().getLifecycle()); this.layersVolume = createRandomVolumeName("pack-layers-"); this.applicationVolume = createRandomVolumeName("pack-app-"); - this.buildCacheVolume = getBuildCacheVolumeName(request); - this.launchCacheVolume = getLaunchCacheVolumeName(request); + this.buildCache = getBuildCache(request); + this.launchCache = getLaunchCache(request); this.applicationDirectory = getApplicationDirectory(request); } @@ -110,33 +112,27 @@ protected VolumeName createRandomVolumeName(String prefix) { return VolumeName.random(prefix); } - private VolumeName getBuildCacheVolumeName(BuildRequest request) { + private Cache getBuildCache(BuildRequest request) { if (request.getBuildCache() != null) { - return getVolumeName(request.getBuildCache()); + return request.getBuildCache(); } - return createCacheVolumeName(request, "build"); + return createVolumeCache(request, "build"); } - private VolumeName getLaunchCacheVolumeName(BuildRequest request) { + private Cache getLaunchCache(BuildRequest request) { if (request.getLaunchCache() != null) { - return getVolumeName(request.getLaunchCache()); + return request.getLaunchCache(); } - return createCacheVolumeName(request, "launch"); - } - - private VolumeName getVolumeName(Cache cache) { - if (cache.getVolume() != null) { - return VolumeName.of(cache.getVolume().getName()); - } - return null; + return createVolumeCache(request, "launch"); } private String getApplicationDirectory(BuildRequest request) { return (request.getApplicationDirectory() != null) ? request.getApplicationDirectory() : Directory.APPLICATION; } - private VolumeName createCacheVolumeName(BuildRequest request, String suffix) { - return VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", "." + suffix, 6); + private Cache createVolumeCache(BuildRequest request, String suffix) { + return Cache.volume( + VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", "." + suffix, 6)); } private ApiVersion getPlatformVersion(BuilderMetadata.Lifecycle lifecycle) { @@ -155,9 +151,14 @@ private ApiVersion getPlatformVersion(BuilderMetadata.Lifecycle lifecycle) { void execute() throws IOException { Assert.state(!this.executed, "Lifecycle has already been executed"); this.executed = true; - this.log.executingLifecycle(this.request, this.lifecycleVersion, this.buildCacheVolume); + this.log.executingLifecycle(this.request, this.lifecycleVersion, this.buildCache); if (this.request.isCleanCache()) { - deleteVolume(this.buildCacheVolume); + if (this.buildCache.getVolume() != null) { + deleteVolume(this.buildCache.getVolume().getVolumeName()); + } + if (this.buildCache.getBind() != null) { + deleteBind(this.buildCache.getBind().getSource()); + } } run(createPhase()); this.log.executedLifecycle(this.request); @@ -184,8 +185,8 @@ private Phase createPhase() { phase.withArgs(this.request.getName()); phase.withBinding(Binding.from(this.layersVolume, Directory.LAYERS)); phase.withBinding(Binding.from(this.applicationVolume, this.applicationDirectory)); - phase.withBinding(Binding.from(this.buildCacheVolume, Directory.CACHE)); - phase.withBinding(Binding.from(this.launchCacheVolume, Directory.LAUNCH_CACHE)); + phase.withBinding(Binding.from(getCacheBindingSource(this.buildCache), Directory.CACHE)); + phase.withBinding(Binding.from(getCacheBindingSource(this.launchCache), Directory.LAUNCH_CACHE)); if (this.request.getBindings() != null) { this.request.getBindings().forEach(phase::withBinding); } @@ -199,6 +200,10 @@ private Phase createPhase() { return phase; } + private String getCacheBindingSource(Cache cache) { + return (cache.getVolume() != null) ? cache.getVolume().getName() : cache.getBind().getSource(); + } + private void configureDaemonAccess(Phase phase) { if (this.dockerHost != null) { if (this.dockerHost.isRemote()) { @@ -269,6 +274,15 @@ private void deleteVolume(VolumeName name) throws IOException { this.docker.volume().delete(name, true); } + private void deleteBind(String source) { + try { + FileSystemUtils.deleteRecursively(Path.of(source)); + } + catch (IOException ex) { + throw new IllegalStateException("Error cleaning bind mount directory '" + source + "'", ex); + } + } + /** * Common directories used by the various phases. */ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java index 1ded1fa52613..20eb2944503a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java @@ -239,6 +239,14 @@ void withBuildVolumeCacheAddsCache() throws IOException { assertThat(withCache.getBuildCache()).isEqualTo(Cache.volume("build-volume")); } + @Test + void withBuildBindCacheAddsCache() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + BuildRequest withCache = request.withBuildCache(Cache.bind("/tmp/build-cache")); + assertThat(request.getBuildCache()).isNull(); + assertThat(withCache.getBuildCache()).isEqualTo(Cache.bind("/tmp/build-cache")); + } + @Test void withBuildVolumeCacheWhenCacheIsNullThrowsException() throws IOException { BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); @@ -254,6 +262,14 @@ void withLaunchVolumeCacheAddsCache() throws IOException { assertThat(withCache.getLaunchCache()).isEqualTo(Cache.volume("launch-volume")); } + @Test + void withLaunchBindCacheAddsCache() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + BuildRequest withCache = request.withLaunchCache(Cache.bind("/tmp/launch-cache")); + assertThat(request.getLaunchCache()).isNull(); + assertThat(withCache.getLaunchCache()).isEqualTo(Cache.bind("/tmp/launch-cache")); + } + @Test void withLaunchVolumeCacheWhenCacheIsNullThrowsException() throws IOException { BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java index d8b03407a3d5..40a2a80caa89 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java @@ -218,6 +218,18 @@ void executeWithCacheVolumeNamesExecutesPhases() throws Exception { assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); } + @Test + void executeWithCacheBindMountsExecutesPhases() throws Exception { + given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); + BuildRequest request = getTestRequest().withBuildCache(Cache.bind("/tmp/build-cache")) + .withLaunchCache(Cache.bind("/tmp/launch-cache")); + createLifecycle(request).execute(); + assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-cache-bind-mounts.json")); + assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + } + @Test void executeWithCreatedDateExecutesPhases() throws Exception { given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PrintStreamBuildLogTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PrintStreamBuildLogTests.java index 74cefced82ae..1e25ed10a998 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PrintStreamBuildLogTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PrintStreamBuildLogTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -67,7 +67,7 @@ void printsExpectedOutput() throws Exception { Consumer pullRunImageConsumer = log.pullingImage(runImageReference, ImageType.RUNNER); pullRunImageConsumer.accept(new TotalProgressEvent(100)); log.pulledImage(runImage, ImageType.RUNNER); - log.executingLifecycle(request, LifecycleVersion.parse("0.5"), VolumeName.of("pack-abc.cache")); + log.executingLifecycle(request, LifecycleVersion.parse("0.5"), Cache.volume(VolumeName.of("pack-abc.cache"))); Consumer phase1Consumer = log.runningPhase(request, "alphabet"); phase1Consumer.accept(mockLogEvent("one")); phase1Consumer.accept(mockLogEvent("two")); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-cache-bind-mounts.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-cache-bind-mounts.json new file mode 100644 index 000000000000..2b7814d909c8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-cache-bind-mounts.json @@ -0,0 +1,39 @@ +{ + "User": "root", + "Image": "pack.local/ephemeral-builder", + "Cmd": [ + "/cnb/lifecycle/creator", + "-app", + "/workspace", + "-platform", + "/platform", + "-run-image", + "docker.io/cloudfoundry/run:latest", + "-layers", + "/layers", + "-cache-dir", + "/cache", + "-launch-cache", + "/launch-cache", + "-daemon", + "docker.io/library/my-application:latest" + ], + "Env": [ + "CNB_PLATFORM_API=0.8" + ], + "Labels": { + "author": "spring-boot" + }, + "HostConfig": { + "Binds": [ + "/var/run/docker.sock:/var/run/docker.sock", + "pack-layers-aaaaaaaaaa:/layers", + "pack-app-aaaaaaaaaa:/workspace", + "/tmp/build-cache:/cache", + "/tmp/launch-cache:/launch-cache" + ], + "SecurityOpt" : [ + "label=disable" + ] + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc index 501d4f12d4f9..ef6ffc910eb5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -440,6 +440,20 @@ include::../gradle/packaging/boot-build-image-caches.gradle[tags=caches] include::../gradle/packaging/boot-build-image-caches.gradle.kts[tags=caches] ---- +The caches can be configured to use bind mounts instead of named volumes, as shown in the following example: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-build-image-bind-caches.gradle[tags=caches] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-build-image-bind-caches.gradle.kts[tags=caches] +---- + [[build-image.examples.docker]] === Docker Configuration diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle new file mode 100644 index 000000000000..5bca082e10fd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle @@ -0,0 +1,30 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +tasks.named("bootJar") { + mainClass = 'com.example.ExampleApplication' +} + +// tag::caches[] +tasks.named("bootBuildImage") { + buildCache { + bind { + source = "/tmp/cache-${rootProject.name}.build" + } + } + launchCache { + bind { + source = "/tmp/cache-${rootProject.name}.launch" + } + } +} +// end::caches[] + +tasks.register("bootBuildImageCaches") { + doFirst { + bootBuildImage.buildCache.asCache().with { println "buildCache=$source" } + bootBuildImage.launchCache.asCache().with { println "launchCache=$source" } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle.kts new file mode 100644 index 000000000000..008889f51961 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle.kts @@ -0,0 +1,28 @@ +import org.springframework.boot.gradle.tasks.bundling.BootBuildImage + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +// tag::caches[] +tasks.named("bootBuildImage") { + buildCache { + bind { + source.set("/tmp/cache-${rootProject.name}.build") + } + } + launchCache { + bind { + source.set("/tmp/cache-${rootProject.name}.launch") + } + } +} +// end::caches[] + +tasks.register("bootBuildImageCaches") { + doFirst { + println("buildCache=" + tasks.getByName("bootBuildImage").buildCache.asCache().bind.source) + println("launchCache=" + tasks.getByName("bootBuildImage").launchCache.asCache().bind.source) + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java index d33d6a964966..235a3665f148 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 the original author or authors. + * Copyright 2021-2023 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. @@ -60,6 +60,19 @@ public void volume(Action action) { this.cache = Cache.volume(spec.getName().get()); } + /** + * Configures a bind cache using the given {@code action}. + * @param action the action + */ + public void bind(Action action) { + if (this.cache != null) { + throw new GradleException("Each image building cache can be configured only once"); + } + BindCacheSpec spec = this.objectFactory.newInstance(BindCacheSpec.class); + action.execute(spec); + this.cache = Cache.bind(spec.getSource().get()); + } + /** * Configuration for an image building cache stored in a Docker volume. */ @@ -74,4 +87,18 @@ public abstract static class VolumeCacheSpec { } + /** + * Configuration for an image building cache stored in a bind mount. + */ + public abstract static class BindCacheSpec { + + /** + * Returns the source of the cache. + * @return the cache source + */ + @Input + public abstract Property getSource(); + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java index 562a3f5b139f..84a2506cd715 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java @@ -339,6 +339,14 @@ void bootBuildImageWithCaches() { .containsPattern("launchCache=cache-gradle-[\\d]+.launch"); } + @TestTemplate + void bootBuildImageWithBindCaches() { + BuildResult result = this.gradleBuild.script("src/docs/gradle/packaging/boot-build-image-bind-caches") + .build("bootBuildImageCaches"); + assertThat(result.getOutput()).containsPattern("buildCache=/tmp/cache-gradle-[\\d]+.build") + .containsPattern("launchCache=/tmp/cache-gradle-[\\d]+.launch"); + } + protected void jarFile(File file) throws IOException { try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(file))) { jar.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF")); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java index 2a2238c4a9ee..8e3f23eb2c6d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java @@ -50,6 +50,7 @@ import org.springframework.boot.testsupport.gradle.testkit.GradleBuild; import org.springframework.boot.testsupport.junit.DisabledOnOs; import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable; +import org.springframework.util.FileSystemUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -297,6 +298,26 @@ void buildsImageWithVolumeCaches() throws IOException { deleteVolumes("cache-" + projectName + ".build", "cache-" + projectName + ".launch"); } + @TestTemplate + void buildsImageWithBindCaches() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.build("bootBuildImage"); + String projectName = this.gradleBuild.getProjectDir().getName(); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("docker.io/library/" + projectName); + assertThat(result.getOutput()).contains("---> Test Info buildpack building"); + assertThat(result.getOutput()).contains("---> Test Info buildpack done"); + removeImages(projectName); + String tempDir = System.getProperty("java.io.tmpdir"); + Path buildCachePath = Paths.get(tempDir, "junit-image-cache-" + projectName + "-build"); + Path launchCachePath = Paths.get(tempDir, "junit-image-cache-" + projectName + "-launch"); + assertThat(buildCachePath).exists().isDirectory(); + assertThat(launchCachePath).exists().isDirectory(); + FileSystemUtils.deleteRecursively(buildCachePath); + FileSystemUtils.deleteRecursively(launchCachePath); + } + @TestTemplate void buildsImageWithCreatedDate() throws IOException { writeMainClass(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBindCaches.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBindCaches.gradle new file mode 100644 index 000000000000..b1c8c803350a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBindCaches.gradle @@ -0,0 +1,24 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +java { + sourceCompatibility = '1.8' + targetCompatibility = '1.8' +} + +bootBuildImage { + builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2" + pullPolicy = "IF_NOT_PRESENT" + buildCache { + bind { + source = System.getProperty('java.io.tmpdir') + "/junit-image-cache-${rootProject.name}-build" + } + } + launchCache { + bind { + source = System.getProperty('java.io.tmpdir') + "/junit-image-cache-${rootProject.name}-launch" + } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWhenCachesAreConfiguredTwice.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWhenCachesAreConfiguredTwice.gradle index 7b3c343aab7d..40440edccfc1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWhenCachesAreConfiguredTwice.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWhenCachesAreConfiguredTwice.gradle @@ -12,10 +12,10 @@ bootBuildImage { builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2" buildCache { volume { - name = "build-cache-volume1" + name = "build-cache-volume" } - volume { - name = "build-cache-volum2" + bind { + name = "/tmp/build-cache-bind" } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc index 0abcd2303d44..07b1f1aef6fc 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -420,6 +420,13 @@ The cache volumes can be configured to use alternative names to give more contro include::../maven/packaging-oci-image/caches-pom.xml[tags=caches] ---- +The caches can be configured to use bind mounts instead of named volumes, as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes",tabsize=4] +---- +include::../maven/packaging-oci-image/bind-caches-pom.xml[tags=caches] +---- + [[build-image.examples.docker]] === Docker Configuration diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/bind-caches-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/bind-caches-pom.xml new file mode 100644 index 000000000000..2cf4941fecbe --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/bind-caches-pom.xml @@ -0,0 +1,27 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + /tmp/cache-${project.artifactId}.build + + + + + /tmp/cache-${project.artifactId}.launch + + + + + + + + + \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java index b80cdc0bc44e..8ea336c6a1de 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java @@ -37,6 +37,7 @@ import org.springframework.boot.buildpack.platform.docker.type.VolumeName; import org.springframework.boot.testsupport.junit.DisabledOnOs; import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable; +import org.springframework.util.FileSystemUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -385,19 +386,41 @@ void whenBuildImageIsInvokedWithTags(MavenBuild mavenBuild) { @TestTemplate void whenBuildImageIsInvokedWithVolumeCaches(MavenBuild mavenBuild) { String testBuildId = randomString(); - mavenBuild.project("build-image-caches") + mavenBuild.project("build-image-volume-caches") .goals("package") .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT") .systemProperty("test-build-id", testBuildId) .execute((project) -> { assertThat(buildLog(project)).contains("Building image") - .contains("docker.io/library/build-image-caches:0.0.1.BUILD-SNAPSHOT") + .contains("docker.io/library/build-image-volume-caches:0.0.1.BUILD-SNAPSHOT") .contains("Successfully built image"); - removeImage("build-image-caches", "0.0.1.BUILD-SNAPSHOT"); + removeImage("build-image-volume-caches", "0.0.1.BUILD-SNAPSHOT"); deleteVolumes("cache-" + testBuildId + ".build", "cache-" + testBuildId + ".launch"); }); } + @TestTemplate + void whenBuildImageIsInvokedWithBindCaches(MavenBuild mavenBuild) { + String testBuildId = randomString(); + mavenBuild.project("build-image-bind-caches") + .goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT") + .systemProperty("test-build-id", testBuildId) + .execute((project) -> { + assertThat(buildLog(project)).contains("Building image") + .contains("docker.io/library/build-image-bind-caches:0.0.1.BUILD-SNAPSHOT") + .contains("Successfully built image"); + removeImage("build-image-bind-caches", "0.0.1.BUILD-SNAPSHOT"); + String tempDir = System.getProperty("java.io.tmpdir"); + Path buildCachePath = Paths.get(tempDir, "junit-image-cache-" + testBuildId + "-build"); + Path launchCachePath = Paths.get(tempDir, "junit-image-cache-" + testBuildId + "-launch"); + assertThat(buildCachePath).exists().isDirectory(); + assertThat(launchCachePath).exists().isDirectory(); + FileSystemUtils.deleteRecursively(buildCachePath); + FileSystemUtils.deleteRecursively(launchCachePath); + }); + } + @TestTemplate void whenBuildImageIsInvokedWithCreatedDate(MavenBuild mavenBuild) { String testBuildId = randomString(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bind-caches/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bind-caches/pom.xml new file mode 100644 index 000000000000..349d1519e9e1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bind-caches/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-bind-caches + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image-no-fork + + + + projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2 + + + ${java.io.tmpdir}/junit-image-cache-${test-build-id}-build + + + + + ${java.io.tmpdir}/junit-image-cache-${test-build-id}-launch + + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-caches/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bind-caches/src/main/java/org/test/SampleApplication.java similarity index 93% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-caches/src/main/java/org/test/SampleApplication.java rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bind-caches/src/main/java/org/test/SampleApplication.java index e964724deacd..03544b74e463 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-caches/src/main/java/org/test/SampleApplication.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bind-caches/src/main/java/org/test/SampleApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-caches/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-volume-caches/pom.xml similarity index 96% rename from spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-caches/pom.xml rename to spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-volume-caches/pom.xml index f95eb39f874e..2b92c6dcb825 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-caches/pom.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-volume-caches/pom.xml @@ -3,7 +3,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.boot.maven.it - build-image-caches + build-image-volume-caches 0.0.1.BUILD-SNAPSHOT UTF-8 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-volume-caches/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-volume-caches/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..03544b74e463 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-volume-caches/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2023 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.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java index 491deabe28eb..a64c0387073d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -32,8 +32,8 @@ public class CacheInfo { public CacheInfo() { } - CacheInfo(VolumeCacheInfo volumeCacheInfo) { - this.cache = Cache.volume(volumeCacheInfo.getName()); + private CacheInfo(Cache cache) { + this.cache = cache; } public void setVolume(VolumeCacheInfo info) { @@ -41,10 +41,23 @@ public void setVolume(VolumeCacheInfo info) { this.cache = Cache.volume(info.getName()); } + public void setBind(BindCacheInfo info) { + Assert.state(this.cache == null, "Each image building cache can be configured only once"); + this.cache = Cache.bind(info.getSource()); + } + Cache asCache() { return this.cache; } + static CacheInfo fromVolume(VolumeCacheInfo cacheInfo) { + return new CacheInfo(Cache.volume(cacheInfo.getName())); + } + + static CacheInfo fromBind(BindCacheInfo cacheInfo) { + return new CacheInfo(Cache.bind(cacheInfo.getSource())); + } + /** * Encapsulates configuration of an image building cache stored in a volume. */ @@ -69,4 +82,28 @@ void setName(String name) { } + /** + * Encapsulates configuration of an image building cache stored in a bind mount. + */ + public static class BindCacheInfo { + + private String source; + + public BindCacheInfo() { + } + + BindCacheInfo(String name) { + this.source = name; + } + + public String getSource() { + return this.source; + } + + void setSource(String source) { + this.source = source; + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java index 86625106bdbe..ed5a8e5d8ede 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java @@ -34,6 +34,7 @@ import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.io.Owner; import org.springframework.boot.buildpack.platform.io.TarArchive; +import org.springframework.boot.maven.CacheInfo.BindCacheInfo; import org.springframework.boot.maven.CacheInfo.VolumeCacheInfo; import static org.assertj.core.api.Assertions.assertThat; @@ -170,21 +171,37 @@ void getBuildRequestWhenHasTagsUsesTags() { } @Test - void getBuildRequestWhenHasBuildVolumeCacheUsesCache() { + void getBuildRequestWhenHasBuildCacheVolumeUsesCache() { Image image = new Image(); - image.buildCache = new CacheInfo(new VolumeCacheInfo("build-cache-vol")); + image.buildCache = CacheInfo.fromVolume(new VolumeCacheInfo("build-cache-vol")); BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); assertThat(request.getBuildCache()).isEqualTo(Cache.volume("build-cache-vol")); } @Test - void getBuildRequestWhenHasLaunchVolumeCacheUsesCache() { + void getBuildRequestWhenHasLaunchCacheVolumeUsesCache() { Image image = new Image(); - image.launchCache = new CacheInfo(new VolumeCacheInfo("launch-cache-vol")); + image.launchCache = CacheInfo.fromVolume(new VolumeCacheInfo("launch-cache-vol")); BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); assertThat(request.getLaunchCache()).isEqualTo(Cache.volume("launch-cache-vol")); } + @Test + void getBuildRequestWhenHasBuildCacheBindUsesCache() { + Image image = new Image(); + image.buildCache = CacheInfo.fromBind(new BindCacheInfo("build-cache-dir")); + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getBuildCache()).isEqualTo(Cache.bind("build-cache-dir")); + } + + @Test + void getBuildRequestWhenHasLaunchCacheBindUsesCache() { + Image image = new Image(); + image.launchCache = CacheInfo.fromBind(new BindCacheInfo("launch-cache-dir")); + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getLaunchCache()).isEqualTo(Cache.bind("launch-cache-dir")); + } + @Test void getBuildRequestWhenHasCreatedDateUsesCreatedDate() { Image image = new Image(); From e95c7bb67002ca137d070952b6adc70c51d0763a Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 22 Aug 2023 09:02:05 +0200 Subject: [PATCH 0343/1215] Upgrade to Groovy 4.0.14 Closes gh-37058 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 9b65c90857ae..ce57fe010c08 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -336,7 +336,7 @@ bom { ] } } - library("Groovy", "4.0.13") { + library("Groovy", "4.0.14") { group("org.apache.groovy") { imports = [ "groovy-bom" From 817b456402603941d4f2ff1fa2ac22bdc0784112 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 22 Aug 2023 09:02:10 +0200 Subject: [PATCH 0344/1215] Upgrade to Hazelcast 5.3.2 Closes gh-37059 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index ce57fe010c08..fb97bc3b6dab 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -366,7 +366,7 @@ bom { ] } } - library("Hazelcast", "5.3.1") { + library("Hazelcast", "5.3.2") { group("com.hazelcast") { modules = [ "hazelcast", From 564acf05c12c03a4ddf387db7bae05c75f5bef2a Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 22 Aug 2023 09:02:10 +0200 Subject: [PATCH 0345/1215] Upgrade to Spring AMQP 3.0.8 Closes gh-36940 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index fb97bc3b6dab..122394bf6889 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1377,7 +1377,7 @@ bom { ] } } - library("Spring AMQP", "3.0.8-SNAPSHOT") { + library("Spring AMQP", "3.0.8") { group("org.springframework.amqp") { imports = [ "spring-amqp-bom" From 95963e07a77cf8dcf4f340ec47cd45dd286a8255 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 22 Aug 2023 09:02:11 +0200 Subject: [PATCH 0346/1215] Upgrade to Spring Kafka 3.0.10 Closes gh-36942 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 122394bf6889..26a2ad63562d 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1434,7 +1434,7 @@ bom { ] } } - library("Spring Kafka", "3.0.10-SNAPSHOT") { + library("Spring Kafka", "3.0.10") { group("org.springframework.kafka") { modules = [ "spring-kafka", From fe0d88dbc4c2ba00d9504a3a7c1bd1845575660b Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 22 Aug 2023 09:02:15 +0200 Subject: [PATCH 0347/1215] Upgrade to Spring Security 6.2.0-M2 Closes gh-37060 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 26a2ad63562d..85c833a85c7f 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1466,7 +1466,7 @@ bom { ] } } - library("Spring Security", "6.2.0-M1") { + library("Spring Security", "6.2.0-M2") { group("org.springframework.security") { imports = [ "spring-security-bom" From 49e0d17a9bf74d46e698e345b6d896752040fb59 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 22 Aug 2023 11:38:26 +0200 Subject: [PATCH 0348/1215] Disable flaky reactor tests See gh-36302 --- .../boot/reactor/ReactorEnvironmentPostProcessorTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/ReactorEnvironmentPostProcessorTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/ReactorEnvironmentPostProcessorTests.java index b7aeae287780..29efc1f0b27a 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/ReactorEnvironmentPostProcessorTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/ReactorEnvironmentPostProcessorTests.java @@ -17,13 +17,13 @@ package org.springframework.boot.reactor; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledForJreRange; import org.junit.jupiter.api.condition.JRE; import reactor.core.Scannable; import reactor.core.publisher.Flux; -import org.springframework.boot.testsupport.classpath.ClassPathOverrides; import org.springframework.mock.env.MockEnvironment; import static org.assertj.core.api.Assertions.assertThat; @@ -34,7 +34,7 @@ * @author Brian Clozel */ -@ClassPathOverrides("io.projectreactor:reactor-tools:3.5.9") +@Disabled("Tests rely on static initialization and are flaky on CI") class ReactorEnvironmentPostProcessorTests { static { From a562caf226b72bc0382ee188206ae34344cd07f4 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 23 Aug 2023 09:38:38 +0200 Subject: [PATCH 0349/1215] Upgrade to Spring Integration 6.2.0-M2 Closes gh-36943 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 85c833a85c7f..f3242cba117a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1427,7 +1427,7 @@ bom { ] } } - library("Spring Integration", "6.2.0-SNAPSHOT") { + library("Spring Integration", "6.2.0-M2") { group("org.springframework.integration") { imports = [ "spring-integration-bom" From 00a698be5113fce087e3d62f9b1948f44566a6ee Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 23 Aug 2023 09:39:54 +0200 Subject: [PATCH 0350/1215] Upgrade to Spring Authorization Server 1.1.2 Closes gh-36941 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f3242cba117a..9cbe97650176 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1384,7 +1384,7 @@ bom { ] } } - library("Spring Authorization Server", "1.1.2-SNAPSHOT") { + library("Spring Authorization Server", "1.1.2") { group("org.springframework.security") { modules = [ "spring-security-oauth2-authorization-server" From 19df3934c61d94f414aa40f380954e51f83bac7d Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 23 Aug 2023 10:19:47 +0200 Subject: [PATCH 0351/1215] Upgrade to Flyway 9.21.2 Closes gh-37076 --- .../boot/autoconfigure/flyway/FlywayPropertiesTests.java | 2 ++ spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java index b2d3607dd843..0562b6c48e9a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayPropertiesTests.java @@ -114,6 +114,8 @@ void expectedPropertiesAreManaged() { "oracleWalletLocation", "sqlServerKerberosLoginFile"); // Properties that are managed by specific extensions ignoreProperties(properties, "oracle", "postgresql", "sqlserver"); + // https://github.com/flyway/flyway/issues/3732 + ignoreProperties(configuration, "environment"); // High level object we can't set with properties ignoreProperties(configuration, "callbacks", "classLoader", "dataSource", "javaMigrations", "javaMigrationClassProvider", "pluginRegister", "resourceProvider", "resolvers"); diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 9cbe97650176..eb2f669fd4be 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -283,7 +283,7 @@ bom { ] } } - library("Flyway", "9.21.1") { + library("Flyway", "9.21.2") { group("org.flywaydb") { modules = [ "flyway-core", From dbdd6739972d27b6571a11455d229baddd7c6d1a Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 23 Aug 2023 10:19:52 +0200 Subject: [PATCH 0352/1215] Upgrade to Maven Enforcer Plugin 3.4.0 Closes gh-37077 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index eb2f669fd4be..f82a4ee71153 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -897,7 +897,7 @@ bom { ] } } - library("Maven Enforcer Plugin", "3.3.0") { + library("Maven Enforcer Plugin", "3.4.0") { group("org.apache.maven.plugins") { plugins = [ "maven-enforcer-plugin" From 216b366bf1dacc32c3b22254301703c896e97fb9 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 23 Aug 2023 10:19:56 +0200 Subject: [PATCH 0353/1215] Upgrade to Mockito 5.5.0 Closes gh-37078 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f82a4ee71153..a0e380560836 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1000,7 +1000,7 @@ bom { ] } } - library("Mockito", "5.4.0") { + library("Mockito", "5.5.0") { group("org.mockito") { imports = [ "mockito-bom" From b8dc5343832d96215928aaed11cb002780145b44 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 23 Aug 2023 10:20:00 +0200 Subject: [PATCH 0354/1215] Upgrade to Testcontainers 1.19.0 Closes gh-37079 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index a0e380560836..9eaba28b98ca 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1498,7 +1498,7 @@ bom { ] } } - library("Testcontainers", "1.18.3") { + library("Testcontainers", "1.19.0") { group("org.testcontainers") { imports = [ "testcontainers-bom" From 0db1552a184277f10cd7936b9491a2a2e61f2dd0 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 24 Aug 2023 09:16:12 +0200 Subject: [PATCH 0355/1215] Upgrade to Netty 4.1.97.Final Closes gh-37090 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 9eaba28b98ca..a799457c5a2d 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1060,7 +1060,7 @@ bom { ] } } - library("Netty", "4.1.96.Final") { + library("Netty", "4.1.97.Final") { group("io.netty") { imports = [ "netty-bom" From adfaf372de80c22ced93c3a8c94887bf5d21a463 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 24 Aug 2023 09:16:16 +0200 Subject: [PATCH 0356/1215] Upgrade to RxJava3 3.1.7 Closes gh-37091 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index a799457c5a2d..fe3240284f73 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1239,7 +1239,7 @@ bom { ] } } - library("RxJava3", "3.1.6") { + library("RxJava3", "3.1.7") { group("io.reactivex.rxjava3") { modules = [ "rxjava" From 8b6e6b610cb9f158f4137976674f87e35fe12c4e Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 24 Aug 2023 09:16:53 +0200 Subject: [PATCH 0357/1215] Upgrade to Spring Batch 5.1.0-M2 Closes gh-36944 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index fe3240284f73..073721461873 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1391,7 +1391,7 @@ bom { ] } } - library("Spring Batch", "5.1.0-SNAPSHOT") { + library("Spring Batch", "5.1.0-M2") { group("org.springframework.batch") { imports = [ "spring-batch-bom" From c370ae2f83bf5c3a720a31f31aaef7cf1fdca7f8 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 24 Aug 2023 09:34:31 +0200 Subject: [PATCH 0358/1215] Upgrade to Kotlin 1.9.10 Closes gh-37092 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index cef268201872..db1bd02fa44c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ org.gradle.caching=true org.gradle.parallel=true org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 -kotlinVersion=1.9.0 +kotlinVersion=1.9.10 nativeBuildToolsVersion=0.9.24 springFrameworkVersion=6.1.0-M4 tomcatVersion=10.1.12 From 847c6aec0129facd77c6059e4dd9472350dd2f15 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Thu, 24 Aug 2023 20:06:14 +0900 Subject: [PATCH 0359/1215] Fix metadata of management.otlp.metrics.export.base-time-unit See gh-37094 --- .../META-INF/additional-spring-configuration-metadata.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index bd717a474b56..225b8841be1b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -2054,6 +2054,10 @@ "level": "error" } }, + { + "name": "management.otlp.metrics.export.base-time-unit", + "defaultValue": "milliseconds" + }, { "name": "management.otlp.tracing.compression", "defaultValue": "none" From 009d92744a77116f92af7b82a383a519b43ec3bb Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 24 Aug 2023 16:49:29 +0200 Subject: [PATCH 0360/1215] Add dependency management for Kotlin Serialization Closes gh-37093 --- spring-boot-project/spring-boot-dependencies/build.gradle | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 073721461873..2f839d2e6b5d 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -803,6 +803,13 @@ bom { ] } } + library("Kotlin Serialization", "1.6.0") { + group("org.jetbrains.kotlinx") { + imports = [ + "kotlinx-serialization-bom" + ] + } + } library("Lettuce", "6.2.6.RELEASE") { group("io.lettuce") { modules = [ From 30eacd553d717417a23bd4d0c48ffb5df67dbd56 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Tue, 29 Aug 2023 00:22:38 +0900 Subject: [PATCH 0361/1215] Add Javadoc since for new setTaskExecutor method See gh-37117 --- .../amqp/AbstractRabbitListenerContainerFactoryConfigurer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractRabbitListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractRabbitListenerContainerFactoryConfigurer.java index 781029791b22..bcfe04a6748f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractRabbitListenerContainerFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractRabbitListenerContainerFactoryConfigurer.java @@ -87,6 +87,7 @@ protected void setRetryTemplateCustomizers(List r /** * Set the task executor to use. * @param taskExecutor the task executor + * @since 3.2.0 */ public void setTaskExecutor(Executor taskExecutor) { this.taskExecutor = taskExecutor; From f8f4a2b9864341be80c0d40673f699bfb45665cb Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 30 Aug 2023 19:58:00 -0700 Subject: [PATCH 0362/1215] Fix Timezone used for now in BuildRequestTests --- .../boot/buildpack/platform/build/BuildRequestTests.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java index 1ded1fa52613..e74302a1636f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java @@ -53,6 +53,8 @@ */ class BuildRequestTests { + private static final ZoneId UTC = ZoneId.of("UTC"); + @TempDir File tempDir; @@ -271,15 +273,15 @@ void withCreatedDateSetsCreatedDate() throws Exception { @Test void withCreatedDateNowSetsCreatedDate() throws Exception { - OffsetDateTime now = OffsetDateTime.now(); + OffsetDateTime now = OffsetDateTime.now(UTC); BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); BuildRequest withCreatedDate = request.withCreatedDate("now"); - OffsetDateTime createdDate = OffsetDateTime.ofInstant(withCreatedDate.getCreatedDate(), ZoneId.of("UTC")); + OffsetDateTime createdDate = OffsetDateTime.ofInstant(withCreatedDate.getCreatedDate(), UTC); assertThat(createdDate.getYear()).isEqualTo(now.getYear()); assertThat(createdDate.getMonth()).isEqualTo(now.getMonth()); assertThat(createdDate.getDayOfMonth()).isEqualTo(now.getDayOfMonth()); withCreatedDate = request.withCreatedDate("NOW"); - createdDate = OffsetDateTime.ofInstant(withCreatedDate.getCreatedDate(), ZoneId.of("UTC")); + createdDate = OffsetDateTime.ofInstant(withCreatedDate.getCreatedDate(), UTC); assertThat(createdDate.getYear()).isEqualTo(now.getYear()); assertThat(createdDate.getMonth()).isEqualTo(now.getMonth()); assertThat(createdDate.getDayOfMonth()).isEqualTo(now.getDayOfMonth()); From 208dcb9305aeffb1059b1881ec394c047bf13fcd Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 31 Aug 2023 11:06:02 +0100 Subject: [PATCH 0363/1215] Rename tests now that Jetty is preferred Closes gh-37155 --- ...rviceMessageSenderBuilderJettyClientIntegrationTests.java} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/{HttpWebServiceMessageSenderBuilderOkHttp3IntegrationTests.java => HttpWebServiceMessageSenderBuilderJettyClientIntegrationTests.java} (95%) diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderOkHttp3IntegrationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderJettyClientIntegrationTests.java similarity index 95% rename from spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderOkHttp3IntegrationTests.java rename to spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderJettyClientIntegrationTests.java index 3215dad49900..ce64fddef180 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderOkHttp3IntegrationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilderJettyClientIntegrationTests.java @@ -32,12 +32,12 @@ /** * Tests for {@link HttpWebServiceMessageSenderBuilder} when Http Components is not - * available. + * available and, therefore, Jetty's client is used instead. * * @author Stephane Nicoll */ @ClassPathExclusions("httpclient5-*.jar") -class HttpWebServiceMessageSenderBuilderOkHttp3IntegrationTests { +class HttpWebServiceMessageSenderBuilderJettyClientIntegrationTests { private final HttpWebServiceMessageSenderBuilder builder = new HttpWebServiceMessageSenderBuilder(); From b8eec2a8a42a9ba7bee8fa92c10f99bd216b97b5 Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Tue, 29 Aug 2023 09:23:58 +0800 Subject: [PATCH 0364/1215] Include JdbcClientAutoConfiguration in @JdbcTest and @DataJpaTest slices See gh-37122 --- ...re.data.jdbc.AutoConfigureDataJdbc.imports | 1 + ...toconfigure.jdbc.AutoConfigureJdbc.imports | 1 + ...igure.orm.jpa.AutoConfigureDataJpa.imports | 1 + .../jdbc/ExampleEntityRowMapper.java | 36 +++++++++++ .../jdbc/ExampleJdbcClientRepository.java | 62 +++++++++++++++++++ .../autoconfigure/jdbc/ExampleRepository.java | 14 ----- .../jdbc/JdbcTestIntegrationTests.java | 24 ++++++- .../orm/jpa/DataJpaTestIntegrationTests.java | 11 +++- 8 files changed, 133 insertions(+), 17 deletions(-) create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleEntityRowMapper.java create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleJdbcClientRepository.java diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.data.jdbc.AutoConfigureDataJdbc.imports b/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.data.jdbc.AutoConfigureDataJdbc.imports index 2fc2a1e54ad0..eb4b3faada1b 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.data.jdbc.AutoConfigureDataJdbc.imports +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.data.jdbc.AutoConfigureDataJdbc.imports @@ -3,6 +3,7 @@ org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfigurati org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration +org.springframework.boot.autoconfigure.jdbc.JdbcClientAutoConfiguration org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureJdbc.imports b/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureJdbc.imports index 5aa3ad940cf4..480dcff0e7c1 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureJdbc.imports +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureJdbc.imports @@ -2,6 +2,7 @@ org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration +org.springframework.boot.autoconfigure.jdbc.JdbcClientAutoConfiguration org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureDataJpa.imports b/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureDataJpa.imports index ba99875857e7..83465fdeba7e 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureDataJpa.imports +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureDataJpa.imports @@ -3,6 +3,7 @@ org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration +org.springframework.boot.autoconfigure.jdbc.JdbcClientAutoConfiguration org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleEntityRowMapper.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleEntityRowMapper.java new file mode 100644 index 000000000000..5897f26716bc --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleEntityRowMapper.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2023 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.test.autoconfigure.jdbc; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.jdbc.core.RowMapper; + +/** + * @author Stephane Nicoll + */ +class ExampleEntityRowMapper implements RowMapper { + + @Override + public ExampleEntity mapRow(ResultSet rs, int rowNum) throws SQLException { + int id = rs.getInt("id"); + String name = rs.getString("name"); + return new ExampleEntity(id, name); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleJdbcClientRepository.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleJdbcClientRepository.java new file mode 100644 index 000000000000..1140090d4ba8 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleJdbcClientRepository.java @@ -0,0 +1,62 @@ +/* + * Copyright 2023 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.test.autoconfigure.jdbc; + +import java.util.Collection; + +import jakarta.transaction.Transactional; + +import org.springframework.jdbc.core.simple.JdbcClient; +import org.springframework.stereotype.Repository; + +/** + * Example repository used with {@link JdbcClient JdbcClient} and + * {@link JdbcTest @JdbcTest} tests. + * + * @author Yanming Zhou + */ +@Repository +public class ExampleJdbcClientRepository { + + private static final ExampleEntityRowMapper ROW_MAPPER = new ExampleEntityRowMapper(); + + private final JdbcClient jdbcClient; + + public ExampleJdbcClientRepository(JdbcClient jdbcClient) { + this.jdbcClient = jdbcClient; + } + + @Transactional + public void save(ExampleEntity entity) { + this.jdbcClient.sql("insert into example (id, name) values (:id, :name)") + .param("id", entity.getId()) + .param("name", entity.getName()) + .update(); + } + + public ExampleEntity findById(int id) { + return this.jdbcClient.sql("select id, name from example where id =:id") + .param("id", id) + .query(ROW_MAPPER) + .single(); + } + + public Collection findAll() { + return this.jdbcClient.sql("select id, name from example").query(ROW_MAPPER).list(); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleRepository.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleRepository.java index 9ac32aeba4d4..0f8c44e3bf09 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleRepository.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleRepository.java @@ -16,14 +16,11 @@ package org.springframework.boot.test.autoconfigure.jdbc; -import java.sql.ResultSet; -import java.sql.SQLException; import java.util.Collection; import jakarta.transaction.Transactional; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Repository; /** @@ -55,15 +52,4 @@ public Collection findAll() { return this.jdbcTemplate.query("select id, name from example", ROW_MAPPER); } - static class ExampleEntityRowMapper implements RowMapper { - - @Override - public ExampleEntity mapRow(ResultSet rs, int rowNum) throws SQLException { - int id = rs.getInt("id"); - String name = rs.getString("name"); - return new ExampleEntity(id, name); - } - - } - } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestIntegrationTests.java index 9cf7781d3b99..44d6fc2ed241 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/JdbcTestIntegrationTests.java @@ -29,6 +29,7 @@ import org.springframework.boot.testcontainers.service.connection.ServiceConnectionAutoConfiguration; import org.springframework.context.ApplicationContext; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.simple.JdbcClient; import org.springframework.test.context.TestPropertySource; import static org.assertj.core.api.Assertions.assertThat; @@ -39,12 +40,16 @@ * Integration tests for {@link JdbcTest @JdbcTest}. * * @author Stephane Nicoll + * @author Yanming Zhou */ @JdbcTest @TestPropertySource( properties = "spring.sql.init.schemaLocations=classpath:org/springframework/boot/test/autoconfigure/jdbc/schema.sql") class JdbcTestIntegrationTests { + @Autowired + private JdbcClient jdbcClient; + @Autowired private JdbcTemplate jdbcTemplate; @@ -54,13 +59,30 @@ class JdbcTestIntegrationTests { @Autowired private ApplicationContext applicationContext; + @Test + void testJdbcClient() { + ExampleJdbcClientRepository repository = new ExampleJdbcClientRepository(this.jdbcClient); + repository.save(new ExampleEntity(1, "John")); + ExampleEntity entity = repository.findById(1); + assertThat(entity.getId()).isOne(); + assertThat(entity.getName()).isEqualTo("John"); + Collection entities = repository.findAll(); + assertThat(entities).hasSize(1); + entity = entities.iterator().next(); + assertThat(entity.getId()).isOne(); + assertThat(entity.getName()).isEqualTo("John"); + } + @Test void testJdbcTemplate() { ExampleRepository repository = new ExampleRepository(this.jdbcTemplate); repository.save(new ExampleEntity(1, "John")); + ExampleEntity entity = repository.findById(1); + assertThat(entity.getId()).isOne(); + assertThat(entity.getName()).isEqualTo("John"); Collection entities = repository.findAll(); assertThat(entities).hasSize(1); - ExampleEntity entity = entities.iterator().next(); + entity = entities.iterator().next(); assertThat(entity.getId()).isOne(); assertThat(entity.getName()).isEqualTo("John"); } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTestIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTestIntegrationTests.java index 277677ab0f9a..f1f84cf1fe81 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTestIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/orm/jpa/DataJpaTestIntegrationTests.java @@ -28,6 +28,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.data.repository.config.BootstrapMode; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.simple.JdbcClient; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -39,6 +40,7 @@ * @author Phillip Webb * @author Andy Wilkinson * @author Scott Frederick + * @author Yanming Zhou */ @DataJpaTest class DataJpaTestIntegrationTests { @@ -46,6 +48,9 @@ class DataJpaTestIntegrationTests { @Autowired private TestEntityManager entities; + @Autowired + private JdbcClient jdbcClient; + @Autowired private JdbcTemplate jdbcTemplate; @@ -72,8 +77,10 @@ void testEntityManagerPersistAndGetId() { Long id = this.entities.persistAndGetId(new ExampleEntity("spring", "123"), Long.class); this.entities.flush(); assertThat(id).isNotNull(); - String reference = this.jdbcTemplate.queryForObject("SELECT REFERENCE FROM EXAMPLE_ENTITY WHERE ID = ?", - String.class, id); + String sql = "SELECT REFERENCE FROM EXAMPLE_ENTITY WHERE ID = ?"; + String reference = this.jdbcTemplate.queryForObject(sql, String.class, id); + assertThat(reference).isEqualTo("123"); + reference = this.jdbcClient.sql(sql).param(id).query(String.class).single(); assertThat(reference).isEqualTo("123"); } From 947e330e9d4cfc537213554f855f6c551e4d3652 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 31 Aug 2023 13:15:50 +0200 Subject: [PATCH 0365/1215] Polish "Include JdbcClientAutoConfiguration in @JdbcTest and @DataJpaTest slices" See gh-37122 --- .../jdbc/ExampleJdbcClientRepository.java | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleJdbcClientRepository.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleJdbcClientRepository.java index 1140090d4ba8..3ba42164716e 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleJdbcClientRepository.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleJdbcClientRepository.java @@ -18,10 +18,7 @@ import java.util.Collection; -import jakarta.transaction.Transactional; - import org.springframework.jdbc.core.simple.JdbcClient; -import org.springframework.stereotype.Repository; /** * Example repository used with {@link JdbcClient JdbcClient} and @@ -29,33 +26,31 @@ * * @author Yanming Zhou */ -@Repository -public class ExampleJdbcClientRepository { +class ExampleJdbcClientRepository { private static final ExampleEntityRowMapper ROW_MAPPER = new ExampleEntityRowMapper(); private final JdbcClient jdbcClient; - public ExampleJdbcClientRepository(JdbcClient jdbcClient) { + ExampleJdbcClientRepository(JdbcClient jdbcClient) { this.jdbcClient = jdbcClient; } - @Transactional - public void save(ExampleEntity entity) { + void save(ExampleEntity entity) { this.jdbcClient.sql("insert into example (id, name) values (:id, :name)") .param("id", entity.getId()) .param("name", entity.getName()) .update(); } - public ExampleEntity findById(int id) { - return this.jdbcClient.sql("select id, name from example where id =:id") + ExampleEntity findById(int id) { + return this.jdbcClient.sql("select id, name from example where id = :id") .param("id", id) .query(ROW_MAPPER) .single(); } - public Collection findAll() { + Collection findAll() { return this.jdbcClient.sql("select id, name from example").query(ROW_MAPPER).list(); } From cf2828fdb880b3e557c65af78e615e6c6457d988 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 31 Aug 2023 13:22:52 +0200 Subject: [PATCH 0366/1215] Add link to Spring Modulith to documentation Closes gh-37106 --- .../src/docs/asciidoc/using/structuring-your-code.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/structuring-your-code.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/structuring-your-code.adoc index 896651f20550..406f22260865 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/structuring-your-code.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/structuring-your-code.adoc @@ -3,6 +3,8 @@ Spring Boot does not require any specific code layout to work. However, there are some best practices that help. +TIP: If you wish to enforce a structure based on domains, take a look at https://spring.io/projects/spring-modulith#overview[Spring Modulith]. + [[using.structuring-your-code.using-the-default-package]] From f1f4e9c0087c28f3e3615556ea4691a6c3b0c9d9 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 31 Aug 2023 14:01:44 +0200 Subject: [PATCH 0367/1215] Implement RestClientBuilderConfigurer Closes gh-36265 --- .../client/RestClientAutoConfiguration.java | 22 ++++--- .../client/RestClientBuilderConfigurer.java | 58 +++++++++++++++++++ .../RestClientAutoConfigurationTests.java | 22 +++++++ .../RestClientBuilderConfigurerTests.java | 54 +++++++++++++++++ 4 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientBuilderConfigurer.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientBuilderConfigurerTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java index 1543caa80da0..31b7f3924415 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java @@ -36,11 +36,12 @@ /** * {@link EnableAutoConfiguration Auto-configuration} for {@link RestClient}. *

    - * This will produce a {@link org.springframework.web.client.RestClient.Builder - * RestClient.Builder} bean with the {@code prototype} scope, meaning each injection point - * will receive a newly cloned instance of the builder. + * This will produce a {@link RestClient.Builder RestClient.Builder} bean with the + * {@code prototype} scope, meaning each injection point will receive a newly cloned + * instance of the builder. * * @author Arjen Poutsma + * @author Moritz Halbritter * @since 3.2.0 */ @AutoConfiguration(after = HttpMessageConvertersAutoConfiguration.class) @@ -51,19 +52,26 @@ public class RestClientAutoConfiguration { @Bean @ConditionalOnMissingBean @Order(Ordered.LOWEST_PRECEDENCE) - public HttpMessageConvertersRestClientCustomizer httpMessageConvertersRestClientCustomizer( + HttpMessageConvertersRestClientCustomizer httpMessageConvertersRestClientCustomizer( ObjectProvider messageConverters) { return new HttpMessageConvertersRestClientCustomizer(messageConverters.getIfUnique()); } + @Bean + @ConditionalOnMissingBean + RestClientBuilderConfigurer restClientBuilderConfigurer(ObjectProvider customizerProvider) { + RestClientBuilderConfigurer configurer = new RestClientBuilderConfigurer(); + configurer.setRestClientCustomizers(customizerProvider.orderedStream().toList()); + return configurer; + } + @Bean @Scope("prototype") @ConditionalOnMissingBean - public RestClient.Builder restClientBuilder(ObjectProvider customizerProvider) { + RestClient.Builder restClientBuilder(RestClientBuilderConfigurer restClientBuilderConfigurer) { RestClient.Builder builder = RestClient.builder() .requestFactory(ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS)); - customizerProvider.orderedStream().forEach((customizer) -> customizer.customize(builder)); - return builder; + return restClientBuilderConfigurer.configure(builder); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientBuilderConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientBuilderConfigurer.java new file mode 100644 index 000000000000..8d6f57bd461f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientBuilderConfigurer.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2023 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.web.client; + +import java.util.List; + +import org.springframework.boot.web.client.RestClientCustomizer; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.Builder; + +/** + * Configure {@link RestClient.Builder} with sensible defaults. + * + * @author Moritz Halbritter + * @since 3.2.0 + */ +public class RestClientBuilderConfigurer { + + private List customizers; + + void setRestClientCustomizers(List customizers) { + this.customizers = customizers; + } + + /** + * Configure the specified {@link RestClient.Builder}. The builder can be further + * tuned and default settings can be overridden. + * @param builder the {@link RestClient.Builder} instance to configure + * @return the configured builder + */ + public RestClient.Builder configure(RestClient.Builder builder) { + applyCustomizers(builder); + return builder; + } + + private void applyCustomizers(Builder builder) { + if (this.customizers != null) { + for (RestClientCustomizer customizer : this.customizers) { + customizer.customize(builder); + } + } + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java index b64a62d1f3c3..d2fb90dbf9a0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java @@ -32,6 +32,7 @@ import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.Builder; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -42,12 +43,22 @@ * Tests for {@link RestClientAutoConfiguration} * * @author Arjen Poutsma + * @author Moritz Halbritter */ class RestClientAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class)); + @Test + void shouldSupplyBeans() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(HttpMessageConvertersRestClientCustomizer.class); + assertThat(context).hasSingleBean(RestClientBuilderConfigurer.class); + assertThat(context).hasSingleBean(RestClient.Builder.class); + }); + } + @Test void shouldCreateBuilder() { this.contextRunner.run((context) -> { @@ -57,6 +68,17 @@ void shouldCreateBuilder() { }); } + @Test + void configurerShouldCallCustomizers() { + this.contextRunner.withUserConfiguration(RestClientCustomizerConfig.class).run((context) -> { + RestClientBuilderConfigurer configurer = context.getBean(RestClientBuilderConfigurer.class); + RestClientCustomizer customizer = context.getBean("restClientCustomizer", RestClientCustomizer.class); + Builder builder = RestClient.builder(); + configurer.configure(builder); + then(customizer).should().customize(builder); + }); + } + @Test void restClientShouldApplyCustomizers() { this.contextRunner.withUserConfiguration(RestClientCustomizerConfig.class).run((context) -> { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientBuilderConfigurerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientBuilderConfigurerTests.java new file mode 100644 index 000000000000..c4c8395c2177 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientBuilderConfigurerTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2023 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.web.client; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.web.client.RestClientCustomizer; +import org.springframework.web.client.RestClient; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RestClientBuilderConfigurer}. + * + * @author Moritz Halbritter + */ +class RestClientBuilderConfigurerTests { + + @Test + void shouldApplyCustomizers() { + RestClientBuilderConfigurer configurer = new RestClientBuilderConfigurer(); + RestClientCustomizer customizer = mock(RestClientCustomizer.class); + configurer.setRestClientCustomizers(List.of(customizer)); + RestClient.Builder builder = RestClient.builder(); + configurer.configure(builder); + then(customizer).should().customize(builder); + } + + @Test + void shouldSupportNullAsCustomizers() { + RestClientBuilderConfigurer configurer = new RestClientBuilderConfigurer(); + configurer.setRestClientCustomizers(null); + assertThatCode(() -> configurer.configure(RestClient.builder())).doesNotThrowAnyException(); + } + +} From 6e7b845bdf50dbac8e433c63b99c8e9e7fa2e668 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Wed, 30 Aug 2023 16:39:37 -0700 Subject: [PATCH 0368/1215] Add support for Apache Pulsar Add support for Apache Pulsar using the Spring for Apache Pulsar project. See gh-34763 Co-authored-by: Phillip Webb --- .../DocumentConfigurationProperties.java | 1 + .../spring-boot-autoconfigure/build.gradle | 3 + .../pulsar/DeadLetterPolicyMapper.java | 49 + .../pulsar/PulsarAutoConfiguration.java | 196 ++++ .../pulsar/PulsarConfiguration.java | 173 ++++ .../pulsar/PulsarProperties.java | 890 ++++++++++++++++++ .../pulsar/PulsarPropertiesMapper.java | 166 ++++ .../PulsarReactiveAutoConfiguration.java | 201 ++++ .../PulsarReactivePropertiesMapper.java | 108 +++ .../autoconfigure/pulsar/package-info.java | 20 + ...itional-spring-configuration-metadata.json | 12 + ...ot.autoconfigure.AutoConfiguration.imports | 2 + .../autoconfigure/pulsar/Customizers.java | 106 +++ .../pulsar/DeadLetterPolicyMapperTests.java | 55 ++ ...lsarAutoConfigurationIntegrationTests.java | 118 +++ .../pulsar/PulsarAutoConfigurationTests.java | 519 ++++++++++ .../pulsar/PulsarConfigurationTests.java | 318 +++++++ .../pulsar/PulsarPropertiesMapperTests.java | 190 ++++ .../pulsar/PulsarPropertiesTests.java | 365 +++++++ .../PulsarReactiveAutoConfigurationTests.java | 473 ++++++++++ .../PulsarReactivePropertiesMapperTests.java | 146 +++ .../spring-boot-dependencies/build.gradle | 98 ++ .../spring-boot-docs/build.gradle | 3 + .../src/docs/asciidoc/attributes.adoc | 1 + .../asciidoc/documentation/messaging.adoc | 1 + .../src/docs/asciidoc/features/ssl.adoc | 1 - .../src/docs/asciidoc/index.adoc | 2 +- .../src/docs/asciidoc/messaging.adoc | 4 +- .../src/docs/asciidoc/messaging/pulsar.adoc | 201 ++++ .../docs/messaging/pulsar/reading/MyBean.java | 30 + .../pulsar/readingreactive/MyBean.java | 50 + .../messaging/pulsar/receiving/MyBean.java | 30 + .../pulsar/receivingreactive/MyBean.java | 33 + .../docs/messaging/pulsar/sending/MyBean.java | 37 + .../pulsar/sendingreactive/MyBean.java | 35 + .../docs/messaging/pulsar/reading/MyBean.kt | 32 + .../pulsar/readingreactive/MyBean.kt | 44 + .../docs/messaging/pulsar/receiving/MyBean.kt | 32 + .../pulsar/receivingreactive/MyBean.kt | 32 + .../docs/messaging/pulsar/sending/MyBean.kt | 33 + .../pulsar/sendingreactive/MyBean.kt | 28 + .../build.gradle | 19 + .../spring-boot-starter-pulsar/build.gradle | 16 + .../testcontainers/DockerImageNames.java | 11 + .../build.gradle | 15 + .../pulsar/reactive/SampleMessage.java | 20 + .../reactive/SampleMessageConsumer.java | 43 + .../SampleReactivePulsarApplication.java | 53 ++ .../src/main/resources/application.properties | 1 + .../SampleReactivePulsarApplicationTests.java | 60 ++ .../build.gradle | 15 + .../java/smoketest/pulsar/SampleMessage.java | 20 + .../pulsar/SampleMessageConsumer.java | 40 + .../pulsar/SamplePulsarApplication.java | 52 + .../src/main/resources/application.properties | 1 + .../pulsar/SamplePulsarApplicationTests.java | 60 ++ 56 files changed, 5261 insertions(+), 3 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/DeadLetterPolicyMapper.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarReactiveAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarReactivePropertiesMapper.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/package-info.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/Customizers.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/DeadLetterPolicyMapperTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarReactiveAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarReactivePropertiesMapperTests.java create mode 100644 spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/pulsar.adoc create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/reading/MyBean.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/readingreactive/MyBean.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/receiving/MyBean.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/receivingreactive/MyBean.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/sending/MyBean.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/sendingreactive/MyBean.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/reading/MyBean.kt create mode 100644 spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/readingreactive/MyBean.kt create mode 100644 spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/receiving/MyBean.kt create mode 100644 spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/receivingreactive/MyBean.kt create mode 100644 spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/sending/MyBean.kt create mode 100644 spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/sendingreactive/MyBean.kt create mode 100644 spring-boot-project/spring-boot-starters/spring-boot-starter-pulsar-reactive/build.gradle create mode 100644 spring-boot-project/spring-boot-starters/spring-boot-starter-pulsar/build.gradle create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/build.gradle create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleMessage.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleMessageConsumer.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleReactivePulsarApplication.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/resources/application.properties create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/test/java/smoketest/pulsar/reactive/SampleReactivePulsarApplicationTests.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/build.gradle create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SampleMessage.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SampleMessageConsumer.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SamplePulsarApplication.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/resources/application.properties create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/test/java/smoketest/pulsar/SamplePulsarApplicationTests.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 8019eb3a27bd..cedb706ea076 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 @@ -171,6 +171,7 @@ private void integrationPrefixes(Config prefix) { prefix.accept("spring.integration"); prefix.accept("spring.jms"); prefix.accept("spring.kafka"); + prefix.accept("spring.pulsar"); prefix.accept("spring.rabbitmq"); prefix.accept("spring.hazelcast"); prefix.accept("spring.webservices"); diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index a30895a521d8..b48da7b7bf45 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -179,6 +179,8 @@ dependencies { optional("org.springframework.data:spring-data-redis") optional("org.springframework.graphql:spring-graphql") optional("org.springframework.hateoas:spring-hateoas") + optional("org.springframework.pulsar:spring-pulsar") + optional("org.springframework.pulsar:spring-pulsar-reactive") optional("org.springframework.security:spring-security-acl") optional("org.springframework.security:spring-security-config") optional("org.springframework.security:spring-security-data") { @@ -255,6 +257,7 @@ dependencies { testImplementation("org.testcontainers:junit-jupiter") testImplementation("org.testcontainers:mongodb") testImplementation("org.testcontainers:neo4j") + testImplementation("org.testcontainers:pulsar") testImplementation("org.testcontainers:testcontainers") testImplementation("org.yaml:snakeyaml") diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/DeadLetterPolicyMapper.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/DeadLetterPolicyMapper.java new file mode 100644 index 000000000000..fc4a4f64b6bc --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/DeadLetterPolicyMapper.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2023 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.pulsar; + +import org.apache.pulsar.client.api.DeadLetterPolicy; +import org.apache.pulsar.client.api.DeadLetterPolicy.DeadLetterPolicyBuilder; + +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.util.Assert; + +/** + * Helper class used to map {@link PulsarProperties.Consumer.DeadLetterPolicy dead letter + * policy properties}. + * + * @author Chris Bono + * @author Phillip Webb + */ +final class DeadLetterPolicyMapper { + + private DeadLetterPolicyMapper() { + } + + static DeadLetterPolicy map(PulsarProperties.Consumer.DeadLetterPolicy policy) { + Assert.state(policy.getMaxRedeliverCount() > 0, + "Pulsar DeadLetterPolicy must have a positive 'max-redelivery-count' property value"); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + DeadLetterPolicyBuilder builder = DeadLetterPolicy.builder(); + map.from(policy::getMaxRedeliverCount).to(builder::maxRedeliverCount); + map.from(policy::getRetryLetterTopic).to(builder::retryLetterTopic); + map.from(policy::getDeadLetterTopic).to(builder::deadLetterTopic); + map.from(policy::getInitialSubscriptionName).to(builder::initialSubscriptionName); + return builder.build(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfiguration.java new file mode 100644 index 000000000000..9ed6ae3b09e7 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfiguration.java @@ -0,0 +1,196 @@ +/* + * Copyright 2012-2023 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.pulsar; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.pulsar.client.api.ConsumerBuilder; +import org.apache.pulsar.client.api.ProducerBuilder; +import org.apache.pulsar.client.api.PulsarClient; +import org.apache.pulsar.client.api.ReaderBuilder; +import org.apache.pulsar.client.api.interceptor.ProducerInterceptor; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.util.LambdaSafe; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.pulsar.annotation.EnablePulsar; +import org.springframework.pulsar.config.ConcurrentPulsarListenerContainerFactory; +import org.springframework.pulsar.config.DefaultPulsarReaderContainerFactory; +import org.springframework.pulsar.config.PulsarAnnotationSupportBeanNames; +import org.springframework.pulsar.core.CachingPulsarProducerFactory; +import org.springframework.pulsar.core.ConsumerBuilderCustomizer; +import org.springframework.pulsar.core.DefaultPulsarConsumerFactory; +import org.springframework.pulsar.core.DefaultPulsarProducerFactory; +import org.springframework.pulsar.core.DefaultPulsarReaderFactory; +import org.springframework.pulsar.core.ProducerBuilderCustomizer; +import org.springframework.pulsar.core.PulsarConsumerFactory; +import org.springframework.pulsar.core.PulsarProducerFactory; +import org.springframework.pulsar.core.PulsarReaderFactory; +import org.springframework.pulsar.core.PulsarTemplate; +import org.springframework.pulsar.core.ReaderBuilderCustomizer; +import org.springframework.pulsar.core.SchemaResolver; +import org.springframework.pulsar.core.TopicResolver; +import org.springframework.pulsar.listener.PulsarContainerProperties; +import org.springframework.pulsar.reader.PulsarReaderContainerProperties; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Apache Pulsar. + * + * @author Chris Bono + * @author Soby Chacko + * @author Alexander Preuß + * @author Phillip Webb + * @since 3.2.0 + */ +@AutoConfiguration +@ConditionalOnClass({ PulsarClient.class, PulsarTemplate.class }) +@Import(PulsarConfiguration.class) +public class PulsarAutoConfiguration { + + private PulsarProperties properties; + + private PulsarPropertiesMapper propertiesMapper; + + PulsarAutoConfiguration(PulsarProperties properties) { + this.properties = properties; + this.propertiesMapper = new PulsarPropertiesMapper(properties); + } + + @Bean + @ConditionalOnMissingBean(PulsarProducerFactory.class) + @ConditionalOnProperty(name = "spring.pulsar.producer.cache.enabled", havingValue = "false") + DefaultPulsarProducerFactory pulsarProducerFactory(PulsarClient pulsarClient, TopicResolver topicResolver, + ObjectProvider> customizersProvider) { + List> lambdaSafeCustomizers = lambdaSafeProducerBuilderCustomizers( + customizersProvider); + return new DefaultPulsarProducerFactory<>(pulsarClient, this.properties.getProducer().getTopicName(), + lambdaSafeCustomizers, topicResolver); + } + + @Bean + @ConditionalOnMissingBean(PulsarProducerFactory.class) + @ConditionalOnProperty(name = "spring.pulsar.producer.cache.enabled", havingValue = "true", matchIfMissing = true) + CachingPulsarProducerFactory cachingPulsarProducerFactory(PulsarClient pulsarClient, TopicResolver topicResolver, + ObjectProvider> customizersProvider) { + PulsarProperties.Producer.Cache cacheProperties = this.properties.getProducer().getCache(); + List> lambdaSafeCustomizers = lambdaSafeProducerBuilderCustomizers( + customizersProvider); + return new CachingPulsarProducerFactory<>(pulsarClient, this.properties.getProducer().getTopicName(), + lambdaSafeCustomizers, topicResolver, cacheProperties.getExpireAfterAccess(), + cacheProperties.getMaximumSize(), cacheProperties.getInitialCapacity()); + } + + private List> lambdaSafeProducerBuilderCustomizers( + ObjectProvider> customizersProvider) { + List> customizers = new ArrayList<>(); + customizers.add(this.propertiesMapper::customizeProducerBuilder); + customizers.addAll(customizersProvider.orderedStream().toList()); + return List.of((builder) -> applyProducerBuilderCustomizers(customizers, builder)); + } + + @SuppressWarnings("unchecked") + private void applyProducerBuilderCustomizers(List> customizers, + ProducerBuilder builder) { + LambdaSafe.callbacks(ProducerBuilderCustomizer.class, customizers, builder) + .invoke((customizer) -> customizer.customize(builder)); + } + + @Bean + @ConditionalOnMissingBean + PulsarTemplate pulsarTemplate(PulsarProducerFactory pulsarProducerFactory, + ObjectProvider producerInterceptors, SchemaResolver schemaResolver, + TopicResolver topicResolver) { + return new PulsarTemplate<>(pulsarProducerFactory, producerInterceptors.orderedStream().toList(), + schemaResolver, topicResolver, this.properties.getTemplate().isObservationsEnabled()); + } + + @Bean + @ConditionalOnMissingBean(PulsarConsumerFactory.class) + DefaultPulsarConsumerFactory pulsarConsumerFactory(PulsarClient pulsarClient, + ObjectProvider> customizersProvider) { + List> customizers = new ArrayList<>(); + customizers.add(this.propertiesMapper::customizeConsumerBuilder); + customizers.addAll(customizersProvider.orderedStream().toList()); + List> lambdaSafeCustomizers = List + .of((builder) -> applyConsumerBuilderCustomizers(customizers, builder)); + return new DefaultPulsarConsumerFactory<>(pulsarClient, lambdaSafeCustomizers); + } + + @SuppressWarnings("unchecked") + private void applyConsumerBuilderCustomizers(List> customizers, + ConsumerBuilder builder) { + LambdaSafe.callbacks(ConsumerBuilderCustomizer.class, customizers, builder) + .invoke((customizer) -> customizer.customize(builder)); + } + + @Bean + @ConditionalOnMissingBean(name = "pulsarListenerContainerFactory") + ConcurrentPulsarListenerContainerFactory pulsarListenerContainerFactory( + PulsarConsumerFactory pulsarConsumerFactory, SchemaResolver schemaResolver, + TopicResolver topicResolver) { + PulsarContainerProperties containerProperties = new PulsarContainerProperties(); + containerProperties.setSchemaResolver(schemaResolver); + containerProperties.setTopicResolver(topicResolver); + this.propertiesMapper.customizeContainerProperties(containerProperties); + return new ConcurrentPulsarListenerContainerFactory<>(pulsarConsumerFactory, containerProperties); + } + + @Bean + @ConditionalOnMissingBean(PulsarReaderFactory.class) + DefaultPulsarReaderFactory pulsarReaderFactory(PulsarClient pulsarClient, + ObjectProvider> customizersProvider) { + List> customizers = new ArrayList<>(); + customizers.add(this.propertiesMapper::customizeReaderBuilder); + customizers.addAll(customizersProvider.orderedStream().toList()); + List> lambdaSafeCustomizers = List + .of((builder) -> applyReaderBuilderCustomizers(customizers, builder)); + return new DefaultPulsarReaderFactory<>(pulsarClient, lambdaSafeCustomizers); + } + + @SuppressWarnings("unchecked") + private void applyReaderBuilderCustomizers(List> customizers, ReaderBuilder builder) { + LambdaSafe.callbacks(ReaderBuilderCustomizer.class, customizers, builder) + .invoke((customizer) -> customizer.customize(builder)); + } + + @Bean + @ConditionalOnMissingBean(name = "pulsarReaderContainerFactory") + DefaultPulsarReaderContainerFactory pulsarReaderContainerFactory(PulsarReaderFactory pulsarReaderFactory, + SchemaResolver schemaResolver) { + PulsarReaderContainerProperties readerContainerProperties = new PulsarReaderContainerProperties(); + readerContainerProperties.setSchemaResolver(schemaResolver); + this.propertiesMapper.customizeReaderContainerProperties(readerContainerProperties); + return new DefaultPulsarReaderContainerFactory<>(pulsarReaderFactory, readerContainerProperties); + } + + @Configuration(proxyBeanMethods = false) + @EnablePulsar + @ConditionalOnMissingBean(name = { PulsarAnnotationSupportBeanNames.PULSAR_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME, + PulsarAnnotationSupportBeanNames.PULSAR_READER_ANNOTATION_PROCESSOR_BEAN_NAME }) + static class EnablePulsarConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfiguration.java new file mode 100644 index 000000000000..14551bed70d3 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfiguration.java @@ -0,0 +1,173 @@ +/* + * Copyright 2012-2023 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.pulsar; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.pulsar.client.admin.PulsarAdminBuilder; +import org.apache.pulsar.client.api.ClientBuilder; +import org.apache.pulsar.client.api.PulsarClient; +import org.apache.pulsar.client.api.PulsarClientException; +import org.apache.pulsar.client.api.Schema; +import org.apache.pulsar.common.schema.SchemaType; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.pulsar.PulsarProperties.Defaults.SchemaInfo; +import org.springframework.boot.autoconfigure.pulsar.PulsarProperties.Defaults.TypeMapping; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.util.LambdaSafe; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.pulsar.core.DefaultPulsarClientFactory; +import org.springframework.pulsar.core.DefaultSchemaResolver; +import org.springframework.pulsar.core.DefaultTopicResolver; +import org.springframework.pulsar.core.PulsarAdminBuilderCustomizer; +import org.springframework.pulsar.core.PulsarAdministration; +import org.springframework.pulsar.core.PulsarClientBuilderCustomizer; +import org.springframework.pulsar.core.PulsarClientFactory; +import org.springframework.pulsar.core.SchemaResolver; +import org.springframework.pulsar.core.SchemaResolver.SchemaResolverCustomizer; +import org.springframework.pulsar.core.TopicResolver; +import org.springframework.pulsar.function.PulsarFunction; +import org.springframework.pulsar.function.PulsarFunctionAdministration; +import org.springframework.pulsar.function.PulsarSink; +import org.springframework.pulsar.function.PulsarSource; + +/** + * Common configuration used by both {@link PulsarAutoConfiguration} and + * {@link PulsarReactiveAutoConfiguration}. A separate configuration class is used so that + * {@link PulsarAutoConfiguration} can be excluded for reactive only application. + * + * @author Chris Bono + * @author Phillip Webb + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(PulsarProperties.class) +class PulsarConfiguration { + + private final PulsarProperties properties; + + private final PulsarPropertiesMapper propertiesMapper; + + PulsarConfiguration(PulsarProperties properties) { + this.properties = properties; + this.propertiesMapper = new PulsarPropertiesMapper(properties); + } + + @Bean + @ConditionalOnMissingBean(PulsarClientFactory.class) + DefaultPulsarClientFactory pulsarClientFactory(ObjectProvider customizersProvider) { + List allCustomizers = new ArrayList<>(); + allCustomizers.add(this.propertiesMapper::customizeClientBuilder); + allCustomizers.addAll(customizersProvider.orderedStream().toList()); + DefaultPulsarClientFactory clientFactory = new DefaultPulsarClientFactory( + (clientBuilder) -> applyClientBuilderCustomizers(allCustomizers, clientBuilder)); + return clientFactory; + } + + private void applyClientBuilderCustomizers(List customizers, + ClientBuilder clientBuilder) { + customizers.forEach((customizer) -> customizer.customize(clientBuilder)); + } + + @Bean + @ConditionalOnMissingBean + PulsarClient pulsarClient(PulsarClientFactory clientFactory) throws PulsarClientException { + return clientFactory.createClient(); + } + + @Bean + @ConditionalOnMissingBean + PulsarAdministration pulsarAdministration( + ObjectProvider pulsarAdminBuilderCustomizers) { + List allCustomizers = new ArrayList<>(); + allCustomizers.add(this.propertiesMapper::customizeAdminBuilder); + allCustomizers.addAll(pulsarAdminBuilderCustomizers.orderedStream().toList()); + return new PulsarAdministration((adminBuilder) -> applyAdminBuilderCustomizers(allCustomizers, adminBuilder)); + } + + private void applyAdminBuilderCustomizers(List customizers, + PulsarAdminBuilder adminBuilder) { + customizers.forEach((customizer) -> customizer.customize(adminBuilder)); + } + + @Bean + @ConditionalOnMissingBean(SchemaResolver.class) + DefaultSchemaResolver pulsarSchemaResolver(ObjectProvider> schemaResolverCustomizers) { + DefaultSchemaResolver schemaResolver = new DefaultSchemaResolver(); + addCustomSchemaMappings(schemaResolver, this.properties.getDefaults().getTypeMappings()); + applySchemaResolverCustomizers(schemaResolverCustomizers.orderedStream().toList(), schemaResolver); + return schemaResolver; + } + + private void addCustomSchemaMappings(DefaultSchemaResolver schemaResolver, List typeMappings) { + if (typeMappings != null) { + typeMappings.forEach((typeMapping) -> addCustomSchemaMapping(schemaResolver, typeMapping)); + } + } + + private void addCustomSchemaMapping(DefaultSchemaResolver schemaResolver, TypeMapping typeMapping) { + SchemaInfo schemaInfo = typeMapping.schemaInfo(); + if (schemaInfo != null) { + Class messageType = typeMapping.messageType(); + SchemaType schemaType = schemaInfo.schemaType(); + Class messageKeyType = schemaInfo.messageKeyType(); + Schema schema = schemaResolver.resolveSchema(schemaType, messageType, messageKeyType).orElseThrow(); + schemaResolver.addCustomSchemaMapping(typeMapping.messageType(), schema); + } + } + + @SuppressWarnings("unchecked") + private void applySchemaResolverCustomizers(List> customizers, + DefaultSchemaResolver schemaResolver) { + LambdaSafe.callbacks(SchemaResolverCustomizer.class, customizers, schemaResolver) + .invoke((customizer) -> customizer.customize(schemaResolver)); + } + + @Bean + @ConditionalOnMissingBean(TopicResolver.class) + DefaultTopicResolver pulsarTopicResolver() { + DefaultTopicResolver topicResolver = new DefaultTopicResolver(); + List typeMappings = this.properties.getDefaults().getTypeMappings(); + if (typeMappings != null) { + typeMappings.forEach((typeMapping) -> addCustomTopicMapping(topicResolver, typeMapping)); + } + return topicResolver; + } + + private void addCustomTopicMapping(DefaultTopicResolver topicResolver, TypeMapping typeMapping) { + String topicName = typeMapping.topicName(); + if (topicName != null) { + topicResolver.addCustomTopicMapping(typeMapping.messageType(), topicName); + } + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(name = "spring.pulsar.function.enabled", havingValue = "true", matchIfMissing = true) + PulsarFunctionAdministration pulsarFunctionAdministration(PulsarAdministration pulsarAdministration, + ObjectProvider pulsarFunctions, ObjectProvider pulsarSinks, + ObjectProvider pulsarSources) { + PulsarProperties.Function properties = this.properties.getFunction(); + return new PulsarFunctionAdministration(pulsarAdministration, pulsarFunctions, pulsarSinks, pulsarSources, + properties.isFailFast(), properties.isPropagateFailures(), properties.isPropagateStopFailures()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java new file mode 100644 index 000000000000..3ed73df57f62 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java @@ -0,0 +1,890 @@ +/* + * Copyright 2012-2023 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.pulsar; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import org.apache.pulsar.client.api.CompressionType; +import org.apache.pulsar.client.api.HashingScheme; +import org.apache.pulsar.client.api.MessageRoutingMode; +import org.apache.pulsar.client.api.ProducerAccessMode; +import org.apache.pulsar.client.api.RegexSubscriptionMode; +import org.apache.pulsar.client.api.SubscriptionInitialPosition; +import org.apache.pulsar.client.api.SubscriptionMode; +import org.apache.pulsar.client.api.SubscriptionType; +import org.apache.pulsar.common.schema.SchemaType; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.util.Assert; + +/** + * Configuration properties Apache Pulsar. + * + * @author Chris Bono + * @author Phillip Webb + * @since 3.2.0 + */ +@ConfigurationProperties("spring.pulsar") +public class PulsarProperties { + + private final Client client = new Client(); + + private final Admin admin = new Admin(); + + private final Defaults defaults = new Defaults(); + + private final Function function = new Function(); + + private final Producer producer = new Producer(); + + private final Consumer consumer = new Consumer(); + + private final Listener listener = new Listener(); + + private final Reader reader = new Reader(); + + private final Template template = new Template(); + + public Client getClient() { + return this.client; + } + + public Admin getAdmin() { + return this.admin; + } + + public Defaults getDefaults() { + return this.defaults; + } + + public Producer getProducer() { + return this.producer; + } + + public Consumer getConsumer() { + return this.consumer; + } + + public Listener getListener() { + return this.listener; + } + + public Reader getReader() { + return this.reader; + } + + public Function getFunction() { + return this.function; + } + + public Template getTemplate() { + return this.template; + } + + public static class Client { + + /** + * Pulsar service URL in the format '(pulsar|pulsar+ssl)://host:port'. + */ + private String serviceUrl = "pulsar://localhost:6650"; + + /** + * Client operation timeout. + */ + private Duration operationTimeout = Duration.ofSeconds(30); + + /** + * Client lookup timeout. + */ + private Duration lookupTimeout = Duration.ofMillis(-1); // FIXME + + /** + * Duration to wait for a connection to a broker to be established. + */ + private Duration connectionTimeout = Duration.ofSeconds(10); + + /** + * Authentication settings. + */ + private final Authentication authentication = new Authentication(); + + public String getServiceUrl() { + return this.serviceUrl; + } + + public void setServiceUrl(String serviceUrl) { + this.serviceUrl = serviceUrl; + } + + public Duration getOperationTimeout() { + return this.operationTimeout; + } + + public void setOperationTimeout(Duration operationTimeout) { + this.operationTimeout = operationTimeout; + } + + public Duration getLookupTimeout() { + return this.lookupTimeout; + } + + public void setLookupTimeout(Duration lookupTimeout) { + this.lookupTimeout = lookupTimeout; + } + + public Duration getConnectionTimeout() { + return this.connectionTimeout; + } + + public void setConnectionTimeout(Duration connectionTimeout) { + this.connectionTimeout = connectionTimeout; + } + + public Authentication getAuthentication() { + return this.authentication; + } + + } + + public static class Admin { + + /** + * Pulsar web URL for the admin endpoint in the format '(http|https)://host:port'. + */ + private String serviceUrl = "http://localhost:8080"; + + /** + * Duration to wait for a connection to server to be established. + */ + private Duration connectionTimeout = Duration.ofMinutes(1); + + /** + * Server response read time out for any request. + */ + private Duration readTimeout = Duration.ofMinutes(1); + + /** + * Server request time out for any request. + */ + private Duration requestTimeout = Duration.ofMinutes(5); + + /** + * Authentication settings. + */ + private final Authentication authentication = new Authentication(); + + public String getServiceUrl() { + return this.serviceUrl; + } + + public void setServiceUrl(String serviceUrl) { + this.serviceUrl = serviceUrl; + } + + public Duration getConnectionTimeout() { + return this.connectionTimeout; + } + + public void setConnectionTimeout(Duration connectionTimeout) { + this.connectionTimeout = connectionTimeout; + } + + public Duration getReadTimeout() { + return this.readTimeout; + } + + public void setReadTimeout(Duration readTimeout) { + this.readTimeout = readTimeout; + } + + public Duration getRequestTimeout() { + return this.requestTimeout; + } + + public void setRequestTimeout(Duration requestTimeout) { + this.requestTimeout = requestTimeout; + } + + public Authentication getAuthentication() { + return this.authentication; + } + + } + + public static class Defaults { + + /** + * List of mappings from message type to topic name and schema info to use as a + * defaults when a topic name and/or schema is not explicitly specified when + * producing or consuming messages of the mapped type. + */ + private List typeMappings = new ArrayList<>(); + + public List getTypeMappings() { + return this.typeMappings; + } + + public void setTypeMappings(List typeMappings) { + this.typeMappings = typeMappings; + } + + /** + * A mapping from message type to topic and/or schema info to use (at least one of + * {@code topicName} or {@code schemaInfo} must be specified. + * + * @param messageType the message type + * @param topicName the topic name + * @param schemaInfo the schema info + */ + public record TypeMapping(Class messageType, String topicName, SchemaInfo schemaInfo) { + + public TypeMapping { + Assert.notNull(messageType, "messageType must not be null"); + Assert.isTrue(topicName != null || schemaInfo != null, + "At least one of topicName or schemaInfo must not be null"); + } + + } + + /** + * Represents a schema - holds enough information to construct an actual schema + * instance. + * + * @param schemaType schema type + * @param messageKeyType message key type (required for key value type) + */ + public record SchemaInfo(SchemaType schemaType, Class messageKeyType) { + + public SchemaInfo { + Assert.notNull(schemaType, "schemaType must not be null"); + Assert.isTrue(schemaType != SchemaType.NONE, "schemaType 'NONE' not supported"); + Assert.isTrue(messageKeyType == null || schemaType == SchemaType.KEY_VALUE, + "messageKeyType can only be set when schemaType is KEY_VALUE"); + } + + } + + } + + public static class Function { + + /** + * Whether to stop processing further function creates/updates when a failure + * occurs. + */ + private boolean failFast = true; + + /** + * Whether to throw an exception if any failure is encountered during server + * startup while creating/updating functions. + */ + private boolean propagateFailures = true; + + /** + * Whether to throw an exception if any failure is encountered during server + * shutdown while enforcing stop policy on functions. + */ + private boolean propagateStopFailures = false; + + public boolean isFailFast() { + return this.failFast; + } + + public void setFailFast(boolean failFast) { + this.failFast = failFast; + } + + public boolean isPropagateFailures() { + return this.propagateFailures; + } + + public void setPropagateFailures(boolean propagateFailures) { + this.propagateFailures = propagateFailures; + } + + public boolean isPropagateStopFailures() { + return this.propagateStopFailures; + } + + public void setPropagateStopFailures(boolean propagateStopFailures) { + this.propagateStopFailures = propagateStopFailures; + } + + } + + public static class Producer { + + /** + * Name for the producer. If not assigned, a unique name is generated. + */ + private String name; + + /** + * Topic the producer will publish to. + */ + private String topicName; + + /** + * Time before a message has to be acknowledged by the broker. + */ + private Duration sendTimeout = Duration.ofSeconds(30); + + /** + * Message routing mode for a partitioned producer. + */ + private MessageRoutingMode messageRoutingMode = MessageRoutingMode.RoundRobinPartition; + + /** + * Message hashing scheme to choose the partition to which the message is + * published. + */ + private HashingScheme hashingScheme = HashingScheme.JavaStringHash; + + /** + * Whether to automatically batch messages. + */ + private boolean batchingEnabled = true; + + /** + * Whether to split large-size messages into multiple chunks. + */ + private boolean chunkingEnabled; + + /** + * Message compression type. + */ + private CompressionType compressionType; + + /** + * Type of access to the topic the producer requires. + */ + private ProducerAccessMode accessMode = ProducerAccessMode.Shared; + + private final Cache cache = new Cache(); + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getTopicName() { + return this.topicName; + } + + public void setTopicName(String topicName) { + this.topicName = topicName; + } + + public Duration getSendTimeout() { + return this.sendTimeout; + } + + public void setSendTimeout(Duration sendTimeout) { + this.sendTimeout = sendTimeout; + } + + public MessageRoutingMode getMessageRoutingMode() { + return this.messageRoutingMode; + } + + public void setMessageRoutingMode(MessageRoutingMode messageRoutingMode) { + this.messageRoutingMode = messageRoutingMode; + } + + public HashingScheme getHashingScheme() { + return this.hashingScheme; + } + + public void setHashingScheme(HashingScheme hashingScheme) { + this.hashingScheme = hashingScheme; + } + + public boolean isBatchingEnabled() { + return this.batchingEnabled; + } + + public void setBatchingEnabled(boolean batchingEnabled) { + this.batchingEnabled = batchingEnabled; + } + + public boolean isChunkingEnabled() { + return this.chunkingEnabled; + } + + public void setChunkingEnabled(boolean chunkingEnabled) { + this.chunkingEnabled = chunkingEnabled; + } + + public CompressionType getCompressionType() { + return this.compressionType; + } + + public void setCompressionType(CompressionType compressionType) { + this.compressionType = compressionType; + } + + public ProducerAccessMode getAccessMode() { + return this.accessMode; + } + + public void setAccessMode(ProducerAccessMode accessMode) { + this.accessMode = accessMode; + } + + public Cache getCache() { + return this.cache; + } + + public static class Cache { + + /** + * Time period to expire unused entries in the cache. + */ + private Duration expireAfterAccess = Duration.ofMinutes(1); + + /** + * Maximum size of cache (entries). + */ + private long maximumSize = 1000L; + + /** + * Initial size of cache. + */ + private int initialCapacity = 50; + + public Duration getExpireAfterAccess() { + return this.expireAfterAccess; + } + + public void setExpireAfterAccess(Duration expireAfterAccess) { + this.expireAfterAccess = expireAfterAccess; + } + + public long getMaximumSize() { + return this.maximumSize; + } + + public void setMaximumSize(long maximumSize) { + this.maximumSize = maximumSize; + } + + public int getInitialCapacity() { + return this.initialCapacity; + } + + public void setInitialCapacity(int initialCapacity) { + this.initialCapacity = initialCapacity; + } + + } + + } + + public static class Consumer { + + /** + * Consumer name to identify a particular consumer from the topic stats. + */ + private String name; + + /** + * Topics the consumer subscribes to. + */ + private List topics; + + /** + * Pattern for topics the consumer subscribes to. + */ + private Pattern topicsPattern; + + /** + * Priority level for shared subscription consumers. + */ + private int priorityLevel = 0; + + /** + * Whether to read messages from the compacted topic rather than the full message + * backlog. + */ + private boolean readCompacted = false; + + /** + * Dead letter policy to use. + */ + @NestedConfigurationProperty + private DeadLetterPolicy deadLetterPolicy; + + /** + * Consumer subscription properties. + */ + private final Subscription subscription = new Subscription(); + + /** + * Whether to auto retry messages. + */ + private boolean retryEnable = false; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public Consumer.Subscription getSubscription() { + return this.subscription; + } + + public List getTopics() { + return this.topics; + } + + public void setTopics(List topics) { + this.topics = topics; + } + + public Pattern getTopicsPattern() { + return this.topicsPattern; + } + + public void setTopicsPattern(Pattern topicsPattern) { + this.topicsPattern = topicsPattern; + } + + public int getPriorityLevel() { + return this.priorityLevel; + } + + public void setPriorityLevel(int priorityLevel) { + this.priorityLevel = priorityLevel; + } + + public boolean isReadCompacted() { + return this.readCompacted; + } + + public void setReadCompacted(boolean readCompacted) { + this.readCompacted = readCompacted; + } + + public DeadLetterPolicy getDeadLetterPolicy() { + return this.deadLetterPolicy; + } + + public void setDeadLetterPolicy(DeadLetterPolicy deadLetterPolicy) { + this.deadLetterPolicy = deadLetterPolicy; + } + + public boolean isRetryEnable() { + return this.retryEnable; + } + + public void setRetryEnable(boolean retryEnable) { + this.retryEnable = retryEnable; + } + + public static class Subscription { + + /** + * Subscription name for the consumer. + */ + private String name; + + /** + * Position where to initialize a newly created subscription. + */ + private SubscriptionInitialPosition initialPosition = SubscriptionInitialPosition.Latest; + + /** + * Subscription mode to be used when subscribing to the topic. + */ + private SubscriptionMode mode = SubscriptionMode.Durable; + + /** + * Determines which type of topics (persistent, non-persistent, or all) the + * consumer should be subscribed to when using pattern subscriptions. + */ + private RegexSubscriptionMode topicsMode = RegexSubscriptionMode.PersistentOnly; + + /** + * Subscription type to be used when subscribing to a topic. + */ + private SubscriptionType type = SubscriptionType.Exclusive; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public SubscriptionInitialPosition getInitialPosition() { + return this.initialPosition; + } + + public void setInitialPosition(SubscriptionInitialPosition initialPosition) { + this.initialPosition = initialPosition; + } + + public SubscriptionMode getMode() { + return this.mode; + } + + public void setMode(SubscriptionMode mode) { + this.mode = mode; + } + + public RegexSubscriptionMode getTopicsMode() { + return this.topicsMode; + } + + public void setTopicsMode(RegexSubscriptionMode topicsMode) { + this.topicsMode = topicsMode; + } + + public SubscriptionType getType() { + return this.type; + } + + public void setType(SubscriptionType type) { + this.type = type; + } + + } + + public static class DeadLetterPolicy { + + /** + * Maximum number of times that a message will be redelivered before being + * sent to the dead letter queue. + */ + private int maxRedeliverCount; + + /** + * Name of the retry topic where the failing messages will be sent. + */ + private String retryLetterTopic; + + /** + * Name of the dead topic where the failing messages will be sent. + */ + private String deadLetterTopic; + + /** + * Name of the initial subscription of the dead letter topic. When not set, + * the initial subscription will not be created. However, when the property is + * set then the broker's 'allowAutoSubscriptionCreation' must be enabled or + * the DLQ producer will fail. + */ + private String initialSubscriptionName; + + public int getMaxRedeliverCount() { + return this.maxRedeliverCount; + } + + public void setMaxRedeliverCount(int maxRedeliverCount) { + this.maxRedeliverCount = maxRedeliverCount; + } + + public String getRetryLetterTopic() { + return this.retryLetterTopic; + } + + public void setRetryLetterTopic(String retryLetterTopic) { + this.retryLetterTopic = retryLetterTopic; + } + + public String getDeadLetterTopic() { + return this.deadLetterTopic; + } + + public void setDeadLetterTopic(String deadLetterTopic) { + this.deadLetterTopic = deadLetterTopic; + } + + public String getInitialSubscriptionName() { + return this.initialSubscriptionName; + } + + public void setInitialSubscriptionName(String initialSubscriptionName) { + this.initialSubscriptionName = initialSubscriptionName; + } + + } + + } + + public static class Listener { + + /** + * SchemaType of the consumed messages. + */ + private SchemaType schemaType; + + /** + * Whether to record observations for when the Observations API is available and + * the client supports it. + */ + private boolean observationEnabled = true; + + public SchemaType getSchemaType() { + return this.schemaType; + } + + public void setSchemaType(SchemaType schemaType) { + this.schemaType = schemaType; + } + + public boolean isObservationEnabled() { + return this.observationEnabled; + } + + public void setObservationEnabled(boolean observationEnabled) { + this.observationEnabled = observationEnabled; + } + + } + + public static class Reader { + + /** + * Reader name. + */ + private String name; + + /** + * Topis the reader subscribes to. + */ + private List topics; + + /** + * Subscription name. + */ + private String subscriptionName; + + /** + * Prefix of subscription role. + */ + private String subscriptionRolePrefix; + + /** + * Whether to read messages from a compacted topic rather than a full message + * backlog of a topic. + */ + private boolean readCompacted; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public List getTopics() { + return this.topics; + } + + public void setTopics(List topics) { + this.topics = topics; + } + + public String getSubscriptionName() { + return this.subscriptionName; + } + + public void setSubscriptionName(String subscriptionName) { + this.subscriptionName = subscriptionName; + } + + public String getSubscriptionRolePrefix() { + return this.subscriptionRolePrefix; + } + + public void setSubscriptionRolePrefix(String subscriptionRolePrefix) { + this.subscriptionRolePrefix = subscriptionRolePrefix; + } + + public boolean isReadCompacted() { + return this.readCompacted; + } + + public void setReadCompacted(boolean readCompacted) { + this.readCompacted = readCompacted; + } + + } + + public static class Template { + + /** + * Whether to record observations for when the Observations API is available. + */ + private boolean observationsEnabled = true; + + public boolean isObservationsEnabled() { + return this.observationsEnabled; + } + + public void setObservationsEnabled(boolean observationsEnabled) { + this.observationsEnabled = observationsEnabled; + } + + } + + public static class Authentication { + + /** + * Fully qualified class name of the authentication plugin. + */ + private String pluginClassName; + + /** + * Authentication parameter(s) as a map of parameter names to parameter values. + */ + private Map param = new LinkedHashMap<>(); + + public String getPluginClassName() { + return this.pluginClassName; + } + + public void setPluginClassName(String pluginClassName) { + this.pluginClassName = pluginClassName; + } + + public Map getParam() { + return this.param; + } + + public void setParam(Map param) { + this.param = param; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java new file mode 100644 index 000000000000..10c3a77597bb --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java @@ -0,0 +1,166 @@ +/* + * Copyright 2012-2023 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.pulsar; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import org.apache.pulsar.client.admin.PulsarAdminBuilder; +import org.apache.pulsar.client.api.ClientBuilder; +import org.apache.pulsar.client.api.ConsumerBuilder; +import org.apache.pulsar.client.api.ProducerBuilder; +import org.apache.pulsar.client.api.PulsarClientException.UnsupportedAuthenticationException; +import org.apache.pulsar.client.api.ReaderBuilder; + +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.pulsar.listener.PulsarContainerProperties; +import org.springframework.pulsar.reader.PulsarReaderContainerProperties; +import org.springframework.util.StringUtils; + +/** + * Helper class used to map {@link PulsarProperties} to various builder customizers. + * + * @author Chris Bono + * @author Phillip Webb + */ +final class PulsarPropertiesMapper { + + private final PulsarProperties properties; + + PulsarPropertiesMapper(PulsarProperties properties) { + this.properties = properties; + } + + void customizeClientBuilder(ClientBuilder clientBuilder) { + PulsarProperties.Client properties = this.properties.getClient(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getServiceUrl).to(clientBuilder::serviceUrl); + map.from(properties::getConnectionTimeout).to(timeoutProperty(clientBuilder::connectionTimeout)); + map.from(properties::getOperationTimeout).to(timeoutProperty(clientBuilder::operationTimeout)); + map.from(properties::getLookupTimeout).to(timeoutProperty(clientBuilder::lookupTimeout)); + customizeAuthentication(clientBuilder::authentication, properties.getAuthentication()); + } + + void customizeAdminBuilder(PulsarAdminBuilder adminBuilder) { + PulsarProperties.Admin properties = this.properties.getAdmin(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getServiceUrl).to(adminBuilder::serviceHttpUrl); + map.from(properties::getConnectionTimeout).to(timeoutProperty(adminBuilder::connectionTimeout)); + map.from(properties::getReadTimeout).to(timeoutProperty(adminBuilder::readTimeout)); + map.from(properties::getRequestTimeout).to(timeoutProperty(adminBuilder::requestTimeout)); + customizeAuthentication(adminBuilder::authentication, properties.getAuthentication()); + } + + private void customizeAuthentication(AuthenticationConsumer authentication, + PulsarProperties.Authentication properties) { + if (StringUtils.hasText(properties.getPluginClassName())) { + try { + authentication.accept(properties.getPluginClassName(), properties.getParam()); + } + catch (UnsupportedAuthenticationException ex) { + throw new IllegalStateException("Unable to configure Pulsar authentication", ex); + } + } + } + + void customizeProducerBuilder(ProducerBuilder producerBuilder) { + PulsarProperties.Producer properties = this.properties.getProducer(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getName).to(producerBuilder::producerName); + map.from(properties::getTopicName).to(producerBuilder::topic); + map.from(properties::getSendTimeout).to(timeoutProperty(producerBuilder::sendTimeout)); + map.from(properties::getMessageRoutingMode).to(producerBuilder::messageRoutingMode); + map.from(properties::getHashingScheme).to(producerBuilder::hashingScheme); + map.from(properties::isBatchingEnabled).to(producerBuilder::enableBatching); + map.from(properties::isChunkingEnabled).to(producerBuilder::enableChunking); + map.from(properties::getCompressionType).to(producerBuilder::compressionType); + map.from(properties::getAccessMode).to(producerBuilder::accessMode); + } + + void customizeConsumerBuilder(ConsumerBuilder consumerBuilder) { + PulsarProperties.Consumer properties = this.properties.getConsumer(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getName).to(consumerBuilder::consumerName); + map.from(properties::getTopics).as(ArrayList::new).to(consumerBuilder::topics); + map.from(properties::getTopicsPattern).to(consumerBuilder::topicsPattern); + map.from(properties::getPriorityLevel).to(consumerBuilder::priorityLevel); + map.from(properties::isReadCompacted).to(consumerBuilder::readCompacted); + map.from(properties::getDeadLetterPolicy).as(DeadLetterPolicyMapper::map).to(consumerBuilder::deadLetterPolicy); + map.from(properties::isRetryEnable).to(consumerBuilder::enableRetry); + customizeConsumerBuilderSubscription(consumerBuilder); + } + + private void customizeConsumerBuilderSubscription(ConsumerBuilder consumerBuilder) { + PulsarProperties.Consumer.Subscription properties = this.properties.getConsumer().getSubscription(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getName).to(consumerBuilder::subscriptionName); + map.from(properties::getInitialPosition).to(consumerBuilder::subscriptionInitialPosition); + map.from(properties::getMode).to(consumerBuilder::subscriptionMode); + map.from(properties::getTopicsMode).to(consumerBuilder::subscriptionTopicsMode); + map.from(properties::getType).to(consumerBuilder::subscriptionType); + } + + void customizeContainerProperties(PulsarContainerProperties containerProperties) { + customizePulsarContainerConsumerSubscriptionProperties(containerProperties); + customizePulsarContainerListenerProperties(containerProperties); + } + + private void customizePulsarContainerConsumerSubscriptionProperties(PulsarContainerProperties containerProperties) { + PulsarProperties.Consumer.Subscription properties = this.properties.getConsumer().getSubscription(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getType).to(containerProperties::setSubscriptionType); + } + + private void customizePulsarContainerListenerProperties(PulsarContainerProperties containerProperties) { + PulsarProperties.Listener properties = this.properties.getListener(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getSchemaType).to(containerProperties::setSchemaType); + map.from(properties::isObservationEnabled).to(containerProperties::setObservationEnabled); + } + + void customizeReaderBuilder(ReaderBuilder readerBuilder) { + PulsarProperties.Reader properties = this.properties.getReader(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getName).to(readerBuilder::readerName); + map.from(properties::getTopics).to(readerBuilder::topics); + map.from(properties::getSubscriptionName).to(readerBuilder::subscriptionName); + map.from(properties::getSubscriptionRolePrefix).to(readerBuilder::subscriptionRolePrefix); + map.from(properties::isReadCompacted).to(readerBuilder::readCompacted); + } + + void customizeReaderContainerProperties(PulsarReaderContainerProperties readerContainerProperties) { + PulsarProperties.Reader properties = this.properties.getReader(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getTopics).to(readerContainerProperties::setTopics); + } + + private Consumer timeoutProperty(BiConsumer setter) { + return (duration) -> setter.accept((int) duration.toMillis(), TimeUnit.MILLISECONDS); + } + + private interface AuthenticationConsumer { + + void accept(String authPluginClassName, Map authParams) + throws UnsupportedAuthenticationException; + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarReactiveAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarReactiveAutoConfiguration.java new file mode 100644 index 000000000000..4c2aeb172d52 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarReactiveAutoConfiguration.java @@ -0,0 +1,201 @@ +/* + * Copyright 2012-2023 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.pulsar; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import org.apache.pulsar.client.api.PulsarClient; +import org.apache.pulsar.reactive.client.adapter.AdaptedReactivePulsarClientFactory; +import org.apache.pulsar.reactive.client.adapter.ProducerCacheProvider; +import org.apache.pulsar.reactive.client.api.ReactiveMessageConsumerBuilder; +import org.apache.pulsar.reactive.client.api.ReactiveMessageReaderBuilder; +import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderBuilder; +import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderCache; +import org.apache.pulsar.reactive.client.api.ReactivePulsarClient; +import org.apache.pulsar.reactive.client.producercache.CaffeineShadedProducerCacheProvider; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.util.LambdaSafe; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.pulsar.config.PulsarAnnotationSupportBeanNames; +import org.springframework.pulsar.core.SchemaResolver; +import org.springframework.pulsar.core.TopicResolver; +import org.springframework.pulsar.reactive.config.DefaultReactivePulsarListenerContainerFactory; +import org.springframework.pulsar.reactive.config.annotation.EnableReactivePulsar; +import org.springframework.pulsar.reactive.core.DefaultReactivePulsarConsumerFactory; +import org.springframework.pulsar.reactive.core.DefaultReactivePulsarReaderFactory; +import org.springframework.pulsar.reactive.core.DefaultReactivePulsarSenderFactory; +import org.springframework.pulsar.reactive.core.ReactiveMessageConsumerBuilderCustomizer; +import org.springframework.pulsar.reactive.core.ReactiveMessageReaderBuilderCustomizer; +import org.springframework.pulsar.reactive.core.ReactiveMessageSenderBuilderCustomizer; +import org.springframework.pulsar.reactive.core.ReactivePulsarConsumerFactory; +import org.springframework.pulsar.reactive.core.ReactivePulsarReaderFactory; +import org.springframework.pulsar.reactive.core.ReactivePulsarSenderFactory; +import org.springframework.pulsar.reactive.core.ReactivePulsarTemplate; +import org.springframework.pulsar.reactive.listener.ReactivePulsarContainerProperties; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Spring for Apache Pulsar + * Reactive. + * + * @author Chris Bono + * @author Christophe Bornet + * @since 3.2.0 + */ +@AutoConfiguration(after = PulsarAutoConfiguration.class) +@ConditionalOnClass({ PulsarClient.class, ReactivePulsarClient.class, ReactivePulsarTemplate.class }) +@Import(PulsarConfiguration.class) +public class PulsarReactiveAutoConfiguration { + + private final PulsarProperties properties; + + private final PulsarReactivePropertiesMapper propertiesMapper; + + PulsarReactiveAutoConfiguration(PulsarProperties properties) { + this.properties = properties; + this.propertiesMapper = new PulsarReactivePropertiesMapper(properties); + } + + @Bean + @ConditionalOnMissingBean + ReactivePulsarClient reactivePulsarClient(PulsarClient pulsarClient) { + return AdaptedReactivePulsarClientFactory.create(pulsarClient); + } + + @Bean + @ConditionalOnMissingBean(ProducerCacheProvider.class) + @ConditionalOnClass(CaffeineShadedProducerCacheProvider.class) + @ConditionalOnProperty(name = "spring.pulsar.producer.cache.enabled", havingValue = "true", matchIfMissing = true) + CaffeineShadedProducerCacheProvider reactivePulsarProducerCacheProvider() { + PulsarProperties.Producer.Cache properties = this.properties.getProducer().getCache(); + return new CaffeineShadedProducerCacheProvider(properties.getExpireAfterAccess(), Duration.ofMinutes(10), + properties.getMaximumSize(), properties.getInitialCapacity()); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(name = "spring.pulsar.producer.cache.enabled", havingValue = "true", matchIfMissing = true) + ReactiveMessageSenderCache reactivePulsarMessageSenderCache( + ObjectProvider producerCacheProvider) { + return reactivePulsarMessageSenderCache(producerCacheProvider.getIfAvailable()); + } + + private ReactiveMessageSenderCache reactivePulsarMessageSenderCache(ProducerCacheProvider producerCacheProvider) { + return (producerCacheProvider != null) ? AdaptedReactivePulsarClientFactory.createCache(producerCacheProvider) + : AdaptedReactivePulsarClientFactory.createCache(); + } + + @Bean + @ConditionalOnMissingBean(ReactivePulsarSenderFactory.class) + DefaultReactivePulsarSenderFactory reactivePulsarSenderFactory(ReactivePulsarClient reactivePulsarClient, + ObjectProvider reactiveMessageSenderCache, TopicResolver topicResolver, + ObjectProvider> customizersProvider) { + List> customizers = new ArrayList<>(); + customizers.add(this.propertiesMapper::customizeMessageSenderBuilder); + customizers.addAll(customizersProvider.orderedStream().toList()); + List> lambdaSafeCustomizers = List + .of((builder) -> applyMessageSenderBuilderCustomizers(customizers, builder)); + return DefaultReactivePulsarSenderFactory.builderFor(reactivePulsarClient) + .withDefaultConfigCustomizers(lambdaSafeCustomizers) + .withMessageSenderCache(reactiveMessageSenderCache.getIfAvailable()) + .withTopicResolver(topicResolver) + .build(); + } + + @SuppressWarnings("unchecked") + private void applyMessageSenderBuilderCustomizers(List> customizers, + ReactiveMessageSenderBuilder builder) { + LambdaSafe.callbacks(ReactiveMessageSenderBuilderCustomizer.class, customizers, builder) + .invoke((customizer) -> customizer.customize(builder)); + } + + @Bean + @ConditionalOnMissingBean(ReactivePulsarConsumerFactory.class) + DefaultReactivePulsarConsumerFactory reactivePulsarConsumerFactory( + ReactivePulsarClient pulsarReactivePulsarClient, + ObjectProvider> customizersProvider) { + List> customizers = new ArrayList<>(); + customizers.add(this.propertiesMapper::customizeMessageConsumerBuilder); + customizers.addAll(customizersProvider.orderedStream().toList()); + List> lambdaSafeCustomizers = List + .of((builder) -> applyMessageConsumerBuilderCustomizers(customizers, builder)); + return new DefaultReactivePulsarConsumerFactory<>(pulsarReactivePulsarClient, lambdaSafeCustomizers); + } + + @SuppressWarnings("unchecked") + private void applyMessageConsumerBuilderCustomizers(List> customizers, + ReactiveMessageConsumerBuilder builder) { + LambdaSafe.callbacks(ReactiveMessageConsumerBuilderCustomizer.class, customizers, builder) + .invoke((customizer) -> customizer.customize(builder)); + } + + @Bean + @ConditionalOnMissingBean(name = "reactivePulsarListenerContainerFactory") + DefaultReactivePulsarListenerContainerFactory reactivePulsarListenerContainerFactory( + ReactivePulsarConsumerFactory reactivePulsarConsumerFactory, SchemaResolver schemaResolver, + TopicResolver topicResolver) { + ReactivePulsarContainerProperties containerProperties = new ReactivePulsarContainerProperties<>(); + containerProperties.setSchemaResolver(schemaResolver); + containerProperties.setTopicResolver(topicResolver); + this.propertiesMapper.customizeContainerProperties(containerProperties); + return new DefaultReactivePulsarListenerContainerFactory<>(reactivePulsarConsumerFactory, containerProperties); + } + + @Bean + @ConditionalOnMissingBean(ReactivePulsarReaderFactory.class) + DefaultReactivePulsarReaderFactory reactivePulsarReaderFactory(ReactivePulsarClient reactivePulsarClient, + ObjectProvider> customizersProvider) { + List> customizers = new ArrayList<>(); + customizers.add(this.propertiesMapper::customizeMessageReaderBuilder); + customizers.addAll(customizersProvider.orderedStream().toList()); + List> lambdaSafeCustomizers = List + .of((builder) -> applyMessageReaderBuilderCustomizers(customizers, builder)); + return new DefaultReactivePulsarReaderFactory<>(reactivePulsarClient, lambdaSafeCustomizers); + } + + @SuppressWarnings("unchecked") + private void applyMessageReaderBuilderCustomizers(List> customizers, + ReactiveMessageReaderBuilder builder) { + LambdaSafe.callbacks(ReactiveMessageReaderBuilderCustomizer.class, customizers, builder) + .invoke((customizer) -> customizer.customize(builder)); + } + + @Bean + @ConditionalOnMissingBean + ReactivePulsarTemplate pulsarReactiveTemplate(ReactivePulsarSenderFactory reactivePulsarSenderFactory, + SchemaResolver schemaResolver, TopicResolver topicResolver) { + return new ReactivePulsarTemplate<>(reactivePulsarSenderFactory, schemaResolver, topicResolver); + } + + @Configuration(proxyBeanMethods = false) + @EnableReactivePulsar + @ConditionalOnMissingBean( + name = PulsarAnnotationSupportBeanNames.REACTIVE_PULSAR_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME) + static class EnableReactivePulsarConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarReactivePropertiesMapper.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarReactivePropertiesMapper.java new file mode 100644 index 000000000000..2f79bbae615f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarReactivePropertiesMapper.java @@ -0,0 +1,108 @@ +/* + * Copyright 2012-2023 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.pulsar; + +import java.util.ArrayList; + +import org.apache.pulsar.reactive.client.api.ReactiveMessageConsumerBuilder; +import org.apache.pulsar.reactive.client.api.ReactiveMessageReaderBuilder; +import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderBuilder; + +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.pulsar.reactive.listener.ReactivePulsarContainerProperties; + +/** + * Helper class used to map reactive {@link PulsarProperties} to various builder + * customizers. + * + * @author Chris Bono + * @author Phillip Webb + */ +final class PulsarReactivePropertiesMapper { + + private final PulsarProperties properties; + + PulsarReactivePropertiesMapper(PulsarProperties properties) { + this.properties = properties; + } + + void customizeMessageSenderBuilder(ReactiveMessageSenderBuilder builder) { + PulsarProperties.Producer properties = this.properties.getProducer(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getName).to(builder::producerName); + map.from(properties::getTopicName).to(builder::topic); + map.from(properties::getSendTimeout).to(builder::sendTimeout); + map.from(properties::getMessageRoutingMode).to(builder::messageRoutingMode); + map.from(properties::getHashingScheme).to(builder::hashingScheme); + map.from(properties::isBatchingEnabled).to(builder::batchingEnabled); + map.from(properties::isChunkingEnabled).to(builder::chunkingEnabled); + map.from(properties::getCompressionType).to(builder::compressionType); + map.from(properties::getAccessMode).to(builder::accessMode); + } + + void customizeMessageConsumerBuilder(ReactiveMessageConsumerBuilder builder) { + PulsarProperties.Consumer properties = this.properties.getConsumer(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getName).to(builder::consumerName); + map.from(properties::getTopics).as(ArrayList::new).to(builder::topics); + map.from(properties::getTopicsPattern).to(builder::topicsPattern); + map.from(properties::getPriorityLevel).to(builder::priorityLevel); + map.from(properties::isReadCompacted).to(builder::readCompacted); + map.from(properties::getDeadLetterPolicy).as(DeadLetterPolicyMapper::map).to(builder::deadLetterPolicy); + map.from(properties::isRetryEnable).to(builder::retryLetterTopicEnable); + customizerMessageConsumerBuilderSubscription(builder); + } + + private void customizerMessageConsumerBuilderSubscription(ReactiveMessageConsumerBuilder builder) { + PulsarProperties.Consumer.Subscription properties = this.properties.getConsumer().getSubscription(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getName).to(builder::subscriptionName); + map.from(properties::getInitialPosition).to(builder::subscriptionInitialPosition); + map.from(properties::getMode).to(builder::subscriptionMode); + map.from(properties::getTopicsMode).to(builder::topicsPatternSubscriptionMode); + map.from(properties::getType).to(builder::subscriptionType); + } + + void customizeContainerProperties(ReactivePulsarContainerProperties containerProperties) { + customizePulsarContainerConsumerSubscriptionProperties(containerProperties); + customizePulsarContainerListenerProperties(containerProperties); + } + + private void customizePulsarContainerConsumerSubscriptionProperties( + ReactivePulsarContainerProperties containerProperties) { + PulsarProperties.Consumer.Subscription properties = this.properties.getConsumer().getSubscription(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getType).to(containerProperties::setSubscriptionType); + } + + private void customizePulsarContainerListenerProperties(ReactivePulsarContainerProperties containerProperties) { + PulsarProperties.Listener properties = this.properties.getListener(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getSchemaType).to(containerProperties::setSchemaType); + } + + void customizeMessageReaderBuilder(ReactiveMessageReaderBuilder builder) { + PulsarProperties.Reader properties = this.properties.getReader(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(properties::getName).to(builder::readerName); + map.from(properties::getTopics).to(builder::topics); + map.from(properties::getSubscriptionName).to(builder::subscriptionName); + map.from(properties::getSubscriptionRolePrefix).to(builder::generatedSubscriptionNamePrefix); + map.from(properties::isReadCompacted).to(builder::readCompacted); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/package-info.java new file mode 100644 index 000000000000..d6ce8ee1d218 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Auto-configuration for Spring for Apache Pulsar. + */ +package org.springframework.boot.autoconfigure.pulsar; 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 8bfa50b648c1..db3b6221d064 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 @@ -2003,6 +2003,18 @@ "name": "spring.neo4j.uri", "defaultValue": "bolt://localhost:7687" }, + { + "name": "spring.pulsar.function.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable function support.", + "defaultValue": true + }, + { + "name": "spring.pulsar.producer.cache.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable caching in the PulsarProducerFactory.", + "defaultValue": true + }, { "name": "spring.quartz.jdbc.comment-prefix", "defaultValue": [ 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 972dad6815eb..7f6c606cd0e3 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 @@ -94,6 +94,8 @@ org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration org.springframework.boot.autoconfigure.netty.NettyAutoConfiguration org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration +org.springframework.boot.autoconfigure.pulsar.PulsarAutoConfiguration +org.springframework.boot.autoconfigure.pulsar.PulsarReactiveAutoConfiguration org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration org.springframework.boot.autoconfigure.r2dbc.R2dbcTransactionManagerAutoConfiguration diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/Customizers.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/Customizers.java new file mode 100644 index 000000000000..7f8de3556175 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/Customizers.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-2023 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.pulsar; + +import java.util.List; +import java.util.function.BiConsumer; + +import org.assertj.core.api.AssertDelegateTarget; +import org.mockito.InOrder; + +import org.springframework.test.util.ReflectionTestUtils; + +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +/** + * Test utility used to check customizers are called correctly. + * + * @param the customizer type + * @param the target class that is customized + * @author Phillip Webb + * @author Chris Bono + */ +final class Customizers { + + private final BiConsumer customizeAction; + + private final Class targetClass; + + @SuppressWarnings("unchecked") + private Customizers(Class targetClass, BiConsumer customizeAction) { + this.customizeAction = customizeAction; + this.targetClass = (Class) targetClass; + } + + /** + * Create an instance by getting the value from a field. + * @param source the source to extract the customizers from + * @param fieldName the field name + * @return a new {@link CustomizersAssert} instance + */ + @SuppressWarnings("unchecked") + CustomizersAssert fromField(Object source, String fieldName) { + return new CustomizersAssert(ReflectionTestUtils.getField(source, fieldName)); + } + + /** + * Create a new {@link Customizers} instance. + * @param the customizer class + * @param the target class that is customized + * @param targetClass the target class that is customized + * @param customizeAction the customizer action to take + * @return a new {@link Customizers} instance + */ + static Customizers of(Class targetClass, BiConsumer customizeAction) { + return new Customizers<>(targetClass, customizeAction); + } + + /** + * Assertions that can be applied to customizers. + */ + final class CustomizersAssert implements AssertDelegateTarget { + + private final List customizers; + + @SuppressWarnings("unchecked") + private CustomizersAssert(Object customizers) { + this.customizers = (customizers instanceof List) ? (List) customizers : List.of((C) customizers); + } + + /** + * Assert that the customize method is called in a specified order. It is expected + * that each customizer has set a unique value so the expected values can be used + * as a verify step. + * @param the value type + * @param call the call the customizer makes + * @param expectedValues the expected values + */ + @SuppressWarnings("unchecked") + void callsInOrder(BiConsumer call, V... expectedValues) { + T target = mock(Customizers.this.targetClass); + BiConsumer customizeAction = Customizers.this.customizeAction; + this.customizers.forEach((customizer) -> customizeAction.accept(customizer, target)); + InOrder ordered = inOrder(target); + for (V expectedValue : expectedValues) { + call.accept(ordered.verify(target), expectedValue); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/DeadLetterPolicyMapperTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/DeadLetterPolicyMapperTests.java new file mode 100644 index 000000000000..afc18050f64b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/DeadLetterPolicyMapperTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2023 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.pulsar; + +import org.apache.pulsar.client.api.DeadLetterPolicy; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link DeadLetterPolicyMapper}. + * + * @author Chris Bono + * @author Phillip Webb + */ +class DeadLetterPolicyMapperTests { + + @Test + void map() { + PulsarProperties.Consumer.DeadLetterPolicy properties = new PulsarProperties.Consumer.DeadLetterPolicy(); + properties.setMaxRedeliverCount(100); + properties.setRetryLetterTopic("my-retry-topic"); + properties.setDeadLetterTopic("my-dlt-topic"); + properties.setInitialSubscriptionName("my-initial-subscription"); + DeadLetterPolicy policy = DeadLetterPolicyMapper.map(properties); + assertThat(policy.getMaxRedeliverCount()).isEqualTo(100); + assertThat(policy.getRetryLetterTopic()).isEqualTo("my-retry-topic"); + assertThat(policy.getDeadLetterTopic()).isEqualTo("my-dlt-topic"); + assertThat(policy.getInitialSubscriptionName()).isEqualTo("my-initial-subscription"); + } + + @Test + void mapWhenMaxRedeliverCountIsNotPositiveThrowsException() { + PulsarProperties.Consumer.DeadLetterPolicy properties = new PulsarProperties.Consumer.DeadLetterPolicy(); + properties.setMaxRedeliverCount(0); + assertThatIllegalStateException().isThrownBy(() -> DeadLetterPolicyMapper.map(properties)) + .withMessage("Pulsar DeadLetterPolicy must have a positive 'max-redelivery-count' property value"); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationIntegrationTests.java new file mode 100644 index 000000000000..14c7a37baf5f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationIntegrationTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2012-2023 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.pulsar; + +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.apache.pulsar.client.api.PulsarClientException; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.PulsarContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.pulsar.annotation.PulsarListener; +import org.springframework.pulsar.core.PulsarTemplate; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link PulsarAutoConfiguration}. + * + * @author Chris Bono + * @author Phillip Webb + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@Testcontainers(disabledWithoutDocker = true) +class PulsarAutoConfigurationIntegrationTests { + + @Container + private static final PulsarContainer PULSAR_CONTAINER = new PulsarContainer(DockerImageNames.pulsar()) + .withStartupAttempts(2) + .withStartupTimeout(Duration.ofMinutes(3)); + + private static final CountDownLatch LISTEN_LATCH = new CountDownLatch(1); + + private static final String TOPIC = "pacit-hello-topic"; + + @DynamicPropertySource + static void pulsarProperties(DynamicPropertyRegistry registry) { + registry.add("spring.pulsar.client.service-url", PULSAR_CONTAINER::getPulsarBrokerUrl); + registry.add("spring.pulsar.admin.service-url", PULSAR_CONTAINER::getHttpServiceUrl); + } + + @Test + void appStartsWithAutoConfiguredSpringPulsarComponents( + @Autowired(required = false) PulsarTemplate pulsarTemplate) { + assertThat(pulsarTemplate).isNotNull(); + } + + @Test + void templateCanBeAccessedDuringWebRequest(@Autowired TestRestTemplate restTemplate) throws InterruptedException { + assertThat(restTemplate.getForObject("/hello", String.class)).startsWith("Hello World -> "); + assertThat(LISTEN_LATCH.await(5, TimeUnit.SECONDS)).isTrue(); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration({ DispatcherServletAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class, + WebMvcAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, JacksonAutoConfiguration.class, + PulsarAutoConfiguration.class, PulsarReactiveAutoConfiguration.class }) + @Import(TestWebController.class) + static class TestConfiguration { + + @PulsarListener(subscriptionName = TOPIC + "-sub", topics = TOPIC) + void listen(String ignored) { + LISTEN_LATCH.countDown(); + } + + } + + @RestController + static class TestWebController { + + private final PulsarTemplate pulsarTemplate; + + TestWebController(PulsarTemplate pulsarTemplate) { + this.pulsarTemplate = pulsarTemplate; + } + + @GetMapping("/hello") + String sayHello() throws PulsarClientException { + return "Hello World -> " + this.pulsarTemplate.send(TOPIC, "hello"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java new file mode 100644 index 000000000000..3710c9313cb7 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java @@ -0,0 +1,519 @@ +/* + * Copyright 2012-2023 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.pulsar; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import com.github.benmanes.caffeine.cache.Caffeine; +import org.apache.pulsar.client.api.ConsumerBuilder; +import org.apache.pulsar.client.api.ProducerBuilder; +import org.apache.pulsar.client.api.PulsarClient; +import org.apache.pulsar.client.api.ReaderBuilder; +import org.apache.pulsar.client.api.interceptor.ProducerInterceptor; +import org.apache.pulsar.common.schema.SchemaType; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.pulsar.annotation.PulsarBootstrapConfiguration; +import org.springframework.pulsar.annotation.PulsarListenerAnnotationBeanPostProcessor; +import org.springframework.pulsar.annotation.PulsarReaderAnnotationBeanPostProcessor; +import org.springframework.pulsar.config.ConcurrentPulsarListenerContainerFactory; +import org.springframework.pulsar.config.DefaultPulsarReaderContainerFactory; +import org.springframework.pulsar.config.PulsarListenerContainerFactory; +import org.springframework.pulsar.config.PulsarListenerEndpointRegistry; +import org.springframework.pulsar.config.PulsarReaderEndpointRegistry; +import org.springframework.pulsar.core.CachingPulsarProducerFactory; +import org.springframework.pulsar.core.ConsumerBuilderCustomizer; +import org.springframework.pulsar.core.DefaultPulsarClientFactory; +import org.springframework.pulsar.core.DefaultPulsarConsumerFactory; +import org.springframework.pulsar.core.DefaultPulsarProducerFactory; +import org.springframework.pulsar.core.DefaultPulsarReaderFactory; +import org.springframework.pulsar.core.DefaultSchemaResolver; +import org.springframework.pulsar.core.DefaultTopicResolver; +import org.springframework.pulsar.core.ProducerBuilderCustomizer; +import org.springframework.pulsar.core.PulsarAdministration; +import org.springframework.pulsar.core.PulsarConsumerFactory; +import org.springframework.pulsar.core.PulsarProducerFactory; +import org.springframework.pulsar.core.PulsarReaderFactory; +import org.springframework.pulsar.core.PulsarTemplate; +import org.springframework.pulsar.core.ReaderBuilderCustomizer; +import org.springframework.pulsar.core.SchemaResolver; +import org.springframework.pulsar.core.TopicResolver; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link PulsarAutoConfiguration}. + * + * @author Chris Bono + * @author Alexander Preuß + * @author Soby Chacko + * @author Phillip Webb + */ +class PulsarAutoConfigurationTests { + + private static final String INTERNAL_PULSAR_LISTENER_ANNOTATION_PROCESSOR = "org.springframework.pulsar.config.internalPulsarListenerAnnotationProcessor"; + + private static final String INTERNAL_PULSAR_READER_ANNOTATION_PROCESSOR = "org.springframework.pulsar.config.internalPulsarReaderAnnotationProcessor"; + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(PulsarAutoConfiguration.class)) + .withBean(PulsarClient.class, () -> mock(PulsarClient.class)); + + @Test + void whenPulsarNotOnClasspathAutoConfigurationIsSkipped() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(PulsarAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(PulsarClient.class)) + .run((context) -> assertThat(context).doesNotHaveBean(PulsarAutoConfiguration.class)); + } + + @Test + void whenSpringPulsarNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(PulsarTemplate.class)) + .run((context) -> assertThat(context).doesNotHaveBean(PulsarAutoConfiguration.class)); + } + + @Test + void whenCustomPulsarListenerAnnotationProcessorDefinedAutoConfigurationIsSkipped() { + this.contextRunner.withBean(INTERNAL_PULSAR_LISTENER_ANNOTATION_PROCESSOR, String.class, () -> "bean") + .run((context) -> assertThat(context).doesNotHaveBean(PulsarBootstrapConfiguration.class)); + } + + @Test + void whenCustomPulsarReaderAnnotationProcessorDefinedAutoConfigurationIsSkipped() { + this.contextRunner.withBean(INTERNAL_PULSAR_READER_ANNOTATION_PROCESSOR, String.class, () -> "bean") + .run((context) -> assertThat(context).doesNotHaveBean(PulsarBootstrapConfiguration.class)); + } + + @Test + void autoConfiguresBeans() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(PulsarConfiguration.class) + .hasSingleBean(DefaultPulsarClientFactory.class) + .hasSingleBean(PulsarClient.class) + .hasSingleBean(PulsarAdministration.class) + .hasSingleBean(DefaultSchemaResolver.class) + .hasSingleBean(DefaultTopicResolver.class) + .hasSingleBean(CachingPulsarProducerFactory.class) + .hasSingleBean(PulsarTemplate.class) + .hasSingleBean(DefaultPulsarConsumerFactory.class) + .hasSingleBean(ConcurrentPulsarListenerContainerFactory.class) + .hasSingleBean(DefaultPulsarReaderFactory.class) + .hasSingleBean(DefaultPulsarReaderContainerFactory.class) + .hasSingleBean(PulsarListenerAnnotationBeanPostProcessor.class) + .hasSingleBean(PulsarListenerEndpointRegistry.class) + .hasSingleBean(PulsarReaderAnnotationBeanPostProcessor.class) + .hasSingleBean(PulsarReaderEndpointRegistry.class)); + } + + @Nested + class ProducerFactoryTests { + + private final ApplicationContextRunner contextRunner = PulsarAutoConfigurationTests.this.contextRunner; + + @Test + @SuppressWarnings("unchecked") + void whenHasUserDefinedBeanDoesNotAutoConfigureBean() { + PulsarProducerFactory producerFactory = mock(PulsarProducerFactory.class); + this.contextRunner + .withBean("customPulsarProducerFactory", PulsarProducerFactory.class, () -> producerFactory) + .run((context) -> assertThat(context).getBean(PulsarProducerFactory.class).isSameAs(producerFactory)); + } + + @Test + void whenNoPropertiesUsesCachingPulsarProducerFactory() { + this.contextRunner.run((context) -> assertThat(context).getBean(PulsarProducerFactory.class) + .isExactlyInstanceOf(CachingPulsarProducerFactory.class)); + } + + @Test + void whenCachingDisabledUsesDefaultPulsarProducerFactory() { + this.contextRunner.withPropertyValues("spring.pulsar.producer.cache.enabled=false") + .run((context) -> assertThat(context).getBean(PulsarProducerFactory.class) + .isExactlyInstanceOf(DefaultPulsarProducerFactory.class)); + } + + @Test + void whenCachingEnabledUsesCachingPulsarProducerFactory() { + this.contextRunner.withPropertyValues("spring.pulsar.producer.cache.enabled=true") + .run((context) -> assertThat(context).getBean(PulsarProducerFactory.class) + .isExactlyInstanceOf(CachingPulsarProducerFactory.class)); + } + + @Test + void whenCachingEnabledAndCaffeineNotOnClasspathStillUsesCaffeine() { + this.contextRunner.withClassLoader(new FilteredClassLoader(Caffeine.class)) + .withPropertyValues("spring.pulsar.producer.cache.enabled=true") + .run((context) -> { + assertThat(context).getBean(CachingPulsarProducerFactory.class) + .extracting("producerCache") + .extracting(Object::getClass) + .extracting(Class::getName) + .isEqualTo("org.springframework.pulsar.core.CaffeineCacheProvider"); + assertThat(context).getBean(CachingPulsarProducerFactory.class) + .extracting("producerCache.cache") + .extracting(Object::getClass) + .extracting(Class::getName) + .asString() + .startsWith("org.springframework.pulsar.shade.com.github.benmanes.caffeine.cache."); + }); + } + + @Test + void whenCustomCachingPropertiesCreatesConfiguredBean() { + this.contextRunner + .withPropertyValues("spring.pulsar.producer.cache.expire-after-access=100s", + "spring.pulsar.producer.cache.maximum-size=5150", + "spring.pulsar.producer.cache.initial-capacity=200") + .run((context) -> assertThat(context).getBean(CachingPulsarProducerFactory.class) + .extracting("producerCache.cache.cache") + .hasFieldOrPropertyWithValue("maximum", 5150L) + .hasFieldOrPropertyWithValue("expiresAfterAccessNanos", TimeUnit.SECONDS.toNanos(100))); + } + + @Test + void whenHasTopicNamePropertyCreatesConfiguredBean() { + this.contextRunner.withPropertyValues("spring.pulsar.producer.topic-name=my-topic") + .run((context) -> assertThat(context).getBean(DefaultPulsarProducerFactory.class) + .hasFieldOrPropertyWithValue("defaultTopic", "my-topic")); + } + + @Test + void injectsExpectedBeans() { + this.contextRunner + .withPropertyValues("spring.pulsar.producer.topic-name=my-topic", + "spring.pulsar.producer.cache.enabled=false") + .run((context) -> assertThat(context).getBean(DefaultPulsarProducerFactory.class) + .hasFieldOrPropertyWithValue("pulsarClient", context.getBean(PulsarClient.class)) + .hasFieldOrPropertyWithValue("topicResolver", context.getBean(TopicResolver.class))); + } + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void whenHasUserDefinedCustomizersAppliesInCorrectOrder(boolean cachingEnabled) { + this.contextRunner + .withPropertyValues("spring.pulsar.producer.cache.enabled=" + cachingEnabled, + "spring.pulsar.producer.name=fromPropsCustomizer") + .withUserConfiguration(ProducerBuilderCustomizersConfig.class) + .run((context) -> { + DefaultPulsarProducerFactory producerFactory = context + .getBean(DefaultPulsarProducerFactory.class); + Customizers, ProducerBuilder> customizers = Customizers + .of(ProducerBuilder.class, ProducerBuilderCustomizer::customize); + assertThat(customizers.fromField(producerFactory, "defaultConfigCustomizers")).callsInOrder( + ProducerBuilder::producerName, "fromPropsCustomizer", "fromCustomizer1", "fromCustomizer2"); + }); + } + + @TestConfiguration(proxyBeanMethods = false) + static class ProducerBuilderCustomizersConfig { + + @Bean + @Order(200) + ProducerBuilderCustomizer customizerFoo() { + return (builder) -> builder.producerName("fromCustomizer2"); + } + + @Bean + @Order(100) + ProducerBuilderCustomizer customizerBar() { + return (builder) -> builder.producerName("fromCustomizer1"); + } + + } + + } + + @Nested + class TemplateTests { + + private final ApplicationContextRunner contextRunner = PulsarAutoConfigurationTests.this.contextRunner; + + @Test + @SuppressWarnings("unchecked") + void whenHasUserDefinedBeanDoesNotAutoConfigureBean() { + PulsarTemplate template = mock(PulsarTemplate.class); + this.contextRunner.withBean("customPulsarTemplate", PulsarTemplate.class, () -> template) + .run((context) -> assertThat(context).getBean(PulsarTemplate.class).isSameAs(template)); + } + + @Test + void injectsExpectedBeans() { + PulsarProducerFactory producerFactory = mock(PulsarProducerFactory.class); + SchemaResolver schemaResolver = mock(SchemaResolver.class); + TopicResolver topicResolver = mock(TopicResolver.class); + this.contextRunner + .withBean("customPulsarProducerFactory", PulsarProducerFactory.class, () -> producerFactory) + .withBean("schemaResolver", SchemaResolver.class, () -> schemaResolver) + .withBean("topicResolver", TopicResolver.class, () -> topicResolver) + .run((context) -> assertThat(context).getBean(PulsarTemplate.class) + .hasFieldOrPropertyWithValue("producerFactory", producerFactory) + .hasFieldOrPropertyWithValue("schemaResolver", schemaResolver) + .hasFieldOrPropertyWithValue("topicResolver", topicResolver)); + } + + @Test + void whenHasUseDefinedProducerInterceptorInjectsBean() { + ProducerInterceptor interceptor = mock(ProducerInterceptor.class); + this.contextRunner.withBean("customProducerInterceptor", ProducerInterceptor.class, () -> interceptor) + .run((context) -> assertThat(context).getBean(PulsarTemplate.class) + .extracting("interceptors") + .asList() + .contains(interceptor)); + } + + @Test + void whenHasUseDefinedProducerInterceptorsInjectsBeansInCorrectOrder() { + this.contextRunner.withUserConfiguration(InterceptorTestConfiguration.class) + .run((context) -> assertThat(context).getBean(PulsarTemplate.class) + .extracting("interceptors") + .asList() + .containsExactly(context.getBean("interceptorBar"), context.getBean("interceptorFoo"))); + } + + @Test + void whenNoPropertiesEnablesObservation() { + this.contextRunner.run((context) -> assertThat(context).getBean(PulsarTemplate.class) + .hasFieldOrPropertyWithValue("observationEnabled", true)); + } + + @Test + void whenObservationsEnabledEnablesObservation() { + this.contextRunner.withPropertyValues("spring.pulsar.template.observations-enabled=true") + .run((context) -> assertThat(context).getBean(PulsarTemplate.class) + .hasFieldOrPropertyWithValue("observationEnabled", true)); + } + + @Test + void whenObservationsDisabledDoesNotEnableObservation() { + this.contextRunner.withPropertyValues("spring.pulsar.template.observations-enabled=false") + .run((context) -> assertThat(context).getBean(PulsarTemplate.class) + .hasFieldOrPropertyWithValue("observationEnabled", false)); + } + + @Configuration(proxyBeanMethods = false) + static class InterceptorTestConfiguration { + + @Bean + @Order(200) + ProducerInterceptor interceptorFoo() { + return mock(ProducerInterceptor.class); + } + + @Bean + @Order(100) + ProducerInterceptor interceptorBar() { + return mock(ProducerInterceptor.class); + } + + } + + } + + @Nested + class ConsumerFactoryTests { + + private final ApplicationContextRunner contextRunner = PulsarAutoConfigurationTests.this.contextRunner; + + @Test + @SuppressWarnings("unchecked") + void whenHasUserDefinedBeanDoesNotAutoConfigureBean() { + PulsarConsumerFactory consumerFactory = mock(PulsarConsumerFactory.class); + this.contextRunner + .withBean("customPulsarConsumerFactory", PulsarConsumerFactory.class, () -> consumerFactory) + .run((context) -> assertThat(context).getBean(PulsarConsumerFactory.class).isSameAs(consumerFactory)); + } + + @Test + void injectsExpectedBeans() { + this.contextRunner.run((context) -> assertThat(context).getBean(DefaultPulsarConsumerFactory.class) + .hasFieldOrPropertyWithValue("pulsarClient", context.getBean(PulsarClient.class))); + } + + @Test + void whenHasUserDefinedCustomizersAppliesInCorrectOrder() { + this.contextRunner.withPropertyValues("spring.pulsar.consumer.name=fromPropsCustomizer") + .withUserConfiguration(ConsumerBuilderCustomizersConfig.class) + .run((context) -> { + DefaultPulsarConsumerFactory consumerFactory = context + .getBean(DefaultPulsarConsumerFactory.class); + Customizers, ConsumerBuilder> customizers = Customizers + .of(ConsumerBuilder.class, ConsumerBuilderCustomizer::customize); + assertThat(customizers.fromField(consumerFactory, "defaultConfigCustomizers")).callsInOrder( + ConsumerBuilder::consumerName, "fromPropsCustomizer", "fromCustomizer1", "fromCustomizer2"); + }); + } + + @TestConfiguration(proxyBeanMethods = false) + static class ConsumerBuilderCustomizersConfig { + + @Bean + @Order(200) + ConsumerBuilderCustomizer customizerFoo() { + return (builder) -> builder.consumerName("fromCustomizer2"); + } + + @Bean + @Order(100) + ConsumerBuilderCustomizer customizerBar() { + return (builder) -> builder.consumerName("fromCustomizer1"); + } + + } + + } + + @Nested + class ListenerTests { + + private final ApplicationContextRunner contextRunner = PulsarAutoConfigurationTests.this.contextRunner; + + @Test + void whenHasUserDefinedListenerContainerFactoryBeanDoesNotAutoConfigureBean() { + PulsarListenerContainerFactory listenerContainerFactory = mock(PulsarListenerContainerFactory.class); + this.contextRunner + .withBean("pulsarListenerContainerFactory", PulsarListenerContainerFactory.class, + () -> listenerContainerFactory) + .run((context) -> assertThat(context).getBean(PulsarListenerContainerFactory.class) + .isSameAs(listenerContainerFactory)); + } + + @Test + @SuppressWarnings("rawtypes") + void injectsExpectedBeans() { + PulsarConsumerFactory consumerFactory = mock(PulsarConsumerFactory.class); + SchemaResolver schemaResolver = mock(SchemaResolver.class); + TopicResolver topicResolver = mock(TopicResolver.class); + this.contextRunner.withBean("pulsarConsumerFactory", PulsarConsumerFactory.class, () -> consumerFactory) + .withBean("schemaResolver", SchemaResolver.class, () -> schemaResolver) + .withBean("topicResolver", TopicResolver.class, () -> topicResolver) + .run((context) -> assertThat(context).getBean(ConcurrentPulsarListenerContainerFactory.class) + .hasFieldOrPropertyWithValue("consumerFactory", consumerFactory) + .extracting(ConcurrentPulsarListenerContainerFactory::getContainerProperties) + .hasFieldOrPropertyWithValue("schemaResolver", schemaResolver) + .hasFieldOrPropertyWithValue("topicResolver", topicResolver)); + } + + @Test + @SuppressWarnings("unchecked") + void whenHasUserDefinedListenerAnnotationBeanPostProcessorBeanDoesNotAutoConfigureBean() { + PulsarListenerAnnotationBeanPostProcessor listenerAnnotationBeanPostProcessor = mock( + PulsarListenerAnnotationBeanPostProcessor.class); + this.contextRunner + .withBean("org.springframework.pulsar.config.internalPulsarListenerAnnotationProcessor", + PulsarListenerAnnotationBeanPostProcessor.class, () -> listenerAnnotationBeanPostProcessor) + .run((context) -> assertThat(context).getBean(PulsarListenerAnnotationBeanPostProcessor.class) + .isSameAs(listenerAnnotationBeanPostProcessor)); + } + + @Test + void whenHasCustomProperties() { + List properties = new ArrayList<>(); + properties.add("spring.pulsar.listener.schema-type=avro"); + this.contextRunner.withPropertyValues(properties.toArray(String[]::new)).run((context) -> { + ConcurrentPulsarListenerContainerFactory factory = context + .getBean(ConcurrentPulsarListenerContainerFactory.class); + assertThat(factory.getContainerProperties().getSchemaType()).isEqualTo(SchemaType.AVRO); + }); + } + + @Test + void whenNoPropertiesEnablesObservation() { + this.contextRunner + .run((context) -> assertThat(context).getBean(ConcurrentPulsarListenerContainerFactory.class) + .hasFieldOrPropertyWithValue("containerProperties.observationEnabled", true)); + } + + @Test + void whenObservationsEnabledEnablesObservation() { + this.contextRunner.withPropertyValues("spring.pulsar.listener.observation-enabled=true") + .run((context) -> assertThat(context).getBean(ConcurrentPulsarListenerContainerFactory.class) + .hasFieldOrPropertyWithValue("containerProperties.observationEnabled", true)); + } + + @Test + void whenObservationsDisabledDoesNotEnableObservation() { + this.contextRunner.withPropertyValues("spring.pulsar.listener.observation-enabled=false") + .run((context) -> assertThat(context).getBean(ConcurrentPulsarListenerContainerFactory.class) + .hasFieldOrPropertyWithValue("containerProperties.observationEnabled", false)); + } + + } + + @Nested + class ReaderFactoryTests { + + private final ApplicationContextRunner contextRunner = PulsarAutoConfigurationTests.this.contextRunner; + + @Test + @SuppressWarnings("unchecked") + void whenHasUserDefinedBeanDoesNotAutoConfigureBean() { + PulsarReaderFactory readerFactory = mock(PulsarReaderFactory.class); + this.contextRunner.withBean("customPulsarReaderFactory", PulsarReaderFactory.class, () -> readerFactory) + .run((context) -> assertThat(context).getBean(PulsarReaderFactory.class).isSameAs(readerFactory)); + } + + @Test + void injectsExpectedBeans() { + this.contextRunner.run((context) -> assertThat(context).getBean(DefaultPulsarReaderFactory.class) + .hasFieldOrPropertyWithValue("pulsarClient", context.getBean(PulsarClient.class))); + } + + @Test + void whenHasUserDefinedCustomizersAppliesInCorrectOrder() { + this.contextRunner.withPropertyValues("spring.pulsar.reader.name=fromPropsCustomizer") + .withUserConfiguration(ReaderBuilderCustomizersConfig.class) + .run((context) -> { + DefaultPulsarReaderFactory readerFactory = context.getBean(DefaultPulsarReaderFactory.class); + Customizers, ReaderBuilder> customizers = Customizers + .of(ReaderBuilder.class, ReaderBuilderCustomizer::customize); + assertThat(customizers.fromField(readerFactory, "defaultConfigCustomizers")).callsInOrder( + ReaderBuilder::readerName, "fromPropsCustomizer", "fromCustomizer1", "fromCustomizer2"); + }); + } + + @TestConfiguration(proxyBeanMethods = false) + static class ReaderBuilderCustomizersConfig { + + @Bean + @Order(200) + ReaderBuilderCustomizer customizerFoo() { + return (builder) -> builder.readerName("fromCustomizer2"); + } + + @Bean + @Order(100) + ReaderBuilderCustomizer customizerBar() { + return (builder) -> builder.readerName("fromCustomizer1"); + } + + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfigurationTests.java new file mode 100644 index 000000000000..70536effacac --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfigurationTests.java @@ -0,0 +1,318 @@ +/* + * Copyright 2012-2023 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.pulsar; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import org.apache.pulsar.client.admin.PulsarAdminBuilder; +import org.apache.pulsar.client.api.ClientBuilder; +import org.apache.pulsar.client.api.PulsarClient; +import org.apache.pulsar.client.api.Schema; +import org.apache.pulsar.common.schema.KeyValueEncodingType; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.assertj.core.api.InstanceOfAssertFactory; +import org.assertj.core.api.MapAssert; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.core.annotation.Order; +import org.springframework.pulsar.core.DefaultPulsarClientFactory; +import org.springframework.pulsar.core.DefaultSchemaResolver; +import org.springframework.pulsar.core.DefaultTopicResolver; +import org.springframework.pulsar.core.PulsarAdminBuilderCustomizer; +import org.springframework.pulsar.core.PulsarAdministration; +import org.springframework.pulsar.core.PulsarClientBuilderCustomizer; +import org.springframework.pulsar.core.PulsarClientFactory; +import org.springframework.pulsar.core.SchemaResolver; +import org.springframework.pulsar.core.SchemaResolver.SchemaResolverCustomizer; +import org.springframework.pulsar.core.TopicResolver; +import org.springframework.pulsar.function.PulsarFunctionAdministration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link PulsarConfiguration}. + * + * @author Chris Bono + * @author Alexander Preuß + * @author Soby Chacko + * @author Phillip Webb + */ +class PulsarConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(PulsarConfiguration.class)) + .withBean(PulsarClient.class, () -> mock(PulsarClient.class)); + + @Nested + class ClientTests { + + @Test + void whenHasUserDefinedClientFactoryBeanDoesNotAutoConfigureBean() { + PulsarClientFactory customFactory = mock(PulsarClientFactory.class); + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(PulsarConfiguration.class)) + .withBean("customPulsarClientFactory", PulsarClientFactory.class, () -> customFactory) + .run((context) -> assertThat(context).getBean(PulsarClientFactory.class).isSameAs(customFactory)); + } + + @Test + void whenHasUserDefinedClientBeanDoesNotAutoConfigureBean() { + PulsarClient customClient = mock(PulsarClient.class); + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(PulsarConfiguration.class)) + .withBean("customPulsarClient", PulsarClient.class, () -> customClient) + .run((context) -> assertThat(context).getBean(PulsarClient.class).isSameAs(customClient)); + } + + @Test + void whenHasUserDefinedCustomizersAppliesInCorrectOrder() { + PulsarConfigurationTests.this.contextRunner + .withUserConfiguration(PulsarClientBuilderCustomizersConfig.class) + .withPropertyValues("spring.pulsar.client.service-url=fromPropsCustomizer") + .run((context) -> { + DefaultPulsarClientFactory clientFactory = context.getBean(DefaultPulsarClientFactory.class); + Customizers customizers = Customizers + .of(ClientBuilder.class, PulsarClientBuilderCustomizer::customize); + assertThat(customizers.fromField(clientFactory, "customizer")).callsInOrder( + ClientBuilder::serviceUrl, "fromPropsCustomizer", "fromCustomizer1", "fromCustomizer2"); + }); + } + + @TestConfiguration(proxyBeanMethods = false) + static class PulsarClientBuilderCustomizersConfig { + + @Bean + @Order(200) + PulsarClientBuilderCustomizer customizerFoo() { + return (builder) -> builder.serviceUrl("fromCustomizer2"); + } + + @Bean + @Order(100) + PulsarClientBuilderCustomizer customizerBar() { + return (builder) -> builder.serviceUrl("fromCustomizer1"); + } + + } + + } + + @Nested + class AdministrationTests { + + private final ApplicationContextRunner contextRunner = PulsarConfigurationTests.this.contextRunner; + + @Test + void whenHasUserDefinedBeanDoesNotAutoConfigureBean() { + PulsarAdministration pulsarAdministration = mock(PulsarAdministration.class); + this.contextRunner + .withBean("customPulsarAdministration", PulsarAdministration.class, () -> pulsarAdministration) + .run((context) -> assertThat(context).getBean(PulsarAdministration.class) + .isSameAs(pulsarAdministration)); + } + + @Test + void whenHasUserDefinedCustomizersAppliesInCorrectOrder() { + this.contextRunner.withUserConfiguration(PulsarAdminBuilderCustomizersConfig.class) + .withPropertyValues("spring.pulsar.admin.service-url=fromPropsCustomizer") + .run((context) -> { + PulsarAdministration pulsarAdmin = context.getBean(PulsarAdministration.class); + Customizers customizers = Customizers + .of(PulsarAdminBuilder.class, PulsarAdminBuilderCustomizer::customize); + assertThat(customizers.fromField(pulsarAdmin, "adminCustomizers")).callsInOrder( + PulsarAdminBuilder::serviceHttpUrl, "fromPropsCustomizer", "fromCustomizer1", + "fromCustomizer2"); + }); + } + + @TestConfiguration(proxyBeanMethods = false) + static class PulsarAdminBuilderCustomizersConfig { + + @Bean + @Order(200) + PulsarAdminBuilderCustomizer customizerFoo() { + return (builder) -> builder.serviceHttpUrl("fromCustomizer2"); + } + + @Bean + @Order(100) + PulsarAdminBuilderCustomizer customizerBar() { + return (builder) -> builder.serviceHttpUrl("fromCustomizer1"); + } + + } + + } + + @Nested + class SchemaResolverTests { + + @SuppressWarnings("rawtypes") + private static final InstanceOfAssertFactory> CLASS_SCHEMA_MAP = InstanceOfAssertFactories + .map(Class.class, Schema.class); + + private final ApplicationContextRunner contextRunner = PulsarConfigurationTests.this.contextRunner; + + @Test + void whenHasUserDefinedBeanDoesNotAutoConfigureBean() { + SchemaResolver schemaResolver = mock(SchemaResolver.class); + this.contextRunner.withBean("customSchemaResolver", SchemaResolver.class, () -> schemaResolver) + .run((context) -> assertThat(context).getBean(SchemaResolver.class).isSameAs(schemaResolver)); + } + + @Test + void whenHasUserDefinedSchemaResolverCustomizer() { + SchemaResolverCustomizer customizer = (schemaResolver) -> schemaResolver + .addCustomSchemaMapping(TestRecord.class, Schema.STRING); + this.contextRunner.withBean("schemaResolverCustomizer", SchemaResolverCustomizer.class, () -> customizer) + .run((context) -> assertThat(context).getBean(DefaultSchemaResolver.class) + .extracting(DefaultSchemaResolver::getCustomSchemaMappings, InstanceOfAssertFactories.MAP) + .containsEntry(TestRecord.class, Schema.STRING)); + } + + @Test + void whenHasDefaultsTypeMappingForPrimitiveAddsToSchemaResolver() { + List properties = new ArrayList<>(); + properties.add("spring.pulsar.defaults.type-mappings[0].message-type=" + TestRecord.CLASS_NAME); + properties.add("spring.pulsar.defaults.type-mappings[0].schema-info.schema-type=STRING"); + this.contextRunner.withPropertyValues(properties.toArray(String[]::new)) + .run((context) -> assertThat(context).getBean(DefaultSchemaResolver.class) + .extracting(DefaultSchemaResolver::getCustomSchemaMappings, InstanceOfAssertFactories.MAP) + .containsOnly(entry(TestRecord.class, Schema.STRING))); + } + + @Test + void whenHasDefaultsTypeMappingForStructAddsToSchemaResolver() { + List properties = new ArrayList<>(); + properties.add("spring.pulsar.defaults.type-mappings[0].message-type=" + TestRecord.CLASS_NAME); + properties.add("spring.pulsar.defaults.type-mappings[0].schema-info.schema-type=JSON"); + Schema expectedSchema = Schema.JSON(TestRecord.class); + this.contextRunner.withPropertyValues(properties.toArray(String[]::new)) + .run((context) -> assertThat(context).getBean(DefaultSchemaResolver.class) + .extracting(DefaultSchemaResolver::getCustomSchemaMappings, CLASS_SCHEMA_MAP) + .hasEntrySatisfying(TestRecord.class, schemaEqualTo(expectedSchema))); + } + + @Test + void whenHasDefaultsTypeMappingForKeyValueAddsToSchemaResolver() { + List properties = new ArrayList<>(); + properties.add("spring.pulsar.defaults.type-mappings[0].message-type=" + TestRecord.CLASS_NAME); + properties.add("spring.pulsar.defaults.type-mappings[0].schema-info.schema-type=key-value"); + properties.add("spring.pulsar.defaults.type-mappings[0].schema-info.message-key-type=java.lang.String"); + Schema expectedSchema = Schema.KeyValue(Schema.STRING, Schema.JSON(TestRecord.class), + KeyValueEncodingType.INLINE); + this.contextRunner.withPropertyValues(properties.toArray(String[]::new)) + .run((context) -> assertThat(context).getBean(DefaultSchemaResolver.class) + .extracting(DefaultSchemaResolver::getCustomSchemaMappings, CLASS_SCHEMA_MAP) + .hasEntrySatisfying(TestRecord.class, schemaEqualTo(expectedSchema))); + } + + @SuppressWarnings("rawtypes") + private Consumer schemaEqualTo(Schema expected) { + return (actual) -> assertThat(actual.getSchemaInfo()).isEqualTo(expected.getSchemaInfo()); + } + + } + + @Nested + class TopicResolverTests { + + private final ApplicationContextRunner contextRunner = PulsarConfigurationTests.this.contextRunner; + + @Test + void whenHasUserDefinedBeanDoesNotAutoConfigureBean() { + TopicResolver topicResolver = mock(TopicResolver.class); + this.contextRunner.withBean("customTopicResolver", TopicResolver.class, () -> topicResolver) + .run((context) -> assertThat(context).getBean(TopicResolver.class).isSameAs(topicResolver)); + } + + @Test + void whenHasDefaultsTypeMappingAddsToSchemaResolver() { + List properties = new ArrayList<>(); + properties.add("spring.pulsar.defaults.type-mappings[0].message-type=" + TestRecord.CLASS_NAME); + properties.add("spring.pulsar.defaults.type-mappings[0].topic-name=foo-topic"); + properties.add("spring.pulsar.defaults.type-mappings[1].message-type=java.lang.String"); + properties.add("spring.pulsar.defaults.type-mappings[1].topic-name=string-topic"); + this.contextRunner.withPropertyValues(properties.toArray(String[]::new)) + .run((context) -> assertThat(context).getBean(TopicResolver.class) + .asInstanceOf(InstanceOfAssertFactories.type(DefaultTopicResolver.class)) + .extracting(DefaultTopicResolver::getCustomTopicMappings, InstanceOfAssertFactories.MAP) + .containsOnly(entry(TestRecord.class, "foo-topic"), entry(String.class, "string-topic"))); + } + + } + + @Nested + class FunctionAdministrationTests { + + private final ApplicationContextRunner contextRunner = PulsarConfigurationTests.this.contextRunner; + + @Test + void whenNoPropertiesAddsFunctionAdministrationBean() { + this.contextRunner.run((context) -> assertThat(context).getBean(PulsarFunctionAdministration.class) + .hasFieldOrPropertyWithValue("failFast", Boolean.TRUE) + .hasFieldOrPropertyWithValue("propagateFailures", Boolean.TRUE) + .hasFieldOrPropertyWithValue("propagateStopFailures", Boolean.FALSE) + .hasNoNullFieldsOrProperties() // ensures object providers set + .extracting("pulsarAdministration") + .isSameAs(context.getBean(PulsarAdministration.class))); + } + + @Test + void whenHasFunctionPropertiesAppliesPropertiesToBean() { + List properties = new ArrayList<>(); + properties.add("spring.pulsar.function.fail-fast=false"); + properties.add("spring.pulsar.function.propagate-failures=false"); + properties.add("spring.pulsar.function.propagate-stop-failures=true"); + this.contextRunner.withPropertyValues(properties.toArray(String[]::new)) + .run((context) -> assertThat(context).getBean(PulsarFunctionAdministration.class) + .hasFieldOrPropertyWithValue("failFast", Boolean.FALSE) + .hasFieldOrPropertyWithValue("propagateFailures", Boolean.FALSE) + .hasFieldOrPropertyWithValue("propagateStopFailures", Boolean.TRUE)); + } + + @Test + void whenHasFunctionDisabledPropertyDoesNotCreateBean() { + this.contextRunner.withPropertyValues("spring.pulsar.function.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(PulsarFunctionAdministration.class)); + } + + @Test + void whenHasCustomFunctionAdministrationBean() { + PulsarFunctionAdministration functionAdministration = mock(PulsarFunctionAdministration.class); + this.contextRunner.withBean(PulsarFunctionAdministration.class, () -> functionAdministration) + .run((context) -> assertThat(context).getBean(PulsarFunctionAdministration.class) + .isSameAs(functionAdministration)); + } + + } + + record TestRecord() { + + private static final String CLASS_NAME = TestRecord.class.getName(); + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java new file mode 100644 index 000000000000..283e06c1d45c --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java @@ -0,0 +1,190 @@ +/* + * Copyright 2012-2023 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.pulsar; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; + +import org.apache.pulsar.client.admin.PulsarAdminBuilder; +import org.apache.pulsar.client.api.ClientBuilder; +import org.apache.pulsar.client.api.CompressionType; +import org.apache.pulsar.client.api.ConsumerBuilder; +import org.apache.pulsar.client.api.DeadLetterPolicy; +import org.apache.pulsar.client.api.HashingScheme; +import org.apache.pulsar.client.api.MessageRoutingMode; +import org.apache.pulsar.client.api.ProducerAccessMode; +import org.apache.pulsar.client.api.ProducerBuilder; +import org.apache.pulsar.client.api.PulsarClientException.UnsupportedAuthenticationException; +import org.apache.pulsar.client.api.ReaderBuilder; +import org.apache.pulsar.client.api.SubscriptionType; +import org.apache.pulsar.common.schema.SchemaType; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.pulsar.PulsarProperties.Consumer; +import org.springframework.pulsar.listener.PulsarContainerProperties; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link PulsarPropertiesMapper}. + * + * @author Chris Bono + * @author Phillip Webb + */ +class PulsarPropertiesMapperTests { + + @Test + void customizeClientBuilderWhenHasNoAuthentication() { + PulsarProperties properties = new PulsarProperties(); + properties.getClient().setServiceUrl("https://example.com"); + properties.getClient().setConnectionTimeout(Duration.ofSeconds(1)); + properties.getClient().setOperationTimeout(Duration.ofSeconds(2)); + properties.getClient().setLookupTimeout(Duration.ofSeconds(3)); + ClientBuilder builder = mock(ClientBuilder.class); + new PulsarPropertiesMapper(properties).customizeClientBuilder(builder); + then(builder).should().serviceUrl("https://example.com"); + then(builder).should().connectionTimeout(1000, TimeUnit.MILLISECONDS); + then(builder).should().operationTimeout(2000, TimeUnit.MILLISECONDS); + then(builder).should().lookupTimeout(3000, TimeUnit.MILLISECONDS); + } + + @Test + void customizeClientBuilderWhenHasAuthentication() throws UnsupportedAuthenticationException { + PulsarProperties properties = new PulsarProperties(); + Map params = Map.of("param", "name"); + properties.getClient().getAuthentication().setPluginClassName("myclass"); + properties.getClient().getAuthentication().setParam(params); + ClientBuilder builder = mock(ClientBuilder.class); + new PulsarPropertiesMapper(properties).customizeClientBuilder(builder); + then(builder).should().authentication("myclass", params); + } + + @Test + void customizeAdminBuilderWhenHasNoAuthentication() { + PulsarProperties properties = new PulsarProperties(); + properties.getAdmin().setServiceUrl("https://example.com"); + properties.getAdmin().setConnectionTimeout(Duration.ofSeconds(1)); + properties.getAdmin().setReadTimeout(Duration.ofSeconds(2)); + properties.getAdmin().setRequestTimeout(Duration.ofSeconds(3)); + PulsarAdminBuilder builder = mock(PulsarAdminBuilder.class); + new PulsarPropertiesMapper(properties).customizeAdminBuilder(builder); + then(builder).should().serviceHttpUrl("https://example.com"); + then(builder).should().connectionTimeout(1000, TimeUnit.MILLISECONDS); + then(builder).should().readTimeout(2000, TimeUnit.MILLISECONDS); + then(builder).should().requestTimeout(3000, TimeUnit.MILLISECONDS); + } + + @Test + void customizeAdminBuilderWhenHasAuthentication() throws UnsupportedAuthenticationException { + PulsarProperties properties = new PulsarProperties(); + Map params = Map.of("param", "name"); + properties.getAdmin().getAuthentication().setPluginClassName("myclass"); + properties.getAdmin().getAuthentication().setParam(params); + PulsarAdminBuilder builder = mock(PulsarAdminBuilder.class); + new PulsarPropertiesMapper(properties).customizeAdminBuilder(builder); + then(builder).should().authentication("myclass", params); + } + + @Test + @SuppressWarnings("unchecked") + void customizeProducerBuilder() { + PulsarProperties properties = new PulsarProperties(); + properties.getProducer().setName("name"); + properties.getProducer().setTopicName("topicname"); + properties.getProducer().setSendTimeout(Duration.ofSeconds(1)); + properties.getProducer().setMessageRoutingMode(MessageRoutingMode.RoundRobinPartition); + properties.getProducer().setHashingScheme(HashingScheme.JavaStringHash); + properties.getProducer().setBatchingEnabled(false); + properties.getProducer().setChunkingEnabled(true); + properties.getProducer().setCompressionType(CompressionType.SNAPPY); + properties.getProducer().setAccessMode(ProducerAccessMode.Exclusive); + ProducerBuilder builder = mock(ProducerBuilder.class); + new PulsarPropertiesMapper(properties).customizeProducerBuilder(builder); + then(builder).should().producerName("name"); + then(builder).should().topic("topicname"); + then(builder).should().sendTimeout(1000, TimeUnit.MILLISECONDS); + then(builder).should().messageRoutingMode(MessageRoutingMode.RoundRobinPartition); + then(builder).should().hashingScheme(HashingScheme.JavaStringHash); + then(builder).should().enableBatching(false); + then(builder).should().enableChunking(true); + then(builder).should().compressionType(CompressionType.SNAPPY); + then(builder).should().accessMode(ProducerAccessMode.Exclusive); + } + + @Test + @SuppressWarnings("unchecked") + void customizeConsumerBuilder() { + PulsarProperties properties = new PulsarProperties(); + List topics = List.of("mytopic"); + Pattern topisPattern = Pattern.compile("my-pattern"); + properties.getConsumer().setName("name"); + properties.getConsumer().setTopics(topics); + properties.getConsumer().setTopicsPattern(topisPattern); + properties.getConsumer().setPriorityLevel(123); + properties.getConsumer().setReadCompacted(true); + Consumer.DeadLetterPolicy deadLetterPolicy = new Consumer.DeadLetterPolicy(); + deadLetterPolicy.setDeadLetterTopic("my-dlt"); + deadLetterPolicy.setMaxRedeliverCount(1); + properties.getConsumer().setDeadLetterPolicy(deadLetterPolicy); + ConsumerBuilder builder = mock(ConsumerBuilder.class); + new PulsarPropertiesMapper(properties).customizeConsumerBuilder(builder); + then(builder).should().consumerName("name"); + then(builder).should().topics(topics); + then(builder).should().topicsPattern(topisPattern); + then(builder).should().priorityLevel(123); + then(builder).should().readCompacted(true); + then(builder).should().deadLetterPolicy(new DeadLetterPolicy(1, null, "my-dlt", null)); + } + + @Test + void customizeContainerProperties() { + PulsarProperties properties = new PulsarProperties(); + properties.getConsumer().getSubscription().setType(SubscriptionType.Shared); + properties.getListener().setSchemaType(SchemaType.AVRO); + properties.getListener().setObservationEnabled(false); + PulsarContainerProperties containerProperties = new PulsarContainerProperties("my-topic-pattern"); + new PulsarPropertiesMapper(properties).customizeContainerProperties(containerProperties); + assertThat(containerProperties.getSubscriptionType()).isEqualTo(SubscriptionType.Shared); + assertThat(containerProperties.getSchemaType()).isEqualTo(SchemaType.AVRO); + assertThat(containerProperties.isObservationEnabled()).isFalse(); + } + + @Test + @SuppressWarnings("unchecked") + void customizeReaderBuilder() { + PulsarProperties properties = new PulsarProperties(); + List topics = List.of("mytopic"); + properties.getReader().setName("name"); + properties.getReader().setTopics(topics); + properties.getReader().setSubscriptionName("subname"); + properties.getReader().setSubscriptionRolePrefix("subroleprefix"); + properties.getReader().setReadCompacted(true); + ReaderBuilder builder = mock(ReaderBuilder.class); + new PulsarPropertiesMapper(properties).customizeReaderBuilder(builder); + then(builder).should().readerName("name"); + then(builder).should().topics(topics); + then(builder).should().subscriptionName("subname"); + then(builder).should().subscriptionRolePrefix("subroleprefix"); + then(builder).should().readCompacted(true); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesTests.java new file mode 100644 index 000000000000..48c17247a4f9 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesTests.java @@ -0,0 +1,365 @@ +/* + * Copyright 2012-2023 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.pulsar; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +import org.apache.pulsar.client.api.CompressionType; +import org.apache.pulsar.client.api.HashingScheme; +import org.apache.pulsar.client.api.MessageRoutingMode; +import org.apache.pulsar.client.api.ProducerAccessMode; +import org.apache.pulsar.client.api.RegexSubscriptionMode; +import org.apache.pulsar.client.api.SubscriptionInitialPosition; +import org.apache.pulsar.client.api.SubscriptionMode; +import org.apache.pulsar.client.api.SubscriptionType; +import org.apache.pulsar.common.schema.SchemaType; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.pulsar.PulsarProperties.Defaults.SchemaInfo; +import org.springframework.boot.autoconfigure.pulsar.PulsarProperties.Defaults.TypeMapping; +import org.springframework.boot.context.properties.bind.BindException; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link PulsarProperties}. + * + * @author Chris Bono + * @author Christophe Bornet + * @author Soby Chacko + * @author Phillip Webb + */ +class PulsarPropertiesTests { + + private PulsarProperties bindPropeties(Map map) { + return new Binder(new MapConfigurationPropertySource(map)).bind("spring.pulsar", PulsarProperties.class).get(); + } + + @Nested + class ClientProperties { + + @Test + void bind() { + Map map = new HashMap<>(); + map.put("spring.pulsar.client.service-url", "my-service-url"); + map.put("spring.pulsar.client.operation-timeout", "1s"); + map.put("spring.pulsar.client.lookup-timeout", "2s"); + map.put("spring.pulsar.client.connection-timeout", "12s"); + PulsarProperties.Client properties = bindPropeties(map).getClient(); + assertThat(properties.getServiceUrl()).isEqualTo("my-service-url"); + assertThat(properties.getOperationTimeout()).isEqualTo(Duration.ofMillis(1000)); + assertThat(properties.getLookupTimeout()).isEqualTo(Duration.ofMillis(2000)); + assertThat(properties.getConnectionTimeout()).isEqualTo(Duration.ofMillis(12000)); + } + + @Test + void bindAuthentication() { + Map map = new HashMap<>(); + map.put("spring.pulsar.client.authentication.plugin-class-name", "com.example.MyAuth"); + map.put("spring.pulsar.client.authentication.param.token", "1234"); + PulsarProperties.Client properties = bindPropeties(map).getClient(); + assertThat(properties.getAuthentication().getPluginClassName()).isEqualTo("com.example.MyAuth"); + assertThat(properties.getAuthentication().getParam()).containsEntry("token", "1234"); + } + + } + + @Nested + class AdminProperties { + + private final String authPluginClassName = "org.apache.pulsar.client.impl.auth.AuthenticationToken"; + + private final String authToken = "1234"; + + @Test + void bind() { + Map map = new HashMap<>(); + map.put("spring.pulsar.admin.service-url", "my-service-url"); + map.put("spring.pulsar.admin.connection-timeout", "12s"); + map.put("spring.pulsar.admin.read-timeout", "13s"); + map.put("spring.pulsar.admin.request-timeout", "14s"); + PulsarProperties.Admin properties = bindPropeties(map).getAdmin(); + assertThat(properties.getServiceUrl()).isEqualTo("my-service-url"); + assertThat(properties.getConnectionTimeout()).isEqualTo(Duration.ofSeconds(12)); + assertThat(properties.getReadTimeout()).isEqualTo(Duration.ofSeconds(13)); + assertThat(properties.getRequestTimeout()).isEqualTo(Duration.ofSeconds(14)); + } + + @Test + void bindAuthentication() { + Map map = new HashMap<>(); + map.put("spring.pulsar.admin.authentication.plugin-class-name", this.authPluginClassName); + map.put("spring.pulsar.admin.authentication.param.token", this.authToken); + PulsarProperties.Admin properties = bindPropeties(map).getAdmin(); + assertThat(properties.getAuthentication().getPluginClassName()).isEqualTo(this.authPluginClassName); + assertThat(properties.getAuthentication().getParam()).containsEntry("token", this.authToken); + } + + } + + @Nested + class DefaultsProperties { + + @Test + void bindWhenNoTypeMappings() { + assertThat(new PulsarProperties().getDefaults().getTypeMappings()).isEmpty(); + } + + @Test + void bindWhenTypeMappingsWithTopicsOnly() { + Map map = new HashMap<>(); + map.put("spring.pulsar.defaults.type-mappings[0].message-type", TestMessage.class.getName()); + map.put("spring.pulsar.defaults.type-mappings[0].topic-name", "foo-topic"); + map.put("spring.pulsar.defaults.type-mappings[1].message-type", String.class.getName()); + map.put("spring.pulsar.defaults.type-mappings[1].topic-name", "string-topic"); + PulsarProperties.Defaults properties = bindPropeties(map).getDefaults(); + TypeMapping expectedTopic1 = new TypeMapping(TestMessage.class, "foo-topic", null); + TypeMapping expectedTopic2 = new TypeMapping(String.class, "string-topic", null); + assertThat(properties.getTypeMappings()).containsExactly(expectedTopic1, expectedTopic2); + } + + @Test + void bindWhenTypeMappingsWithSchemaOnly() { + Map map = new HashMap<>(); + map.put("spring.pulsar.defaults.type-mappings[0].message-type", TestMessage.class.getName()); + map.put("spring.pulsar.defaults.type-mappings[0].schema-info.schema-type", "JSON"); + PulsarProperties.Defaults properties = bindPropeties(map).getDefaults(); + TypeMapping expected = new TypeMapping(TestMessage.class, null, new SchemaInfo(SchemaType.JSON, null)); + assertThat(properties.getTypeMappings()).containsExactly(expected); + } + + @Test + void bindWhenTypeMappingsWithTopicAndSchema() { + Map map = new HashMap<>(); + map.put("spring.pulsar.defaults.type-mappings[0].message-type", TestMessage.class.getName()); + map.put("spring.pulsar.defaults.type-mappings[0].topic-name", "foo-topic"); + map.put("spring.pulsar.defaults.type-mappings[0].schema-info.schema-type", "JSON"); + PulsarProperties.Defaults properties = bindPropeties(map).getDefaults(); + TypeMapping expected = new TypeMapping(TestMessage.class, "foo-topic", + new SchemaInfo(SchemaType.JSON, null)); + assertThat(properties.getTypeMappings()).containsExactly(expected); + } + + @Test + void bindWhenTypeMappingsWithKeyValueSchema() { + Map map = new HashMap<>(); + map.put("spring.pulsar.defaults.type-mappings[0].message-type", TestMessage.class.getName()); + map.put("spring.pulsar.defaults.type-mappings[0].schema-info.schema-type", "KEY_VALUE"); + map.put("spring.pulsar.defaults.type-mappings[0].schema-info.message-key-type", String.class.getName()); + PulsarProperties.Defaults properties = bindPropeties(map).getDefaults(); + TypeMapping expected = new TypeMapping(TestMessage.class, null, + new SchemaInfo(SchemaType.KEY_VALUE, String.class)); + assertThat(properties.getTypeMappings()).containsExactly(expected); + } + + @Test + void bindWhenNoSchemaThrowsException() { + Map map = new HashMap<>(); + map.put("spring.pulsar.defaults.type-mappings[0].message-type", TestMessage.class.getName()); + map.put("spring.pulsar.defaults.type-mappings[0].schema-info.message-key-type", String.class.getName()); + assertThatExceptionOfType(BindException.class).isThrownBy(() -> bindPropeties(map)) + .havingRootCause() + .withMessageContaining("schemaType must not be null"); + } + + @Test + void bindWhenSchemaTypeNoneThrowsException() { + Map map = new HashMap<>(); + map.put("spring.pulsar.defaults.type-mappings[0].message-type", TestMessage.class.getName()); + map.put("spring.pulsar.defaults.type-mappings[0].schema-info.schema-type", "NONE"); + assertThatExceptionOfType(BindException.class).isThrownBy(() -> bindPropeties(map)) + .havingRootCause() + .withMessageContaining("schemaType 'NONE' not supported"); + } + + @Test + void bindWhenMessageKeyTypeSetOnNonKeyValueSchemaThrowsException() { + Map map = new HashMap<>(); + map.put("spring.pulsar.defaults.type-mappings[0].message-type", TestMessage.class.getName()); + map.put("spring.pulsar.defaults.type-mappings[0].schema-info.schema-type", "JSON"); + map.put("spring.pulsar.defaults.type-mappings[0].schema-info.message-key-type", String.class.getName()); + assertThatExceptionOfType(BindException.class).isThrownBy(() -> bindPropeties(map)) + .havingRootCause() + .withMessageContaining("messageKeyType can only be set when schemaType is KEY_VALUE"); + } + + record TestMessage(String value) { + } + + } + + @Nested + class FunctionProperties { + + @Test + void defaults() { + PulsarProperties.Function properties = new PulsarProperties.Function(); + assertThat(properties.isFailFast()).isTrue(); + assertThat(properties.isPropagateFailures()).isTrue(); + assertThat(properties.isPropagateStopFailures()).isFalse(); + } + + @Test + void bind() { + Map props = new HashMap<>(); + props.put("spring.pulsar.function.fail-fast", "false"); + props.put("spring.pulsar.function.propagate-failures", "false"); + props.put("spring.pulsar.function.propagate-stop-failures", "true"); + PulsarProperties.Function properties = bindPropeties(props).getFunction(); + assertThat(properties.isFailFast()).isFalse(); + assertThat(properties.isPropagateFailures()).isFalse(); + assertThat(properties.isPropagateStopFailures()).isTrue(); + } + + } + + @Nested + class ProducerProperties { + + @Test + void bind() { + Map map = new HashMap<>(); + map.put("spring.pulsar.producer.name", "my-producer"); + map.put("spring.pulsar.producer.topic-name", "my-topic"); + map.put("spring.pulsar.producer.send-timeout", "2s"); + map.put("spring.pulsar.producer.message-routing-mode", "custompartition"); + map.put("spring.pulsar.producer.hashing-scheme", "murmur3_32hash"); + map.put("spring.pulsar.producer.batching-enabled", "false"); + map.put("spring.pulsar.producer.chunking-enabled", "true"); + map.put("spring.pulsar.producer.compression-type", "lz4"); + map.put("spring.pulsar.producer.access-mode", "exclusive"); + map.put("spring.pulsar.producer.cache.expire-after-access", "2s"); + map.put("spring.pulsar.producer.cache.maximum-size", "3"); + map.put("spring.pulsar.producer.cache.initial-capacity", "5"); + PulsarProperties.Producer properties = bindPropeties(map).getProducer(); + assertThat(properties.getName()).isEqualTo("my-producer"); + assertThat(properties.getTopicName()).isEqualTo("my-topic"); + assertThat(properties.getSendTimeout()).isEqualTo(Duration.ofSeconds(2)); + assertThat(properties.getMessageRoutingMode()).isEqualTo(MessageRoutingMode.CustomPartition); + assertThat(properties.getHashingScheme()).isEqualTo(HashingScheme.Murmur3_32Hash); + assertThat(properties.isBatchingEnabled()).isFalse(); + assertThat(properties.isChunkingEnabled()).isTrue(); + assertThat(properties.getCompressionType()).isEqualTo(CompressionType.LZ4); + assertThat(properties.getAccessMode()).isEqualTo(ProducerAccessMode.Exclusive); + assertThat(properties.getCache().getExpireAfterAccess()).isEqualTo(Duration.ofSeconds(2)); + assertThat(properties.getCache().getMaximumSize()).isEqualTo(3); + assertThat(properties.getCache().getInitialCapacity()).isEqualTo(5); + } + + } + + @Nested + class ConsumerPropertiesTests { + + @Test + void bind() { + Map map = new HashMap<>(); + map.put("spring.pulsar.consumer.name", "my-consumer"); + map.put("spring.pulsar.consumer.subscription.initial-position", "earliest"); + map.put("spring.pulsar.consumer.subscription.mode", "nondurable"); + map.put("spring.pulsar.consumer.subscription.name", "my-subscription"); + map.put("spring.pulsar.consumer.subscription.topics-mode", "all-topics"); + map.put("spring.pulsar.consumer.subscription.type", "shared"); + map.put("spring.pulsar.consumer.topics[0]", "my-topic"); + map.put("spring.pulsar.consumer.topics-pattern", "my-pattern"); + map.put("spring.pulsar.consumer.priority-level", "8"); + map.put("spring.pulsar.consumer.read-compacted", "true"); + map.put("spring.pulsar.consumer.dead-letter-policy.max-redeliver-count", "4"); + map.put("spring.pulsar.consumer.dead-letter-policy.retry-letter-topic", "my-retry-topic"); + map.put("spring.pulsar.consumer.dead-letter-policy.dead-letter-topic", "my-dlt-topic"); + map.put("spring.pulsar.consumer.dead-letter-policy.initial-subscription-name", "my-initial-subscription"); + map.put("spring.pulsar.consumer.retry-enable", "true"); + PulsarProperties.Consumer properties = bindPropeties(map).getConsumer(); + assertThat(properties.getName()).isEqualTo("my-consumer"); + assertThat(properties.getSubscription()).satisfies((subscription) -> { + assertThat(subscription.getName()).isEqualTo("my-subscription"); + assertThat(subscription.getType()).isEqualTo(SubscriptionType.Shared); + assertThat(subscription.getMode()).isEqualTo(SubscriptionMode.NonDurable); + assertThat(subscription.getInitialPosition()).isEqualTo(SubscriptionInitialPosition.Earliest); + assertThat(subscription.getTopicsMode()).isEqualTo(RegexSubscriptionMode.AllTopics); + }); + assertThat(properties.getTopics()).containsExactly("my-topic"); + assertThat(properties.getTopicsPattern().toString()).isEqualTo("my-pattern"); + assertThat(properties.getPriorityLevel()).isEqualTo(8); + assertThat(properties.isReadCompacted()).isTrue(); + assertThat(properties.getDeadLetterPolicy()).satisfies((policy) -> { + assertThat(policy.getMaxRedeliverCount()).isEqualTo(4); + assertThat(policy.getRetryLetterTopic()).isEqualTo("my-retry-topic"); + assertThat(policy.getDeadLetterTopic()).isEqualTo("my-dlt-topic"); + assertThat(policy.getInitialSubscriptionName()).isEqualTo("my-initial-subscription"); + }); + assertThat(properties.isRetryEnable()).isTrue(); + } + + } + + @Nested + class ListenerProperties { + + @Test + void bind() { + Map map = new HashMap<>(); + map.put("spring.pulsar.listener.schema-type", "avro"); + map.put("spring.pulsar.listener.observation-enabled", "false"); + PulsarProperties.Listener properties = bindPropeties(map).getListener(); + assertThat(properties.getSchemaType()).isEqualTo(SchemaType.AVRO); + assertThat(properties.isObservationEnabled()).isFalse(); + } + + } + + @Nested + class ReaderProperties { + + @Test + void bind() { + Map map = new HashMap<>(); + map.put("spring.pulsar.reader.name", "my-reader"); + map.put("spring.pulsar.reader.topics", "my-topic"); + map.put("spring.pulsar.reader.subscription-name", "my-subscription"); + map.put("spring.pulsar.reader.subscription-role-prefix", "sub-role"); + map.put("spring.pulsar.reader.read-compacted", "true"); + PulsarProperties.Reader properties = bindPropeties(map).getReader(); + assertThat(properties.getName()).isEqualTo("my-reader"); + assertThat(properties.getTopics()).containsExactly("my-topic"); + assertThat(properties.getSubscriptionName()).isEqualTo("my-subscription"); + assertThat(properties.getSubscriptionRolePrefix()).isEqualTo("sub-role"); + assertThat(properties.isReadCompacted()).isTrue(); + } + + } + + @Nested + class TemplateProperties { + + @Test + void bind() { + Map map = new HashMap<>(); + map.put("spring.pulsar.template.observations-enabled", "false"); + PulsarProperties.Template properties = bindPropeties(map).getTemplate(); + assertThat(properties.isObservationsEnabled()).isFalse(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarReactiveAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarReactiveAutoConfigurationTests.java new file mode 100644 index 000000000000..4f3ab011ea2e --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarReactiveAutoConfigurationTests.java @@ -0,0 +1,473 @@ +/* + * Copyright 2012-2023 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.pulsar; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import com.github.benmanes.caffeine.cache.Caffeine; +import org.apache.pulsar.client.api.PulsarClient; +import org.apache.pulsar.common.schema.SchemaType; +import org.apache.pulsar.reactive.client.adapter.ProducerCacheProvider; +import org.apache.pulsar.reactive.client.api.ReactiveMessageConsumerBuilder; +import org.apache.pulsar.reactive.client.api.ReactiveMessageReaderBuilder; +import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderBuilder; +import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderCache; +import org.apache.pulsar.reactive.client.api.ReactivePulsarClient; +import org.apache.pulsar.reactive.client.producercache.CaffeineShadedProducerCacheProvider; +import org.assertj.core.api.AbstractObjectAssert; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.core.annotation.Order; +import org.springframework.pulsar.core.DefaultSchemaResolver; +import org.springframework.pulsar.core.DefaultTopicResolver; +import org.springframework.pulsar.core.PulsarAdministration; +import org.springframework.pulsar.core.SchemaResolver; +import org.springframework.pulsar.core.TopicResolver; +import org.springframework.pulsar.reactive.config.DefaultReactivePulsarListenerContainerFactory; +import org.springframework.pulsar.reactive.config.ReactivePulsarListenerContainerFactory; +import org.springframework.pulsar.reactive.config.ReactivePulsarListenerEndpointRegistry; +import org.springframework.pulsar.reactive.config.annotation.ReactivePulsarBootstrapConfiguration; +import org.springframework.pulsar.reactive.config.annotation.ReactivePulsarListenerAnnotationBeanPostProcessor; +import org.springframework.pulsar.reactive.core.DefaultReactivePulsarConsumerFactory; +import org.springframework.pulsar.reactive.core.DefaultReactivePulsarReaderFactory; +import org.springframework.pulsar.reactive.core.DefaultReactivePulsarSenderFactory; +import org.springframework.pulsar.reactive.core.ReactiveMessageConsumerBuilderCustomizer; +import org.springframework.pulsar.reactive.core.ReactiveMessageReaderBuilderCustomizer; +import org.springframework.pulsar.reactive.core.ReactiveMessageSenderBuilderCustomizer; +import org.springframework.pulsar.reactive.core.ReactivePulsarConsumerFactory; +import org.springframework.pulsar.reactive.core.ReactivePulsarReaderFactory; +import org.springframework.pulsar.reactive.core.ReactivePulsarSenderFactory; +import org.springframework.pulsar.reactive.core.ReactivePulsarTemplate; +import org.springframework.pulsar.reactive.listener.ReactivePulsarContainerProperties; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link PulsarReactiveAutoConfiguration}. + * + * @author Chris Bono + * @author Christophe Bornet + * @author Phillip Webb + */ +class PulsarReactiveAutoConfigurationTests { + + private static final String INTERNAL_PULSAR_LISTENER_ANNOTATION_PROCESSOR = "org.springframework.pulsar.config.internalReactivePulsarListenerAnnotationProcessor"; + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(PulsarReactiveAutoConfiguration.class)) + .withBean(PulsarClient.class, () -> mock(PulsarClient.class)); + + @Test + void whenPulsarNotOnClasspathAutoConfigurationIsSkipped() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(PulsarReactiveAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(PulsarClient.class)) + .run((context) -> assertThat(context).doesNotHaveBean(PulsarReactiveAutoConfiguration.class)); + } + + @Test + void whenReactivePulsarNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(ReactivePulsarClient.class)) + .run((context) -> assertThat(context).doesNotHaveBean(PulsarReactiveAutoConfiguration.class)); + } + + @Test + void whenReactiveSpringPulsarNotOnClasspathAutoConfigurationIsSkipped() { + this.contextRunner.withClassLoader(new FilteredClassLoader(ReactivePulsarTemplate.class)) + .run((context) -> assertThat(context).doesNotHaveBean(PulsarReactiveAutoConfiguration.class)); + } + + @Test + void whenCustomPulsarListenerAnnotationProcessorDefinedAutoConfigurationIsSkipped() { + this.contextRunner.withBean(INTERNAL_PULSAR_LISTENER_ANNOTATION_PROCESSOR, String.class, () -> "bean") + .run((context) -> assertThat(context).doesNotHaveBean(ReactivePulsarBootstrapConfiguration.class)); + } + + @Test + void autoConfiguresBeans() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(PulsarConfiguration.class) + .hasSingleBean(PulsarClient.class) + .hasSingleBean(PulsarAdministration.class) + .hasSingleBean(DefaultSchemaResolver.class) + .hasSingleBean(DefaultTopicResolver.class) + .hasSingleBean(ReactivePulsarClient.class) + .hasSingleBean(CaffeineShadedProducerCacheProvider.class) + .hasSingleBean(ReactiveMessageSenderCache.class) + .hasSingleBean(DefaultReactivePulsarSenderFactory.class) + .hasSingleBean(ReactivePulsarTemplate.class) + .hasSingleBean(DefaultReactivePulsarConsumerFactory.class) + .hasSingleBean(DefaultReactivePulsarListenerContainerFactory.class) + .hasSingleBean(ReactivePulsarListenerAnnotationBeanPostProcessor.class) + .hasSingleBean(ReactivePulsarListenerEndpointRegistry.class)); + } + + @Test + @SuppressWarnings("rawtypes") + void injectsExpectedBeansIntoReactivePulsarClient() { + this.contextRunner.run((context) -> { + PulsarClient pulsarClient = context.getBean(PulsarClient.class); + assertThat(context).hasNotFailed() + .getBean(ReactivePulsarClient.class) + .extracting("reactivePulsarResourceAdapter") + .extracting("pulsarClientSupplier", InstanceOfAssertFactories.type(Supplier.class)) + .extracting(Supplier::get) + .isSameAs(pulsarClient); + }); + } + + @ParameterizedTest + @ValueSource(classes = { ReactivePulsarClient.class, ProducerCacheProvider.class, ReactiveMessageSenderCache.class, + ReactivePulsarSenderFactory.class, ReactivePulsarConsumerFactory.class, ReactivePulsarReaderFactory.class, + ReactivePulsarTemplate.class }) + void whenHasUserDefinedBeanDoesNotAutoConfigureBean(Class beanClass) { + T bean = mock(beanClass); + this.contextRunner.withBean(beanClass.getName(), beanClass, () -> bean) + .run((context) -> assertThat(context).getBean(beanClass).isSameAs(bean)); + } + + @Nested + class SenderFactoryTests { + + private final ApplicationContextRunner contextRunner = PulsarReactiveAutoConfigurationTests.this.contextRunner; + + @Test + void injectsExpectedBeans() { + ReactivePulsarClient client = mock(ReactivePulsarClient.class); + ReactiveMessageSenderCache cache = mock(ReactiveMessageSenderCache.class); + this.contextRunner.withPropertyValues("spring.pulsar.producer.topic-name=test-topic") + .withBean("customReactivePulsarClient", ReactivePulsarClient.class, () -> client) + .withBean("customReactiveMessageSenderCache", ReactiveMessageSenderCache.class, () -> cache) + .run((context) -> { + DefaultReactivePulsarSenderFactory senderFactory = context + .getBean(DefaultReactivePulsarSenderFactory.class); + assertThat(senderFactory) + .extracting("reactivePulsarClient", InstanceOfAssertFactories.type(ReactivePulsarClient.class)) + .isSameAs(client); + assertThat(senderFactory) + .extracting("reactiveMessageSenderCache", + InstanceOfAssertFactories.type(ReactiveMessageSenderCache.class)) + .isSameAs(cache); + assertThat(senderFactory) + .extracting("topicResolver", InstanceOfAssertFactories.type(TopicResolver.class)) + .isSameAs(context.getBean(TopicResolver.class)); + }); + } + + @Test + void injectsExpectedBeansIntoReactiveMessageSenderCache() { + ProducerCacheProvider provider = mock(ProducerCacheProvider.class); + this.contextRunner.withBean("customProducerCacheProvider", ProducerCacheProvider.class, () -> provider) + .run((context) -> assertThat(context).getBean(ReactiveMessageSenderCache.class) + .extracting("cacheProvider", InstanceOfAssertFactories.type(ProducerCacheProvider.class)) + .isSameAs(provider)); + } + + @Test + void whenHasUserDefinedCustomizersAppliesInCorrectOrder() { + this.contextRunner.withPropertyValues("spring.pulsar.producer.name=fromPropsCustomizer") + .withUserConfiguration(ReactiveMessageSenderBuilderCustomizerConfig.class) + .run((context) -> { + DefaultReactivePulsarSenderFactory producerFactory = context + .getBean(DefaultReactivePulsarSenderFactory.class); + Customizers, ReactiveMessageSenderBuilder> customizers = Customizers + .of(ReactiveMessageSenderBuilder.class, ReactiveMessageSenderBuilderCustomizer::customize); + assertThat(customizers.fromField(producerFactory, "defaultConfigCustomizers")).callsInOrder( + ReactiveMessageSenderBuilder::producerName, "fromPropsCustomizer", "fromCustomizer1", + "fromCustomizer2"); + }); + } + + @TestConfiguration(proxyBeanMethods = false) + static class ReactiveMessageSenderBuilderCustomizerConfig { + + @Bean + @Order(200) + ReactiveMessageSenderBuilderCustomizer customizerFoo() { + return (builder) -> builder.producerName("fromCustomizer2"); + } + + @Bean + @Order(100) + ReactiveMessageSenderBuilderCustomizer customizerBar() { + return (builder) -> builder.producerName("fromCustomizer1"); + } + + } + + } + + @Nested + class TemplateTests { + + private final ApplicationContextRunner contextRunner = PulsarReactiveAutoConfigurationTests.this.contextRunner; + + @Test + @SuppressWarnings("rawtypes") + void injectsExpectedBeans() { + ReactivePulsarSenderFactory senderFactory = mock(ReactivePulsarSenderFactory.class); + SchemaResolver schemaResolver = mock(SchemaResolver.class); + this.contextRunner + .withBean("customReactivePulsarSenderFactory", ReactivePulsarSenderFactory.class, () -> senderFactory) + .withBean("schemaResolver", SchemaResolver.class, () -> schemaResolver) + .run((context) -> assertThat(context).getBean(ReactivePulsarTemplate.class).satisfies((template) -> { + assertThat(template).extracting("reactiveMessageSenderFactory").isSameAs(senderFactory); + assertThat(template).extracting("schemaResolver").isSameAs(schemaResolver); + })); + } + + } + + @Nested + class ConsumerFactoryTests { + + private final ApplicationContextRunner contextRunner = PulsarReactiveAutoConfigurationTests.this.contextRunner; + + @Test + void injectsExpectedBeans() { + ReactivePulsarClient client = mock(ReactivePulsarClient.class); + this.contextRunner.withBean("customReactivePulsarClient", ReactivePulsarClient.class, () -> client) + .run((context) -> { + ReactivePulsarConsumerFactory consumerFactory = context + .getBean(DefaultReactivePulsarConsumerFactory.class); + assertThat(consumerFactory) + .extracting("reactivePulsarClient", InstanceOfAssertFactories.type(ReactivePulsarClient.class)) + .isSameAs(client); + }); + } + + @Test + void whenHasUserDefinedCustomizersAppliesInCorrectOrder() { + this.contextRunner.withPropertyValues("spring.pulsar.consumer.name=fromPropsCustomizer") + .withUserConfiguration(ReactiveMessageConsumerBuilderCustomizerConfig.class) + .run((context) -> { + DefaultReactivePulsarConsumerFactory consumerFactory = context + .getBean(DefaultReactivePulsarConsumerFactory.class); + Customizers, ReactiveMessageConsumerBuilder> customizers = Customizers + .of(ReactiveMessageConsumerBuilder.class, ReactiveMessageConsumerBuilderCustomizer::customize); + assertThat(customizers.fromField(consumerFactory, "defaultConfigCustomizers")).callsInOrder( + ReactiveMessageConsumerBuilder::consumerName, "fromPropsCustomizer", "fromCustomizer1", + "fromCustomizer2"); + }); + } + + @TestConfiguration(proxyBeanMethods = false) + static class ReactiveMessageConsumerBuilderCustomizerConfig { + + @Bean + @Order(200) + ReactiveMessageConsumerBuilderCustomizer customizerFoo() { + return (builder) -> builder.consumerName("fromCustomizer2"); + } + + @Bean + @Order(100) + ReactiveMessageConsumerBuilderCustomizer customizerBar() { + return (builder) -> builder.consumerName("fromCustomizer1"); + } + + } + + } + + @Nested + class ListenerTests { + + private final ApplicationContextRunner contextRunner = PulsarReactiveAutoConfigurationTests.this.contextRunner; + + @Test + void whenHasUserDefinedBeanDoesNotAutoConfigureBean() { + ReactivePulsarListenerContainerFactory listenerContainerFactory = mock( + ReactivePulsarListenerContainerFactory.class); + this.contextRunner + .withBean("reactivePulsarListenerContainerFactory", ReactivePulsarListenerContainerFactory.class, + () -> listenerContainerFactory) + .run((context) -> assertThat(context).getBean(ReactivePulsarListenerContainerFactory.class) + .isSameAs(listenerContainerFactory)); + } + + @Test + void whenHasUserDefinedReactivePulsarListenerAnnotationBeanPostProcessorDoesNotAutoConfigureBean() { + ReactivePulsarListenerAnnotationBeanPostProcessor listenerAnnotationBeanPostProcessor = mock( + ReactivePulsarListenerAnnotationBeanPostProcessor.class); + this.contextRunner.withBean(INTERNAL_PULSAR_LISTENER_ANNOTATION_PROCESSOR, + ReactivePulsarListenerAnnotationBeanPostProcessor.class, () -> listenerAnnotationBeanPostProcessor) + .run((context) -> assertThat(context).getBean(ReactivePulsarListenerAnnotationBeanPostProcessor.class) + .isSameAs(listenerAnnotationBeanPostProcessor)); + } + + @Test + void whenHasCustomProperties() { + List properties = new ArrayList<>(); + properties.add("spring.pulsar.listener.schema-type=avro"); + this.contextRunner.withPropertyValues(properties.toArray(String[]::new)).run((context) -> { + DefaultReactivePulsarListenerContainerFactory factory = context + .getBean(DefaultReactivePulsarListenerContainerFactory.class); + assertThat(factory.getContainerProperties().getSchemaType()).isEqualTo(SchemaType.AVRO); + }); + } + + @Test + void injectsExpectedBeans() { + ReactivePulsarConsumerFactory consumerFactory = mock(ReactivePulsarConsumerFactory.class); + SchemaResolver schemaResolver = mock(SchemaResolver.class); + this.contextRunner + .withBean("customReactivePulsarConsumerFactory", ReactivePulsarConsumerFactory.class, + () -> consumerFactory) + .withBean("schemaResolver", SchemaResolver.class, () -> schemaResolver) + .run((context) -> { + DefaultReactivePulsarListenerContainerFactory containerFactory = context + .getBean(DefaultReactivePulsarListenerContainerFactory.class); + assertThat(containerFactory).extracting("consumerFactory").isSameAs(consumerFactory); + assertThat(containerFactory) + .extracting(DefaultReactivePulsarListenerContainerFactory::getContainerProperties) + .extracting(ReactivePulsarContainerProperties::getSchemaResolver) + .isSameAs(schemaResolver); + }); + } + + } + + @Nested + class ReaderFactoryTests { + + private final ApplicationContextRunner contextRunner = PulsarReactiveAutoConfigurationTests.this.contextRunner; + + @Test + void injectsExpectedBeans() { + ReactivePulsarClient client = mock(ReactivePulsarClient.class); + this.contextRunner.withPropertyValues("spring.pulsar.reader.name=test-reader") + .withBean("customReactivePulsarClient", ReactivePulsarClient.class, () -> client) + .run((context) -> { + DefaultReactivePulsarReaderFactory readerFactory = context + .getBean(DefaultReactivePulsarReaderFactory.class); + assertThat(readerFactory) + .extracting("reactivePulsarClient", InstanceOfAssertFactories.type(ReactivePulsarClient.class)) + .isSameAs(client); + }); + } + + @Test + void whenHasUserDefinedCustomizersAppliesInCorrectOrder() { + this.contextRunner.withPropertyValues("spring.pulsar.reader.name=fromPropsCustomizer") + .withUserConfiguration(ReactiveMessageReaderBuilderCustomizerConfig.class) + .run((context) -> { + DefaultReactivePulsarReaderFactory readerFactory = context + .getBean(DefaultReactivePulsarReaderFactory.class); + Customizers, ReactiveMessageReaderBuilder> customizers = Customizers + .of(ReactiveMessageReaderBuilder.class, ReactiveMessageReaderBuilderCustomizer::customize); + assertThat(customizers.fromField(readerFactory, "defaultConfigCustomizers")).callsInOrder( + ReactiveMessageReaderBuilder::readerName, "fromPropsCustomizer", "fromCustomizer1", + "fromCustomizer2"); + }); + } + + @TestConfiguration(proxyBeanMethods = false) + static class ReactiveMessageReaderBuilderCustomizerConfig { + + @Bean + @Order(200) + ReactiveMessageReaderBuilderCustomizer customizerFoo() { + return (builder) -> builder.readerName("fromCustomizer2"); + } + + @Bean + @Order(100) + ReactiveMessageReaderBuilderCustomizer customizerBar() { + return (builder) -> builder.readerName("fromCustomizer1"); + } + + } + + } + + @Nested + class SenderCacheAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = PulsarReactiveAutoConfigurationTests.this.contextRunner; + + @Test + void whenNoPropertiesEnablesCaching() { + this.contextRunner.run(this::assertCaffeineProducerCacheProvider); + } + + @Test + void whenCachingEnabledEnablesCaching() { + this.contextRunner.withPropertyValues("spring.pulsar.producer.cache.enabled=true") + .run(this::assertCaffeineProducerCacheProvider); + } + + @Test + void whenCachingDisabledDoesNotEnableCaching() { + this.contextRunner.withPropertyValues("spring.pulsar.producer.cache.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(ProducerCacheProvider.class) + .doesNotHaveBean(ReactiveMessageSenderCache.class)); + } + + @Test + void whenCachingEnabledAndCaffeineNotOnClasspathStillUsesCaffeine() { + // The reactive client shades Caffeine - it should still be used + this.contextRunner.withClassLoader(new FilteredClassLoader(Caffeine.class)) + .withPropertyValues("spring.pulsar.producer.cache.enabled=true") + .run(this::assertCaffeineProducerCacheProvider); + } + + @Test + void whenCachingEnabledAndNoCacheProviderAvailable() { + // The reactive client uses a shaded caffeine cache provider as its internal + // cache + this.contextRunner.withClassLoader(new FilteredClassLoader(CaffeineShadedProducerCacheProvider.class)) + .withPropertyValues("spring.pulsar.producer.cache.enabled=true") + .run((context) -> assertThat(context).doesNotHaveBean(ProducerCacheProvider.class) + .getBean(ReactiveMessageSenderCache.class) + .extracting("cacheProvider") + .isExactlyInstanceOf(CaffeineShadedProducerCacheProvider.class)); + } + + @Test + void whenCustomCachingPropertiesCreatesConfiguredBean() { + this.contextRunner + .withPropertyValues("spring.pulsar.producer.cache.expire-after-access=100s", + "spring.pulsar.producer.cache.maximum-size=5150", + "spring.pulsar.producer.cache.initial-capacity=200") + .run((context) -> assertCaffeineProducerCacheProvider(context).extracting("cache.cache") + .hasFieldOrPropertyWithValue("expiresAfterAccessNanos", Duration.ofSeconds(100).toNanos()) + .hasFieldOrPropertyWithValue("maximum", 5150L)); + } + + private AbstractObjectAssert assertCaffeineProducerCacheProvider( + AssertableApplicationContext context) { + return assertThat(context).hasSingleBean(ReactiveMessageSenderCache.class) + .getBean(ProducerCacheProvider.class) + .isExactlyInstanceOf(CaffeineShadedProducerCacheProvider.class); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarReactivePropertiesMapperTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarReactivePropertiesMapperTests.java new file mode 100644 index 000000000000..df078b21a354 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarReactivePropertiesMapperTests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2012-2023 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.pulsar; + +import java.time.Duration; +import java.util.List; +import java.util.regex.Pattern; + +import org.apache.pulsar.client.api.CompressionType; +import org.apache.pulsar.client.api.DeadLetterPolicy; +import org.apache.pulsar.client.api.HashingScheme; +import org.apache.pulsar.client.api.MessageRoutingMode; +import org.apache.pulsar.client.api.ProducerAccessMode; +import org.apache.pulsar.client.api.RegexSubscriptionMode; +import org.apache.pulsar.client.api.SubscriptionInitialPosition; +import org.apache.pulsar.client.api.SubscriptionMode; +import org.apache.pulsar.client.api.SubscriptionType; +import org.apache.pulsar.common.schema.SchemaType; +import org.apache.pulsar.reactive.client.api.ReactiveMessageConsumerBuilder; +import org.apache.pulsar.reactive.client.api.ReactiveMessageReaderBuilder; +import org.apache.pulsar.reactive.client.api.ReactiveMessageSenderBuilder; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.pulsar.PulsarProperties.Consumer; +import org.springframework.boot.autoconfigure.pulsar.PulsarProperties.Consumer.Subscription; +import org.springframework.pulsar.reactive.listener.ReactivePulsarContainerProperties; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link PulsarReactivePropertiesMapper}. + * + * @author Chris Bono + * @author Phillip Webb + */ +class PulsarReactivePropertiesMapperTests { + + @Test + @SuppressWarnings("unchecked") + void customizeMessageSenderBuilder() { + PulsarProperties properties = new PulsarProperties(); + properties.getProducer().setName("name"); + properties.getProducer().setTopicName("topicname"); + properties.getProducer().setSendTimeout(Duration.ofSeconds(1)); + properties.getProducer().setMessageRoutingMode(MessageRoutingMode.RoundRobinPartition); + properties.getProducer().setHashingScheme(HashingScheme.JavaStringHash); + properties.getProducer().setBatchingEnabled(false); + properties.getProducer().setChunkingEnabled(true); + properties.getProducer().setCompressionType(CompressionType.SNAPPY); + properties.getProducer().setAccessMode(ProducerAccessMode.Exclusive); + ReactiveMessageSenderBuilder builder = mock(ReactiveMessageSenderBuilder.class); + new PulsarReactivePropertiesMapper(properties).customizeMessageSenderBuilder(builder); + then(builder).should().producerName("name"); + then(builder).should().topic("topicname"); + then(builder).should().sendTimeout(Duration.ofSeconds(1)); + then(builder).should().messageRoutingMode(MessageRoutingMode.RoundRobinPartition); + then(builder).should().hashingScheme(HashingScheme.JavaStringHash); + then(builder).should().batchingEnabled(false); + then(builder).should().chunkingEnabled(true); + then(builder).should().compressionType(CompressionType.SNAPPY); + then(builder).should().accessMode(ProducerAccessMode.Exclusive); + } + + @Test + @SuppressWarnings("unchecked") + void customizeMessageConsumerBuilder() { + PulsarProperties properties = new PulsarProperties(); + List topics = List.of("mytopic"); + Pattern topisPattern = Pattern.compile("my-pattern"); + properties.getConsumer().setName("name"); + properties.getConsumer().setTopics(topics); + properties.getConsumer().setTopicsPattern(topisPattern); + properties.getConsumer().setPriorityLevel(123); + properties.getConsumer().setReadCompacted(true); + Consumer.DeadLetterPolicy deadLetterPolicy = new Consumer.DeadLetterPolicy(); + deadLetterPolicy.setDeadLetterTopic("my-dlt"); + deadLetterPolicy.setMaxRedeliverCount(1); + properties.getConsumer().setDeadLetterPolicy(deadLetterPolicy); + properties.getConsumer().setRetryEnable(false); + Subscription subscriptionProperties = properties.getConsumer().getSubscription(); + subscriptionProperties.setName("subname"); + subscriptionProperties.setInitialPosition(SubscriptionInitialPosition.Earliest); + subscriptionProperties.setMode(SubscriptionMode.NonDurable); + subscriptionProperties.setTopicsMode(RegexSubscriptionMode.NonPersistentOnly); + subscriptionProperties.setType(SubscriptionType.Key_Shared); + ReactiveMessageConsumerBuilder builder = mock(ReactiveMessageConsumerBuilder.class); + new PulsarReactivePropertiesMapper(properties).customizeMessageConsumerBuilder(builder); + then(builder).should().consumerName("name"); + then(builder).should().topics(topics); + then(builder).should().topicsPattern(topisPattern); + then(builder).should().priorityLevel(123); + then(builder).should().readCompacted(true); + then(builder).should().deadLetterPolicy(new DeadLetterPolicy(1, null, "my-dlt", null)); + then(builder).should().retryLetterTopicEnable(false); + then(builder).should().subscriptionName("subname"); + then(builder).should().subscriptionInitialPosition(SubscriptionInitialPosition.Earliest); + then(builder).should().subscriptionMode(SubscriptionMode.NonDurable); + then(builder).should().topicsPatternSubscriptionMode(RegexSubscriptionMode.NonPersistentOnly); + then(builder).should().subscriptionType(SubscriptionType.Key_Shared); + } + + @Test + void customizeContainerProperties() { + PulsarProperties properties = new PulsarProperties(); + properties.getConsumer().getSubscription().setType(SubscriptionType.Shared); + properties.getListener().setSchemaType(SchemaType.AVRO); + ReactivePulsarContainerProperties containerProperties = new ReactivePulsarContainerProperties<>(); + new PulsarReactivePropertiesMapper(properties).customizeContainerProperties(containerProperties); + assertThat(containerProperties.getSubscriptionType()).isEqualTo(SubscriptionType.Shared); + assertThat(containerProperties.getSchemaType()).isEqualTo(SchemaType.AVRO); + } + + @Test + @SuppressWarnings("unchecked") + void customizeMessageReaderBuilder() { + List topics = List.of("my-topic"); + PulsarProperties properties = new PulsarProperties(); + properties.getReader().setName("name"); + properties.getReader().setTopics(topics); + properties.getReader().setSubscriptionName("subname"); + properties.getReader().setSubscriptionRolePrefix("srp"); + ReactiveMessageReaderBuilder builder = mock(ReactiveMessageReaderBuilder.class); + new PulsarReactivePropertiesMapper(properties).customizeMessageReaderBuilder(builder); + then(builder).should().readerName("name"); + then(builder).should().topics(topics); + then(builder).should().subscriptionName("subname"); + then(builder).should().generatedSubscriptionNamePrefix("srp"); + } + +} diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f1430172615a..9fd61d6a046c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1128,6 +1128,96 @@ bom { ] } } + library("Pulsar", "3.1.0") { + group("org.apache.pulsar") { + modules = [ + "bouncy-castle-bc", + "bouncy-castle-bcfips", + "pulsar-client-1x-base", + "pulsar-client-1x", + "pulsar-client-2x-shaded", + "pulsar-client-admin-api", + "pulsar-client-admin-original", + "pulsar-client-admin", + "pulsar-client-all", + "pulsar-client-api", + "pulsar-client-auth-athenz", + "pulsar-client-auth-sasl", + "pulsar-client-messagecrypto-bc", + "pulsar-client-original", + "pulsar-client-tools-api", + "pulsar-client-tools", + "pulsar-client", + "pulsar-common", + "pulsar-config-validation", + "pulsar-functions-api", + "pulsar-functions-proto", + "pulsar-functions-utils", + "pulsar-io-aerospike", + "pulsar-io-alluxio", + "pulsar-io-aws", + "pulsar-io-batch-data-generator", + "pulsar-io-batch-discovery-triggerers", + "pulsar-io-canal", + "pulsar-io-cassandra", + "pulsar-io-common", + "pulsar-io-core", + "pulsar-io-data-generator", + "pulsar-io-debezium-core", + "pulsar-io-debezium-mongodb", + "pulsar-io-debezium-mssql", + "pulsar-io-debezium-mysql", + "pulsar-io-debezium-oracle", + "pulsar-io-debezium-postgres", + "pulsar-io-debezium", + "pulsar-io-dynamodb", + "pulsar-io-elastic-search", + "pulsar-io-file", + "pulsar-io-flume", + "pulsar-io-hbase", + "pulsar-io-hdfs2", + "pulsar-io-hdfs3", + "pulsar-io-http", + "pulsar-io-influxdb", + "pulsar-io-jdbc-clickhouse", + "pulsar-io-jdbc-core", + "pulsar-io-jdbc-mariadb", + "pulsar-io-jdbc-openmldb", + "pulsar-io-jdbc-postgres", + "pulsar-io-jdbc-sqlite", + "pulsar-io-jdbc", + "pulsar-io-kafka-connect-adaptor-nar", + "pulsar-io-kafka-connect-adaptor", + "pulsar-io-kafka", + "pulsar-io-kinesis", + "pulsar-io-mongo", + "pulsar-io-netty", + "pulsar-io-nsq", + "pulsar-io-rabbitmq", + "pulsar-io-redis", + "pulsar-io-solr", + "pulsar-io-twitter", + "pulsar-io", + "pulsar-metadata", + "pulsar-presto-connector-original", + "pulsar-presto-connector", + "pulsar-sql", + "pulsar-transaction-common", + "pulsar-websocket" + ] + } + } + library("Pulsar Reactive", "0.3.0") { + group("org.apache.pulsar") { + modules = [ + "pulsar-client-reactive-adapter", + "pulsar-client-reactive-api", + "pulsar-client-reactive-jackson", + "pulsar-client-reactive-producer-cache-caffeine-shaded", + "pulsar-client-reactive-producer-cache-caffeine" + ] + } + } library("Quartz", "2.3.2") { group("org.quartz-scheduler") { modules = [ @@ -1477,6 +1567,14 @@ bom { ] } } + library("Spring Pulsar", "1.0.0-SNAPSHOT") { + group("org.springframework.pulsar") { + modules = [ + "spring-pulsar", + "spring-pulsar-reactive" + ] + } + } library("Spring RESTDocs", "3.0.0") { considerSnapshots() group("org.springframework.restdocs") { diff --git a/spring-boot-project/spring-boot-docs/build.gradle b/spring-boot-project/spring-boot-docs/build.gradle index 750de80a6e41..0a92048ce5ce 100644 --- a/spring-boot-project/spring-boot-docs/build.gradle +++ b/spring-boot-project/spring-boot-docs/build.gradle @@ -163,6 +163,8 @@ dependencies { implementation("org.springframework.graphql:spring-graphql-test") implementation("org.springframework.kafka:spring-kafka") implementation("org.springframework.kafka:spring-kafka-test") + implementation("org.springframework.pulsar:spring-pulsar") + implementation("org.springframework.pulsar:spring-pulsar-reactive") implementation("org.springframework.restdocs:spring-restdocs-mockmvc") implementation("org.springframework.restdocs:spring-restdocs-restassured") implementation("org.springframework.restdocs:spring-restdocs-webtestclient") @@ -336,6 +338,7 @@ tasks.withType(org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask) { "spring-graphql-version": versionConstraints["org.springframework.graphql:spring-graphql"], "spring-integration-version": versionConstraints["org.springframework.integration:spring-integration-core"], "spring-kafka-version": versionConstraints["org.springframework.kafka:spring-kafka"], + "spring-pulsar-version": versionConstraints["org.springframework.pulsar:spring-pulsar"], "spring-security-version": securityVersion, "spring-authorization-server-version": versionConstraints["org.springframework.security:spring-security-oauth2-authorization-server"], "spring-webservices-version": versionConstraints["org.springframework.ws:spring-ws-core"], diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc index f867c630e187..f8aab54bfd24 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc @@ -90,6 +90,7 @@ :spring-integration: https://spring.io/projects/spring-integration :spring-integration-docs: https://docs.spring.io/spring-integration/docs/{spring-integration-version}/reference/html/ :spring-kafka-docs: https://docs.spring.io/spring-kafka/docs/{spring-kafka-version}/reference/html/ +:spring-pulsar-docs: https://docs.spring.io/spring-pulsar/docs/{spring-pulsar-version}/reference/html/ :spring-restdocs: https://spring.io/projects/spring-restdocs :spring-security: https://spring.io/projects/spring-security :spring-security-docs: https://docs.spring.io/spring-security/reference/{spring-security-version} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/messaging.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/messaging.adoc index 51412fde0c9b..d6ced6c27794 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/messaging.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/messaging.adoc @@ -5,5 +5,6 @@ If your application uses any messaging protocol, see one or more of the followin * *JMS:* <> * *AMQP:* <> * *Kafka:* <> +* *Pulsar:* <> * *RSocket:* <> * *Spring Integration:* <> diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/ssl.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/ssl.adoc index 5f15b9720b89..759755c25aa7 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/ssl.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/ssl.adoc @@ -104,4 +104,3 @@ In addition, the `SslBundle` provides details about the key being used, the prot The following example shows retrieving an `SslBundle` and using it to create an `SSLContext`: include::code:MyComponent[] - diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/index.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/index.adoc index 3d6604e85b76..4966c197d1b7 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/index.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/index.adoc @@ -19,7 +19,7 @@ The reference documentation consists of the following sections: <> :: Servlet Web, Reactive Web, Embedded Container Support, Graceful Shutdown, and more. <> :: SQL and NOSQL data access. <> :: Caching, Quartz Scheduler, REST clients, Sending email, Spring Web Services, and more. -<> :: JMS, AMQP, Apache Kafka, RSocket, WebSocket, and Spring Integration. +<> :: JMS, AMQP, Apache Kafka, Apache Pulsar, RSocket, WebSocket, and Spring Integration. <> :: Efficient container images and Building container images with Dockerfiles and Cloud Native Buildpacks. <> :: Monitoring, Metrics, Auditing, and more. <> :: Deploying to the Cloud, and Installing as a Unix application. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging.adoc index 8b6a5ec6e626..12aca393d1a6 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging.adoc @@ -6,7 +6,7 @@ The Spring Framework provides extensive support for integrating with messaging s Spring AMQP provides a similar feature set for the Advanced Message Queuing Protocol. Spring Boot also provides auto-configuration options for `RabbitTemplate` and RabbitMQ. Spring WebSocket natively includes support for STOMP messaging, and Spring Boot has support for that through starters and a small amount of auto-configuration. -Spring Boot also has support for Apache Kafka. +Spring Boot also has support for Apache Kafka and Apache Pulsar. include::messaging/jms.adoc[] @@ -15,6 +15,8 @@ include::messaging/amqp.adoc[] include::messaging/kafka.adoc[] +include::messaging/pulsar.adoc[] + include::messaging/rsocket.adoc[] include::messaging/spring-integration.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/pulsar.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/pulsar.adoc new file mode 100644 index 000000000000..8e3f78ebc871 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/pulsar.adoc @@ -0,0 +1,201 @@ +[[messaging.pulsar]] +== Apache Pulsar Support +https://pulsar.apache.org/[Apache Pulsar] is supported by providing auto-configuration of the {spring-pulsar-docs}[Spring for Apache Pulsar] project. + +Spring Boot will auto-configure and register the classic (imperative) Spring Pulsar components when `org.springframework.pulsar:spring-pulsar` is on the classpath. +It will do the same for the reactive components when `org.springframework.pulsar:spring-pulsar-reactive` is on the classpath. + +There are `spring-boot-starter-pulsar` and `spring-boot-starter-pulsar-reactive` "`Starters`" for conveniently collecting the dependencies for imperative and reactive use, respectively. + + + +[[messaging.pulsar.connecting]] +=== Connecting to Pulsar +When you use the Pulsar starter, Spring Boot will auto-configure and register a `PulsarClient` bean. + +By default, the application tries to connect to a local Pulsar instance at `pulsar://localhost:6650`. +This can be adjusted by setting the configprop:spring.pulsar.client.service-url[] property to a different value. + +NOTE: The value must be a valid https://pulsar.apache.org/docs/client-libraries-java/#connection-urls[Pulsar Protocol] URL + +You can configure the client by specifying any of the `spring.pulsar.client.*` prefixed application properties. + +If you need more control over the configuration, consider registering one or more `PulsarClientBuilderCustomizer` beans. + + + +[[messaging.pulsar.connecting.auth]] +==== Authentication +To connect to a Pulsar cluster that requires authentication, you need to specify which authentication plugin to use by setting the `authPluginClassName` and any parameters required by the plugin. +You can set the parameters as a map of parameter names to parameter values. +The following example shows how to configure the `AuthenticationOAuth2` plugin. + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- +spring: + pulsar: + client: + authentication: + plugin-class-name: org.apache.pulsar.client.impl.auth.oauth2.AuthenticationOAuth2 + param: + issuerUrl: https://auth.server.cloud/ + privateKey: file:///Users/some-key.json + audience: urn:sn:acme:dev:my-instance +---- + +[NOTE] +==== +You need to ensure that names defined under `+spring.pulsar.client.authentication.param.*+` exactly match those expected by your auth plugin (which is typically camel cased). +Spring Boot will not attempt any kind of relaxed binding for these entries. + +For example, if you want to configure the issuer url for the `AuthenticationOAuth2` auth plugin you must use `+spring.pulsar.client.authentication.param.issuerUrl+`. +If you use other forms, such as `issuerurl` or `issuer-url`, the setting will not be applied to the plugin. +==== + +For complete details on the client and authentication see the Spring Pulsar {spring-pulsar-docs}#pulsar-client[reference documentation]. + + + +[[messaging.pulsar.connecting-reactive]] +=== Connecting to Pulsar Reactively +When the Reactive auto-configuration is activated, Spring Boot will auto-configure and register a `ReactivePulsarClient` bean. + +The `ReactivePulsarClient` adapts an instance of the previously described `PulsarClient`. +Therefore, follow the previous section to configure the `PulsarClient` used by the `ReactivePulsarClient`. + + + +[[messaging.pulsar.admin]] +=== Connecting to Pulsar Administration +Spring Pulsar's `PulsarAdministration` client is also auto-configured. + +By default, the application tries to connect to a local Pulsar instance at `\http://localhost:8080`. +This can be adjusted by setting the configprop:spring.pulsar.admin.service-url[] property to a different value in the form `(http|https)://:`. + +If you need more control over the configuration, consider registering one or more `PulsarAdminBuilderCustomizer` beans. + + +[[messaging.pulsar.admin.auth]] +==== Authentication +When accessing a Pulsar cluster that requires authentication, the admin client requires the same security configuration as the regular Pulsar client. +You can use the aforementioned <> by replacing `spring.pulsar.client.authentication` with `spring.pulsar.admin.authentication`. + +TIP: To create a topic on startup, add a bean of type `PulsarTopic`. +If the topic already exists, the bean is ignored. + + + +[[messaging.pulsar.sending]] +=== Sending a Message +Spring's `PulsarTemplate` is auto-configured, and you can use it to send messages, as shown in the following example: + +include::code:MyBean[] + +The `PulsarTemplate` relies on a `PulsarProducerFactory` to create the underlying Pulsar producer. +Spring Boot auto-configuration also provides this producer factory, which by default, caches the producers that it creates. +You can configure the producer factory and cache settings by specifying any of the `spring.pulsar.producer.\*` and `spring.pulsar.producer.cache.*` prefixed application properties. + +If you need more control over the producer factory configuration, consider registering one or more `ProducerBuilderCustomizer` beans. +These customizers are applied to all created producers. +You can also pass in a `ProducerBuilderCustomizer` when sending a message to only affect the current producer. + +If you need more control over the message being sent, you can pass in a `TypedMessageBuilderCustomizer` when sending a message. + + + +[[messaging.pulsar.sending-reactive]] +=== Sending a Message Reactively +When the Reactive auto-configuration is activated, Spring's `ReactivePulsarTemplate` is auto-configured, and you can use it to send messages, as shown in the following example: + +include::code:MyBean[] + +The `ReactivePulsarTemplate` relies on a `ReactivePulsarSenderFactory` to actually create the underlying sender. +Spring Boot auto-configuration also provides this sender factory, which by default, caches the producers that it creates. +You can configure the sender factory and cache settings by specifying any of the `spring.pulsar.producer.\*` and `spring.pulsar.producer.cache.*` prefixed application properties. + +If you need more control over the sender factory configuration, consider registering one or more `ReactiveMessageSenderBuilderCustomizer` beans. +These customizers are applied to all created senders. +You can also pass in a `ReactiveMessageSenderBuilderCustomizer` when sending a message to only affect the current sender. + +If you need more control over the message being sent, you can pass in a `MessageSpecBuilderCustomizer` when sending a message. + + + +[[messaging.pulsar.receiving]] +=== Receiving a Message +When the Apache Pulsar infrastructure is present, any bean can be annotated with `@PulsarListener` to create a listener endpoint. +The following component creates a listener endpoint on the `someTopic` topic: + +include::code:MyBean[] + +Spring Boot auto-configuration provides all the components necessary for `PulsarListener`, such as the `PulsarListenerContainerFactory` and the consumer factory it uses to construct the underlying Pulsar consumers. +You can configure these components by specifying any of the `spring.pulsar.listener.\*` and `spring.pulsar.consumer.*` prefixed application properties. + +If you need more control over the consumer factory configuration, consider registering one or more `ConsumerBuilderCustomizer` beans. +These customizers are applied to all consumers created by the factory, and therefore all `@PulsarListener` instances. +You can also customize a single listener by setting the `consumerCustomizer` attribute of the `@PulsarListener` annotation. + + + +[[messaging.pulsar.receiving-reactive]] +=== Receiving a Message Reactively +When the Apache Pulsar infrastructure is present and the Reactive auto-configuration is activated, any bean can be annotated with `@ReactivePulsarListener` to create a reactive listener endpoint. +The following component creates a reactive listener endpoint on the `someTopic` topic: + +include::code:MyBean[] + +Spring Boot auto-configuration provides all the components necessary for `ReactivePulsarListener`, such as the `ReactivePulsarListenerContainerFactory` and the consumer factory it uses to construct the underlying reactive Pulsar consumers. +You can configure these components by specifying any of the `spring.pulsar.listener.*` and `spring.pulsar.consumer.*` prefixed application properties. + +If you need more control over the consumer factory configuration, consider registering one or more `ReactiveMessageConsumerBuilderCustomizer` beans. +These customizers are applied to all consumers created by the factory, and therefore all `@ReactivePulsarListener` instances. +You can also customize a single listener by setting the `consumerCustomizer` attribute of the `@ReactivePulsarListener` annotation. + + + +[[messaging.pulsar.reading]] +=== Reading a Message +The Pulsar reader interface enables applications to manually manage cursors. +When you use a reader to connect to a topic you need to specify which message the reader begins reading from when it connects to a topic. + +When the Apache Pulsar infrastructure is present, any bean can be annotated with `@PulsarReader` to consume messages using a reader. +The following component creates a reader endpoint that starts reading messages from the beginning of the `someTopic` topic: + +include::code:MyBean[] + +The `@PulsarReader` relies on a `PulsarReaderFactory` to create the underlying Pulsar reader. +Spring Boot auto-configuration provides this reader factory which can be customized by setting any of the `spring.pulsar.reader.*` prefixed application properties. + +If you need more control over the reader factory configuration, consider registering one or more `ReaderBuilderCustomizer` beans. +These customizers are applied to all readers created by the factory, and therefore all `@PulsarReader` instances. +You can also customize a single listener by setting the `readerCustomizer` attribute of the `@PulsarReader` annotation. + + + +[[messaging.pulsar.reading-reactive]] +=== Reading a Message Reactively +When the Apache Pulsar infrastructure is present and the Reactive auto-configuration is activated, Spring's `ReactivePulsarReaderFactory` is provided, and you can use it to create a reader in order to read messages in a reactive fashion. +The following component creates a reader using the provided factory and reads a single message from 5 minutes ago from the `someTopic` topic: + +include::code:MyBean[] + +Spring Boot auto-configuration provides this reader factory which can be customized by setting any of the `spring.pulsar.reader.*` prefixed application properties. + +If you need more control over the reader factory configuration, consider passing in one or more `ReactiveMessageReaderBuilderCustomizer` instances when using the factory to create a reader. + +If you need more control over the reader factory configuration, consider registering one or more `ReactiveMessageReaderBuilderCustomizer` beans. +These customizers are applied to all created readers. +You can also pass one or more `ReactiveMessageReaderBuilderCustomizer` when creating a reader to only apply the customizations to the created reader. + +TIP: For more details on any of the above components and to discover other available features, see the Spring for Apache Pulsar {spring-pulsar-docs}[reference documentation]. + + + +[[messaging.pulsar.additional-properties]] +=== Additional Pulsar Properties +The properties supported by auto-configuration are shown in the <> section of the Appendix. +Note that, for the most part, these properties (hyphenated or camelCase) map directly to the Apache Pulsar configuration properties. +See the Apache Pulsar documentation for details. + +Only a subset of the properties supported by Pulsar are available directly through the `PulsarProperties` class. +If you wish to tune the auto-configured components with additional properties that are not directly supported, you can use the customizer supported by each aforementioned component. diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/reading/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/reading/MyBean.java new file mode 100644 index 000000000000..f13cf6ec5451 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/reading/MyBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2023-2023 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.docs.messaging.pulsar.reading; + +import org.springframework.pulsar.annotation.PulsarReader; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + @PulsarReader(topics = "someTopic", startMessageId = "earliest") + public void processMessage(String content) { + // ... + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/readingreactive/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/readingreactive/MyBean.java new file mode 100644 index 000000000000..c42145288d55 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/readingreactive/MyBean.java @@ -0,0 +1,50 @@ +/* + * Copyright 2023-2023 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.docs.messaging.pulsar.readingreactive; + +import java.time.Instant; +import java.util.List; + +import org.apache.pulsar.client.api.Message; +import org.apache.pulsar.client.api.Schema; +import org.apache.pulsar.reactive.client.api.StartAtSpec; +import reactor.core.publisher.Mono; + +import org.springframework.pulsar.reactive.core.ReactiveMessageReaderBuilderCustomizer; +import org.springframework.pulsar.reactive.core.ReactivePulsarReaderFactory; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final ReactivePulsarReaderFactory pulsarReaderFactory; + + public MyBean(ReactivePulsarReaderFactory pulsarReaderFactory) { + this.pulsarReaderFactory = pulsarReaderFactory; + } + + public void someMethod() { + ReactiveMessageReaderBuilderCustomizer readerBuilderCustomizer = (readerBuilder) -> readerBuilder + .topic("someTopic") + .startAtSpec(StartAtSpec.ofInstant(Instant.now().minusSeconds(5))); + Mono> message = this.pulsarReaderFactory + .createReader(Schema.STRING, List.of(readerBuilderCustomizer)) + .readOne(); + // ... + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/receiving/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/receiving/MyBean.java new file mode 100644 index 000000000000..103e4ac8d65a --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/receiving/MyBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2023-2023 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.docs.messaging.pulsar.receiving; + +import org.springframework.pulsar.annotation.PulsarListener; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + @PulsarListener(topics = "someTopic") + public void processMessage(String content) { + // ... + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/receivingreactive/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/receivingreactive/MyBean.java new file mode 100644 index 000000000000..3dd9e8ffba98 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/receivingreactive/MyBean.java @@ -0,0 +1,33 @@ +/* + * Copyright 2023-2023 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.docs.messaging.pulsar.receivingreactive; + +import reactor.core.publisher.Mono; + +import org.springframework.pulsar.reactive.config.annotation.ReactivePulsarListener; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + @ReactivePulsarListener(topics = "someTopic") + public Mono processMessage(String content) { + // ... + return Mono.empty(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/sending/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/sending/MyBean.java new file mode 100644 index 000000000000..7b6610b03e92 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/sending/MyBean.java @@ -0,0 +1,37 @@ +/* + * Copyright 2023-2023 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.docs.messaging.pulsar.sending; + +import org.apache.pulsar.client.api.PulsarClientException; + +import org.springframework.pulsar.core.PulsarTemplate; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final PulsarTemplate pulsarTemplate; + + public MyBean(PulsarTemplate pulsarTemplate) { + this.pulsarTemplate = pulsarTemplate; + } + + public void someMethod() throws PulsarClientException { + this.pulsarTemplate.send("someTopic", "Hello"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/sendingreactive/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/sendingreactive/MyBean.java new file mode 100644 index 000000000000..1784f4ea8059 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/pulsar/sendingreactive/MyBean.java @@ -0,0 +1,35 @@ +/* + * Copyright 2023-2023 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.docs.messaging.pulsar.sendingreactive; + +import org.springframework.pulsar.reactive.core.ReactivePulsarTemplate; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final ReactivePulsarTemplate pulsarTemplate; + + public MyBean(ReactivePulsarTemplate pulsarTemplate) { + this.pulsarTemplate = pulsarTemplate; + } + + public void someMethod() { + this.pulsarTemplate.send("someTopic", "Hello").subscribe(); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/reading/MyBean.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/reading/MyBean.kt new file mode 100644 index 000000000000..bb2936cc07d5 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/reading/MyBean.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2022 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.docs.messaging.pulsar.reading + +import org.springframework.pulsar.annotation.PulsarReader +import org.springframework.stereotype.Component + +@Suppress("UNUSED_PARAMETER") +@Component +class MyBean { + + @PulsarReader(topics = ["someTopic"], startMessageId = "earliest") + fun processMessage(content: String?) { + // ... + } + +} + diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/readingreactive/MyBean.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/readingreactive/MyBean.kt new file mode 100644 index 000000000000..7651be558113 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/readingreactive/MyBean.kt @@ -0,0 +1,44 @@ +/* +* Copyright 2023-2023 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.docs.messaging.pulsar.readingreactive + +import org.apache.pulsar.client.api.Schema +import org.apache.pulsar.reactive.client.api.ReactiveMessageReaderBuilder +import org.apache.pulsar.reactive.client.api.StartAtSpec +import org.springframework.pulsar.reactive.core.ReactiveMessageReaderBuilderCustomizer +import org.springframework.pulsar.reactive.core.ReactivePulsarReaderFactory +import org.springframework.stereotype.Component +import java.time.Instant + +@Suppress("UNUSED_PARAMETER", "UNUSED_VARIABLE") +@Component +class MyBean(private val pulsarReaderFactory: ReactivePulsarReaderFactory) { + + fun someMethod() { + val readerBuilderCustomizer = ReactiveMessageReaderBuilderCustomizer { + readerBuilder: ReactiveMessageReaderBuilder -> + readerBuilder + .topic("someTopic") + .startAtSpec(StartAtSpec.ofInstant(Instant.now().minusSeconds(5))) + } + val message = pulsarReaderFactory + .createReader(Schema.STRING, listOf(readerBuilderCustomizer)) + .readOne() + // ... + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/receiving/MyBean.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/receiving/MyBean.kt new file mode 100644 index 000000000000..80ee6160ab43 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/receiving/MyBean.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2022 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.docs.messaging.pulsar.receiving + +import org.springframework.pulsar.annotation.PulsarListener +import org.springframework.stereotype.Component + +@Suppress("UNUSED_PARAMETER") +@Component +class MyBean { + + @PulsarListener(topics = ["someTopic"]) + fun processMessage(content: String?) { + // ... + } + +} + diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/receivingreactive/MyBean.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/receivingreactive/MyBean.kt new file mode 100644 index 000000000000..6434ff849225 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/receivingreactive/MyBean.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2023-2023 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.docs.messaging.pulsar.receivingreactive + +import org.springframework.pulsar.reactive.config.annotation.ReactivePulsarListener +import org.springframework.stereotype.Component +import reactor.core.publisher.Mono + +@Component +@Suppress("UNUSED_PARAMETER") +class MyBean { + + @ReactivePulsarListener(topics = ["someTopic"]) + fun processMessage(content: String?): Mono { + // ... + return Mono.empty() + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/sending/MyBean.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/sending/MyBean.kt new file mode 100644 index 000000000000..9a94168c6cb1 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/sending/MyBean.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2022 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.docs.messaging.pulsar.sending + +import org.apache.pulsar.client.api.PulsarClientException +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.pulsar.core.PulsarTemplate +import org.springframework.stereotype.Component + +@Component +class MyBean(private val pulsarTemplate: PulsarTemplate) { + + @Throws(PulsarClientException::class) + fun someMethod() { + pulsarTemplate.send("someTopic", "Hello") + } + +} + diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/sendingreactive/MyBean.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/sendingreactive/MyBean.kt new file mode 100644 index 000000000000..3205912919ec --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/pulsar/sendingreactive/MyBean.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2023-2023 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.docs.messaging.pulsar.sendingreactive + +import org.springframework.pulsar.reactive.core.ReactivePulsarTemplate +import org.springframework.stereotype.Component + +@Component +class MyBean(private val pulsarTemplate: ReactivePulsarTemplate) { + + fun someMethod() { + pulsarTemplate.send("someTopic", "Hello").subscribe() + } + +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-pulsar-reactive/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-pulsar-reactive/build.gradle new file mode 100644 index 000000000000..777b69567bb9 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-pulsar-reactive/build.gradle @@ -0,0 +1,19 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Spring for Apache Pulsar Reactive" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.springframework.pulsar:spring-pulsar-reactive") +} + +checkRuntimeClasspathForConflicts { + ignore { name -> name.startsWith("org/bouncycastle/") || + name.matches("^org\\/apache\\/pulsar\\/.*\\/package-info.class\$") || + name.equals("findbugsExclude.xml") || + name.startsWith("org/springframework/pulsar/shade/com/github/benmanes/caffeine/") || + name.startsWith("org/springframework/pulsar/shade/com/google/errorprone/") || + name.startsWith("org/springframework/pulsar/shade/org/checkerframework/") } +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-pulsar/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-pulsar/build.gradle new file mode 100644 index 000000000000..87b4c4b6283b --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-pulsar/build.gradle @@ -0,0 +1,16 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using Spring for Apache Pulsar" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("org.springframework.pulsar:spring-pulsar") +} + +checkRuntimeClasspathForConflicts { + ignore { name -> name.startsWith("org/bouncycastle/") || + name.matches("^org\\/apache\\/pulsar\\/.*\\/package-info.class\$") || + name.equals("findbugsExclude.xml") } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java index 6c9eb457d757..780e594e1a8e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java @@ -24,6 +24,7 @@ * @author Stephane Nicoll * @author Eddú Meléndez * @author Moritz Halbritter + * @author Chris Bono * @since 2.3.6 */ public final class DockerImageNames { @@ -50,6 +51,8 @@ public final class DockerImageNames { private static final String ORACLE_XE_VERSION = "18.4.0-slim"; + private static final String PULSAR_VERSION = "3.1.0"; + private static final String POSTGRESQL_VERSION = "14.0"; private static final String RABBIT_VERSION = "3.11-alpine"; @@ -153,6 +156,14 @@ public static DockerImageName oracleXe() { return DockerImageName.parse("gvenzl/oracle-xe").withTag(ORACLE_XE_VERSION); } + /** + * Return a {@link DockerImageName} suitable for running Apache Pulsar. + * @return a docker image name for running pulsar + */ + public static DockerImageName pulsar() { + return DockerImageName.parse("apachepulsar/pulsar").withTag(PULSAR_VERSION); + } + /** * Return a {@link DockerImageName} suitable for running PostgreSQL. * @return a docker image name for running postgresql diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/build.gradle new file mode 100644 index 000000000000..34c98479a7e7 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/build.gradle @@ -0,0 +1,15 @@ +plugins { + id "java" + id "org.springframework.boot.conventions" +} + +description = "Spring Boot Pulsar smoke test" + +dependencies { + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-pulsar-reactive")) + testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) + testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + testImplementation("org.awaitility:awaitility") + testImplementation("org.testcontainers:junit-jupiter") + testImplementation("org.testcontainers:pulsar") +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleMessage.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleMessage.java new file mode 100644 index 000000000000..7fde2d1b975e --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleMessage.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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 smoketest.pulsar.reactive; + +record SampleMessage(Integer id, String content) { +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleMessageConsumer.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleMessageConsumer.java new file mode 100644 index 000000000000..02cbe744697f --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleMessageConsumer.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2023 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 smoketest.pulsar.reactive; + +import java.util.ArrayList; +import java.util.List; + +import reactor.core.publisher.Mono; + +import org.springframework.pulsar.reactive.config.annotation.ReactivePulsarListener; +import org.springframework.stereotype.Component; + +@Component +class SampleMessageConsumer { + + private List consumed = new ArrayList<>(); + + List getConsumed() { + return this.consumed; + } + + @ReactivePulsarListener(topics = SampleReactivePulsarApplication.TOPIC) + Mono consumeMessagesFromPulsarTopic(SampleMessage msg) { + System.out.println("**** CONSUME: " + msg); + this.consumed.add(msg); + return Mono.empty(); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleReactivePulsarApplication.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleReactivePulsarApplication.java new file mode 100644 index 000000000000..460bc4ba4c2f --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleReactivePulsarApplication.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2023 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 smoketest.pulsar.reactive; + +import org.apache.pulsar.reactive.client.api.MessageSpec; +import reactor.core.publisher.Flux; + +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.pulsar.core.PulsarTopic; +import org.springframework.pulsar.reactive.core.ReactivePulsarTemplate; + +@SpringBootApplication +public class SampleReactivePulsarApplication { + + static final String TOPIC = "pulsar-reactive-smoke-test-topic"; + + @Bean + PulsarTopic pulsarTestTopic() { + return PulsarTopic.builder(TOPIC).numberOfPartitions(1).build(); + } + + @Bean + ApplicationRunner sendMessagesToPulsarTopic(ReactivePulsarTemplate template) { + return (args) -> Flux.range(0, 10) + .map((i) -> new SampleMessage(i, "message:" + i)) + .map(MessageSpec::of) + .as((msgs) -> template.send(TOPIC, msgs)) + .doOnNext((sendResult) -> System.out.println("*** PRODUCE: " + sendResult.getMessageId())) + .subscribe(); + } + + public static void main(String[] args) { + SpringApplication.run(SampleReactivePulsarApplication.class, args); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/resources/application.properties new file mode 100644 index 000000000000..cdb9707b22b1 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.pulsar.reactive.consumer.subscription-initial-position=earliest diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/test/java/smoketest/pulsar/reactive/SampleReactivePulsarApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/test/java/smoketest/pulsar/reactive/SampleReactivePulsarApplicationTests.java new file mode 100644 index 000000000000..eb13dc4280fe --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/test/java/smoketest/pulsar/reactive/SampleReactivePulsarApplicationTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2023 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 smoketest.pulsar.reactive; + +import java.time.Duration; +import java.util.stream.IntStream; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.PulsarContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Testcontainers(disabledWithoutDocker = true) +class SampleReactivePulsarApplicationTests { + + @Container + private static final PulsarContainer PULSAR_CONTAINER = new PulsarContainer(DockerImageNames.pulsar()) + .withStartupAttempts(2) + .withStartupTimeout(Duration.ofMinutes(3)); + + @DynamicPropertySource + static void pulsarProperties(DynamicPropertyRegistry registry) { + registry.add("spring.pulsar.client.service-url", PULSAR_CONTAINER::getPulsarBrokerUrl); + registry.add("spring.pulsar.admin.service-url", PULSAR_CONTAINER::getHttpServiceUrl); + } + + @Test + void appProducesAndConsumesSampleMessages(@Autowired SampleMessageConsumer consumer) { + Integer[] expectedIds = IntStream.range(0, 10).boxed().toArray(Integer[]::new); + Awaitility.await() + .atMost(Duration.ofSeconds(20)) + .untilAsserted(() -> assertThat(consumer.getConsumed()).extracting(SampleMessage::id) + .containsExactly(expectedIds)); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/build.gradle new file mode 100644 index 000000000000..e60e0ba606aa --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/build.gradle @@ -0,0 +1,15 @@ +plugins { + id "java" + id "org.springframework.boot.conventions" +} + +description = "Spring Boot Pulsar smoke test" + +dependencies { + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-pulsar")) + testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) + testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + testImplementation("org.awaitility:awaitility") + testImplementation("org.testcontainers:junit-jupiter") + testImplementation("org.testcontainers:pulsar") +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SampleMessage.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SampleMessage.java new file mode 100644 index 000000000000..3887ce61f13a --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SampleMessage.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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 smoketest.pulsar; + +record SampleMessage(Integer id, String content) { +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SampleMessageConsumer.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SampleMessageConsumer.java new file mode 100644 index 000000000000..e3c80cf223c8 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SampleMessageConsumer.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2023 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 smoketest.pulsar; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.pulsar.annotation.PulsarListener; +import org.springframework.stereotype.Component; + +@Component +class SampleMessageConsumer { + + private List consumed = new ArrayList<>(); + + List getConsumed() { + return this.consumed; + } + + @PulsarListener(topics = SamplePulsarApplication.TOPIC) + void consumeMessagesFromPulsarTopic(SampleMessage msg) { + System.out.println("**** CONSUME: " + msg); + this.consumed.add(msg); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SamplePulsarApplication.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SamplePulsarApplication.java new file mode 100644 index 000000000000..adc801e3d7ae --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SamplePulsarApplication.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2023 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 smoketest.pulsar; + +import org.apache.pulsar.client.api.MessageId; + +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.pulsar.core.PulsarTemplate; +import org.springframework.pulsar.core.PulsarTopic; + +@SpringBootApplication +public class SamplePulsarApplication { + + static final String TOPIC = "pulsar-smoke-test-topic"; + + @Bean + PulsarTopic pulsarTestTopic() { + return PulsarTopic.builder(TOPIC).numberOfPartitions(1).build(); + } + + @Bean + ApplicationRunner sendMessagesToPulsarTopic(PulsarTemplate template) { + return (args) -> { + for (int i = 0; i < 10; i++) { + MessageId msgId = template.send(TOPIC, new SampleMessage(i, "message:" + i)); + System.out.println("*** PRODUCE: " + msgId); + } + }; + } + + public static void main(String[] args) { + SpringApplication.run(SamplePulsarApplication.class, args); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/resources/application.properties new file mode 100644 index 000000000000..25502d64c7be --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.pulsar.consumer.subscription-initial-position=earliest diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/test/java/smoketest/pulsar/SamplePulsarApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/test/java/smoketest/pulsar/SamplePulsarApplicationTests.java new file mode 100644 index 000000000000..4918a58ba42e --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/test/java/smoketest/pulsar/SamplePulsarApplicationTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2023 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 smoketest.pulsar; + +import java.time.Duration; +import java.util.stream.IntStream; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.PulsarContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Testcontainers(disabledWithoutDocker = true) +class SamplePulsarApplicationTests { + + @Container + private static final PulsarContainer PULSAR_CONTAINER = new PulsarContainer(DockerImageNames.pulsar()) + .withStartupAttempts(2) + .withStartupTimeout(Duration.ofMinutes(3)); + + @DynamicPropertySource + static void pulsarProperties(DynamicPropertyRegistry registry) { + registry.add("spring.pulsar.client.service-url", PULSAR_CONTAINER::getPulsarBrokerUrl); + registry.add("spring.pulsar.admin.service-url", PULSAR_CONTAINER::getHttpServiceUrl); + } + + @Test + void appProducesAndConsumesSampleMessages(@Autowired SampleMessageConsumer consumer) { + Integer[] expectedIds = IntStream.range(0, 10).boxed().toArray(Integer[]::new); + Awaitility.await() + .atMost(Duration.ofSeconds(20)) + .untilAsserted(() -> assertThat(consumer.getConsumed()).extracting(SampleMessage::id) + .containsExactly(expectedIds)); + } + +} From 975cb279050e5a55a4642d9551289bcdd0cbfcde Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 5 Sep 2023 18:23:39 -0700 Subject: [PATCH 0369/1215] Protect against concurrent list updates in Pulsar samples See gh-34763 --- .../java/smoketest/pulsar/reactive/SampleMessageConsumer.java | 4 ++-- .../src/main/java/smoketest/pulsar/SampleMessageConsumer.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleMessageConsumer.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleMessageConsumer.java index 02cbe744697f..92ed71072e74 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleMessageConsumer.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleMessageConsumer.java @@ -16,8 +16,8 @@ package smoketest.pulsar.reactive; -import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import reactor.core.publisher.Mono; @@ -27,7 +27,7 @@ @Component class SampleMessageConsumer { - private List consumed = new ArrayList<>(); + private List consumed = new CopyOnWriteArrayList<>(); List getConsumed() { return this.consumed; diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SampleMessageConsumer.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SampleMessageConsumer.java index e3c80cf223c8..196dcbb5b453 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SampleMessageConsumer.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SampleMessageConsumer.java @@ -16,8 +16,8 @@ package smoketest.pulsar; -import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import org.springframework.pulsar.annotation.PulsarListener; import org.springframework.stereotype.Component; @@ -25,7 +25,7 @@ @Component class SampleMessageConsumer { - private List consumed = new ArrayList<>(); + private List consumed = new CopyOnWriteArrayList<>(); List getConsumed() { return this.consumed; From 2ebcdb059a6e9d7af8fbaf84e3e07b08750799a3 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 5 Sep 2023 18:46:36 -0700 Subject: [PATCH 0370/1215] Tweak Pulsar smoke test timeouts See gh-34763 --- .../SampleReactivePulsarApplicationTests.java | 14 ++++++++++---- .../pulsar/SamplePulsarApplicationTests.java | 14 ++++++++++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/test/java/smoketest/pulsar/reactive/SampleReactivePulsarApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/test/java/smoketest/pulsar/reactive/SampleReactivePulsarApplicationTests.java index eb13dc4280fe..48643ab3085e 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/test/java/smoketest/pulsar/reactive/SampleReactivePulsarApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/test/java/smoketest/pulsar/reactive/SampleReactivePulsarApplicationTests.java @@ -37,6 +37,8 @@ @Testcontainers(disabledWithoutDocker = true) class SampleReactivePulsarApplicationTests { + private static final Integer[] EXPECTED_IDS = IntStream.range(0, 10).boxed().toArray(Integer[]::new); + @Container private static final PulsarContainer PULSAR_CONTAINER = new PulsarContainer(DockerImageNames.pulsar()) .withStartupAttempts(2) @@ -50,11 +52,15 @@ static void pulsarProperties(DynamicPropertyRegistry registry) { @Test void appProducesAndConsumesSampleMessages(@Autowired SampleMessageConsumer consumer) { - Integer[] expectedIds = IntStream.range(0, 10).boxed().toArray(Integer[]::new); Awaitility.await() - .atMost(Duration.ofSeconds(20)) - .untilAsserted(() -> assertThat(consumer.getConsumed()).extracting(SampleMessage::id) - .containsExactly(expectedIds)); + .atMost(Duration.ofMinutes(3)) + .with() + .pollInterval(Duration.ofMillis(500)) + .untilAsserted(() -> hasExpectedIds(consumer)); + } + + private void hasExpectedIds(SampleMessageConsumer consumer) { + assertThat(consumer.getConsumed()).extracting(SampleMessage::id).containsExactly(EXPECTED_IDS); } } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/test/java/smoketest/pulsar/SamplePulsarApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/test/java/smoketest/pulsar/SamplePulsarApplicationTests.java index 4918a58ba42e..5877370ba7a8 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/test/java/smoketest/pulsar/SamplePulsarApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/test/java/smoketest/pulsar/SamplePulsarApplicationTests.java @@ -37,6 +37,8 @@ @Testcontainers(disabledWithoutDocker = true) class SamplePulsarApplicationTests { + private static final Integer[] EXPECTED_IDS = IntStream.range(0, 10).boxed().toArray(Integer[]::new); + @Container private static final PulsarContainer PULSAR_CONTAINER = new PulsarContainer(DockerImageNames.pulsar()) .withStartupAttempts(2) @@ -50,11 +52,15 @@ static void pulsarProperties(DynamicPropertyRegistry registry) { @Test void appProducesAndConsumesSampleMessages(@Autowired SampleMessageConsumer consumer) { - Integer[] expectedIds = IntStream.range(0, 10).boxed().toArray(Integer[]::new); Awaitility.await() - .atMost(Duration.ofSeconds(20)) - .untilAsserted(() -> assertThat(consumer.getConsumed()).extracting(SampleMessage::id) - .containsExactly(expectedIds)); + .atMost(Duration.ofMinutes(3)) + .with() + .pollInterval(Duration.ofMillis(500)) + .untilAsserted(() -> hasExpectedIds(consumer)); + } + + private void hasExpectedIds(SampleMessageConsumer consumer) { + assertThat(consumer.getConsumed()).extracting(SampleMessage::id).containsExactly(EXPECTED_IDS); } } From eacf92b1b28785327339f84159bf6b64a89ee002 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Tue, 5 Sep 2023 00:39:34 -0500 Subject: [PATCH 0371/1215] Combine Pulsar smoke tests * Simplify produce/consume verify via OutputCapture * Remove spring-boot-smoke-test-pulsar-reactive as no other smoke tests split them out See gh-37196 --- .../build.gradle | 15 ----- .../pulsar/reactive/SampleMessage.java | 20 ------ .../reactive/SampleMessageConsumer.java | 43 ------------ .../src/main/resources/application.properties | 1 - .../SampleReactivePulsarApplicationTests.java | 66 ------------------- .../build.gradle | 1 + .../smoketest/pulsar/ImperativeAppConfig.java | 58 ++++++++++++++++ .../smoketest/pulsar/ReactiveAppConfig.java} | 28 +++++--- .../pulsar/SampleMessageConsumer.java | 40 ----------- .../pulsar/SamplePulsarApplication.java | 23 ------- .../src/main/resources/application.properties | 2 +- .../pulsar/SamplePulsarApplicationTests.java | 55 ++++++++++++---- src/checkstyle/checkstyle-suppressions.xml | 1 + 13 files changed, 121 insertions(+), 232 deletions(-) delete mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/build.gradle delete mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleMessage.java delete mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleMessageConsumer.java delete mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/resources/application.properties delete mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/test/java/smoketest/pulsar/reactive/SampleReactivePulsarApplicationTests.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/ImperativeAppConfig.java rename spring-boot-tests/spring-boot-smoke-tests/{spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleReactivePulsarApplication.java => spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/ReactiveAppConfig.java} (59%) delete mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SampleMessageConsumer.java diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/build.gradle deleted file mode 100644 index 34c98479a7e7..000000000000 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/build.gradle +++ /dev/null @@ -1,15 +0,0 @@ -plugins { - id "java" - id "org.springframework.boot.conventions" -} - -description = "Spring Boot Pulsar smoke test" - -dependencies { - implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-pulsar-reactive")) - testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) - testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) - testImplementation("org.awaitility:awaitility") - testImplementation("org.testcontainers:junit-jupiter") - testImplementation("org.testcontainers:pulsar") -} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleMessage.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleMessage.java deleted file mode 100644 index 7fde2d1b975e..000000000000 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleMessage.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2012-2023 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 smoketest.pulsar.reactive; - -record SampleMessage(Integer id, String content) { -} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleMessageConsumer.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleMessageConsumer.java deleted file mode 100644 index 92ed71072e74..000000000000 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleMessageConsumer.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2012-2023 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 smoketest.pulsar.reactive; - -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -import reactor.core.publisher.Mono; - -import org.springframework.pulsar.reactive.config.annotation.ReactivePulsarListener; -import org.springframework.stereotype.Component; - -@Component -class SampleMessageConsumer { - - private List consumed = new CopyOnWriteArrayList<>(); - - List getConsumed() { - return this.consumed; - } - - @ReactivePulsarListener(topics = SampleReactivePulsarApplication.TOPIC) - Mono consumeMessagesFromPulsarTopic(SampleMessage msg) { - System.out.println("**** CONSUME: " + msg); - this.consumed.add(msg); - return Mono.empty(); - } - -} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/resources/application.properties deleted file mode 100644 index cdb9707b22b1..000000000000 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.pulsar.reactive.consumer.subscription-initial-position=earliest diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/test/java/smoketest/pulsar/reactive/SampleReactivePulsarApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/test/java/smoketest/pulsar/reactive/SampleReactivePulsarApplicationTests.java deleted file mode 100644 index 48643ab3085e..000000000000 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/test/java/smoketest/pulsar/reactive/SampleReactivePulsarApplicationTests.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2012-2023 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 smoketest.pulsar.reactive; - -import java.time.Duration; -import java.util.stream.IntStream; - -import org.awaitility.Awaitility; -import org.junit.jupiter.api.Test; -import org.testcontainers.containers.PulsarContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.testsupport.testcontainers.DockerImageNames; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; - -import static org.assertj.core.api.Assertions.assertThat; - -@SpringBootTest -@Testcontainers(disabledWithoutDocker = true) -class SampleReactivePulsarApplicationTests { - - private static final Integer[] EXPECTED_IDS = IntStream.range(0, 10).boxed().toArray(Integer[]::new); - - @Container - private static final PulsarContainer PULSAR_CONTAINER = new PulsarContainer(DockerImageNames.pulsar()) - .withStartupAttempts(2) - .withStartupTimeout(Duration.ofMinutes(3)); - - @DynamicPropertySource - static void pulsarProperties(DynamicPropertyRegistry registry) { - registry.add("spring.pulsar.client.service-url", PULSAR_CONTAINER::getPulsarBrokerUrl); - registry.add("spring.pulsar.admin.service-url", PULSAR_CONTAINER::getHttpServiceUrl); - } - - @Test - void appProducesAndConsumesSampleMessages(@Autowired SampleMessageConsumer consumer) { - Awaitility.await() - .atMost(Duration.ofMinutes(3)) - .with() - .pollInterval(Duration.ofMillis(500)) - .untilAsserted(() -> hasExpectedIds(consumer)); - } - - private void hasExpectedIds(SampleMessageConsumer consumer) { - assertThat(consumer.getConsumed()).extracting(SampleMessage::id).containsExactly(EXPECTED_IDS); - } - -} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/build.gradle index e60e0ba606aa..6058d1127ae1 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/build.gradle @@ -7,6 +7,7 @@ description = "Spring Boot Pulsar smoke test" dependencies { implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-pulsar")) + implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-pulsar-reactive")) testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) testImplementation("org.awaitility:awaitility") diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/ImperativeAppConfig.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/ImperativeAppConfig.java new file mode 100644 index 000000000000..32f939bdb402 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/ImperativeAppConfig.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2023 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 smoketest.pulsar; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.pulsar.annotation.PulsarListener; +import org.springframework.pulsar.core.PulsarTemplate; +import org.springframework.pulsar.core.PulsarTopic; + +@Configuration(proxyBeanMethods = false) +@Profile("smoketest.pulsar.imperative") +class ImperativeAppConfig { + + private static final Log LOG = LogFactory.getLog(ImperativeAppConfig.class); + + private static final String TOPIC = "pulsar-smoke-test-topic"; + + @Bean + PulsarTopic pulsarTestTopic() { + return PulsarTopic.builder(TOPIC).numberOfPartitions(1).build(); + } + + @Bean + ApplicationRunner sendMessagesToPulsarTopic(PulsarTemplate template) { + return (args) -> { + for (int i = 0; i < 10; i++) { + template.send(TOPIC, new SampleMessage(i, "message:" + i)); + LOG.info("++++++PRODUCE IMPERATIVE:(" + i + ")------"); + } + }; + } + + @PulsarListener(topics = TOPIC) + void consumeMessagesFromPulsarTopic(SampleMessage msg) { + LOG.info("++++++CONSUME IMPERATIVE:(" + msg.id() + ")------"); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleReactivePulsarApplication.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/ReactiveAppConfig.java similarity index 59% rename from spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleReactivePulsarApplication.java rename to spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/ReactiveAppConfig.java index 460bc4ba4c2f..0d2b30343342 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar-reactive/src/main/java/smoketest/pulsar/reactive/SampleReactivePulsarApplication.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/ReactiveAppConfig.java @@ -14,22 +14,29 @@ * limitations under the License. */ -package smoketest.pulsar.reactive; +package smoketest.pulsar; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.apache.pulsar.reactive.client.api.MessageSpec; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.boot.ApplicationRunner; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import org.springframework.pulsar.core.PulsarTopic; +import org.springframework.pulsar.reactive.config.annotation.ReactivePulsarListener; import org.springframework.pulsar.reactive.core.ReactivePulsarTemplate; -@SpringBootApplication -public class SampleReactivePulsarApplication { +@Configuration(proxyBeanMethods = false) +@Profile("smoketest.pulsar.reactive") +class ReactiveAppConfig { - static final String TOPIC = "pulsar-reactive-smoke-test-topic"; + private static final Log LOG = LogFactory.getLog(ReactiveAppConfig.class); + + private static final String TOPIC = "pulsar-reactive-smoke-test-topic"; @Bean PulsarTopic pulsarTestTopic() { @@ -42,12 +49,15 @@ ApplicationRunner sendMessagesToPulsarTopic(ReactivePulsarTemplate new SampleMessage(i, "message:" + i)) .map(MessageSpec::of) .as((msgs) -> template.send(TOPIC, msgs)) - .doOnNext((sendResult) -> System.out.println("*** PRODUCE: " + sendResult.getMessageId())) + .doOnNext((sendResult) -> LOG + .info("++++++PRODUCE REACTIVE:(" + sendResult.getMessageSpec().getValue().id() + ")------")) .subscribe(); } - public static void main(String[] args) { - SpringApplication.run(SampleReactivePulsarApplication.class, args); + @ReactivePulsarListener(topics = TOPIC) + Mono consumeMessagesFromPulsarTopic(SampleMessage msg) { + LOG.info("++++++CONSUME REACTIVE:(" + msg.id() + ")------"); + return Mono.empty(); } } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SampleMessageConsumer.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SampleMessageConsumer.java deleted file mode 100644 index 196dcbb5b453..000000000000 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SampleMessageConsumer.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2012-2023 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 smoketest.pulsar; - -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -import org.springframework.pulsar.annotation.PulsarListener; -import org.springframework.stereotype.Component; - -@Component -class SampleMessageConsumer { - - private List consumed = new CopyOnWriteArrayList<>(); - - List getConsumed() { - return this.consumed; - } - - @PulsarListener(topics = SamplePulsarApplication.TOPIC) - void consumeMessagesFromPulsarTopic(SampleMessage msg) { - System.out.println("**** CONSUME: " + msg); - this.consumed.add(msg); - } - -} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SamplePulsarApplication.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SamplePulsarApplication.java index adc801e3d7ae..560967bb2d0d 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SamplePulsarApplication.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/SamplePulsarApplication.java @@ -16,35 +16,12 @@ package smoketest.pulsar; -import org.apache.pulsar.client.api.MessageId; - -import org.springframework.boot.ApplicationRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.pulsar.core.PulsarTemplate; -import org.springframework.pulsar.core.PulsarTopic; @SpringBootApplication public class SamplePulsarApplication { - static final String TOPIC = "pulsar-smoke-test-topic"; - - @Bean - PulsarTopic pulsarTestTopic() { - return PulsarTopic.builder(TOPIC).numberOfPartitions(1).build(); - } - - @Bean - ApplicationRunner sendMessagesToPulsarTopic(PulsarTemplate template) { - return (args) -> { - for (int i = 0; i < 10; i++) { - MessageId msgId = template.send(TOPIC, new SampleMessage(i, "message:" + i)); - System.out.println("*** PRODUCE: " + msgId); - } - }; - } - public static void main(String[] args) { SpringApplication.run(SamplePulsarApplication.class, args); } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/resources/application.properties index 25502d64c7be..b1ae3ec6f4ee 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/resources/application.properties @@ -1 +1 @@ -spring.pulsar.consumer.subscription-initial-position=earliest +spring.pulsar.consumer.subscription.initial-position=earliest diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/test/java/smoketest/pulsar/SamplePulsarApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/test/java/smoketest/pulsar/SamplePulsarApplicationTests.java index 5877370ba7a8..e30cf1a64089 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/test/java/smoketest/pulsar/SamplePulsarApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/test/java/smoketest/pulsar/SamplePulsarApplicationTests.java @@ -17,30 +17,34 @@ package smoketest.pulsar; import java.time.Duration; +import java.util.ArrayList; +import java.util.List; import java.util.stream.IntStream; import org.awaitility.Awaitility; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.testcontainers.containers.PulsarContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import static org.assertj.core.api.Assertions.assertThat; -@SpringBootTest @Testcontainers(disabledWithoutDocker = true) +@ExtendWith(OutputCaptureExtension.class) class SamplePulsarApplicationTests { - private static final Integer[] EXPECTED_IDS = IntStream.range(0, 10).boxed().toArray(Integer[]::new); - @Container - private static final PulsarContainer PULSAR_CONTAINER = new PulsarContainer(DockerImageNames.pulsar()) + static final PulsarContainer PULSAR_CONTAINER = new PulsarContainer(DockerImageNames.pulsar()) .withStartupAttempts(2) .withStartupTimeout(Duration.ofMinutes(3)); @@ -50,17 +54,40 @@ static void pulsarProperties(DynamicPropertyRegistry registry) { registry.add("spring.pulsar.admin.service-url", PULSAR_CONTAINER::getHttpServiceUrl); } - @Test - void appProducesAndConsumesSampleMessages(@Autowired SampleMessageConsumer consumer) { - Awaitility.await() - .atMost(Duration.ofMinutes(3)) - .with() - .pollInterval(Duration.ofMillis(500)) - .untilAsserted(() -> hasExpectedIds(consumer)); + @Nested + @SpringBootTest + @ActiveProfiles("smoketest.pulsar.imperative") + class ImperativeApp { + + @Test + void appProducesAndConsumesMessages(CapturedOutput output) { + List expectedOutput = new ArrayList<>(); + IntStream.range(0, 10).forEachOrdered((i) -> { + expectedOutput.add("++++++PRODUCE IMPERATIVE:(" + i + ")------"); + expectedOutput.add("++++++CONSUME IMPERATIVE:(" + i + ")------"); + }); + Awaitility.waitAtMost(Duration.ofSeconds(30)) + .untilAsserted(() -> assertThat(output).contains(expectedOutput)); + } + } - private void hasExpectedIds(SampleMessageConsumer consumer) { - assertThat(consumer.getConsumed()).extracting(SampleMessage::id).containsExactly(EXPECTED_IDS); + @Nested + @SpringBootTest + @ActiveProfiles("smoketest.pulsar.reactive") + class ReactiveApp { + + @Test + void appProducesAndConsumesMessagesReactively(CapturedOutput output) { + List expectedOutput = new ArrayList<>(); + IntStream.range(0, 10).forEachOrdered((i) -> { + expectedOutput.add("++++++PRODUCE REACTIVE:(" + i + ")------"); + expectedOutput.add("++++++CONSUME REACTIVE:(" + i + ")------"); + }); + Awaitility.waitAtMost(Duration.ofSeconds(30)) + .untilAsserted(() -> assertThat(output).contains(expectedOutput)); + } + } } diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 3f7d8b5e0655..f224ac25609f 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -32,6 +32,7 @@ + From 9497f3d91cfa36a6553c3db25d0657523cf69a0d Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 5 Sep 2023 20:41:35 -0700 Subject: [PATCH 0372/1215] Polish "Combine Pulsar smoke tests" See gh-37196 --- .../smoketest/pulsar/ImperativeAppConfig.java | 6 +-- .../smoketest/pulsar/ReactiveAppConfig.java | 6 +-- .../pulsar/SamplePulsarApplicationTests.java | 46 +++++++++++-------- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/ImperativeAppConfig.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/ImperativeAppConfig.java index 32f939bdb402..e7482711fbac 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/ImperativeAppConfig.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/ImperativeAppConfig.java @@ -31,7 +31,7 @@ @Profile("smoketest.pulsar.imperative") class ImperativeAppConfig { - private static final Log LOG = LogFactory.getLog(ImperativeAppConfig.class); + private static final Log logger = LogFactory.getLog(ImperativeAppConfig.class); private static final String TOPIC = "pulsar-smoke-test-topic"; @@ -45,14 +45,14 @@ ApplicationRunner sendMessagesToPulsarTopic(PulsarTemplate templa return (args) -> { for (int i = 0; i < 10; i++) { template.send(TOPIC, new SampleMessage(i, "message:" + i)); - LOG.info("++++++PRODUCE IMPERATIVE:(" + i + ")------"); + logger.info("++++++PRODUCE IMPERATIVE:(" + i + ")------"); } }; } @PulsarListener(topics = TOPIC) void consumeMessagesFromPulsarTopic(SampleMessage msg) { - LOG.info("++++++CONSUME IMPERATIVE:(" + msg.id() + ")------"); + logger.info("++++++CONSUME IMPERATIVE:(" + msg.id() + ")------"); } } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/ReactiveAppConfig.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/ReactiveAppConfig.java index 0d2b30343342..844178bd44be 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/ReactiveAppConfig.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/main/java/smoketest/pulsar/ReactiveAppConfig.java @@ -34,7 +34,7 @@ @Profile("smoketest.pulsar.reactive") class ReactiveAppConfig { - private static final Log LOG = LogFactory.getLog(ReactiveAppConfig.class); + private static final Log logger = LogFactory.getLog(ReactiveAppConfig.class); private static final String TOPIC = "pulsar-reactive-smoke-test-topic"; @@ -49,14 +49,14 @@ ApplicationRunner sendMessagesToPulsarTopic(ReactivePulsarTemplate new SampleMessage(i, "message:" + i)) .map(MessageSpec::of) .as((msgs) -> template.send(TOPIC, msgs)) - .doOnNext((sendResult) -> LOG + .doOnNext((sendResult) -> logger .info("++++++PRODUCE REACTIVE:(" + sendResult.getMessageSpec().getValue().id() + ")------")) .subscribe(); } @ReactivePulsarListener(topics = TOPIC) Mono consumeMessagesFromPulsarTopic(SampleMessage msg) { - LOG.info("++++++CONSUME REACTIVE:(" + msg.id() + ")------"); + logger.info("++++++CONSUME REACTIVE:(" + msg.id() + ")------"); return Mono.empty(); } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/test/java/smoketest/pulsar/SamplePulsarApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/test/java/smoketest/pulsar/SamplePulsarApplicationTests.java index e30cf1a64089..b32be31dfe2f 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/test/java/smoketest/pulsar/SamplePulsarApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/test/java/smoketest/pulsar/SamplePulsarApplicationTests.java @@ -44,27 +44,29 @@ class SamplePulsarApplicationTests { @Container - static final PulsarContainer PULSAR_CONTAINER = new PulsarContainer(DockerImageNames.pulsar()) - .withStartupAttempts(2) + static final PulsarContainer container = new PulsarContainer(DockerImageNames.pulsar()).withStartupAttempts(2) .withStartupTimeout(Duration.ofMinutes(3)); @DynamicPropertySource static void pulsarProperties(DynamicPropertyRegistry registry) { - registry.add("spring.pulsar.client.service-url", PULSAR_CONTAINER::getPulsarBrokerUrl); - registry.add("spring.pulsar.admin.service-url", PULSAR_CONTAINER::getHttpServiceUrl); + registry.add("spring.pulsar.client.service-url", container::getPulsarBrokerUrl); + registry.add("spring.pulsar.admin.service-url", container::getHttpServiceUrl); } - @Nested - @SpringBootTest - @ActiveProfiles("smoketest.pulsar.imperative") - class ImperativeApp { + abstract class PulsarApplication { + + private final String type; + + PulsarApplication(String type) { + this.type = type; + } @Test void appProducesAndConsumesMessages(CapturedOutput output) { List expectedOutput = new ArrayList<>(); IntStream.range(0, 10).forEachOrdered((i) -> { - expectedOutput.add("++++++PRODUCE IMPERATIVE:(" + i + ")------"); - expectedOutput.add("++++++CONSUME IMPERATIVE:(" + i + ")------"); + expectedOutput.add("++++++PRODUCE %s:(%s)------".formatted(this.type, i)); + expectedOutput.add("++++++CONSUME %s:(%s)------".formatted(this.type, i)); }); Awaitility.waitAtMost(Duration.ofSeconds(30)) .untilAsserted(() -> assertThat(output).contains(expectedOutput)); @@ -72,20 +74,24 @@ void appProducesAndConsumesMessages(CapturedOutput output) { } + @Nested + @SpringBootTest + @ActiveProfiles("smoketest.pulsar.imperative") + class ImperativePulsarApplication extends PulsarApplication { + + ImperativePulsarApplication() { + super("IMPERATIVE"); + } + + } + @Nested @SpringBootTest @ActiveProfiles("smoketest.pulsar.reactive") - class ReactiveApp { + class ReactivePulsarApplication extends PulsarApplication { - @Test - void appProducesAndConsumesMessagesReactively(CapturedOutput output) { - List expectedOutput = new ArrayList<>(); - IntStream.range(0, 10).forEachOrdered((i) -> { - expectedOutput.add("++++++PRODUCE REACTIVE:(" + i + ")------"); - expectedOutput.add("++++++CONSUME REACTIVE:(" + i + ")------"); - }); - Awaitility.waitAtMost(Duration.ofSeconds(30)) - .untilAsserted(() -> assertThat(output).contains(expectedOutput)); + ReactivePulsarApplication() { + super("REACTIVE"); } } From 320dd0e24e5c07f233ebdf653e0669a05542ad46 Mon Sep 17 00:00:00 2001 From: anessi Date: Tue, 5 Sep 2023 10:54:55 +0200 Subject: [PATCH 0373/1215] Add virtual host support for Rabbit Stream Add a new property 'spring.rabbitmq.stream.virtual-host' which can be used to set a custom virtual host for streams. See gh-37189 --- .../autoconfigure/amqp/RabbitProperties.java | 14 ++++++++++++++ .../amqp/RabbitStreamConfiguration.java | 4 ++++ .../amqp/RabbitStreamConfigurationTests.java | 18 ++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java index 4e95d7346bf9..a9ae5efc4114 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java @@ -1206,6 +1206,12 @@ public static final class Stream { */ private int port = DEFAULT_STREAM_PORT; + /** + * Virtual host of a RabbitMQ instance with the Stream plugin enabled. When not + * set, spring.rabbitmq.virtual-host is used. + */ + private String virtualHost; + /** * Login user to authenticate to the broker. When not set, * spring.rabbitmq.username is used. @@ -1239,6 +1245,14 @@ public void setPort(int port) { this.port = port; } + public String getVirtualHost() { + return this.virtualHost; + } + + public void setVirtualHost(String virtualHost) { + this.virtualHost = virtualHost; + } + public String getUsername() { return this.username; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfiguration.java index 6547cfdc4e9c..dfacce8455ae 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfiguration.java @@ -102,6 +102,10 @@ static EnvironmentBuilder configure(EnvironmentBuilder builder, RabbitProperties PropertyMapper map = PropertyMapper.get(); map.from(stream.getHost()).to(builder::host); map.from(stream.getPort()).to(builder::port); + map.from(stream.getVirtualHost()) + .as(withFallback(properties::getVirtualHost)) + .whenNonNull() + .to(builder::virtualHost); map.from(stream.getUsername()).as(withFallback(properties::getUsername)).whenNonNull().to(builder::username); map.from(stream.getPassword()).as(withFallback(properties::getPassword)).whenNonNull().to(builder::password); return builder; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfigurationTests.java index eec28db57a32..95549628d1e7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfigurationTests.java @@ -143,6 +143,24 @@ void whenStreamHostIsSetThenEnvironmentUsesCustomHost() { then(builder).should().host("stream.rabbit.example.com"); } + @Test + void whenStreamVirtualHostIsSetThenEnvironmentUsesCustomVirtualHost() { + EnvironmentBuilder builder = mock(EnvironmentBuilder.class); + RabbitProperties properties = new RabbitProperties(); + properties.getStream().setVirtualHost("stream-virtual-host"); + RabbitStreamConfiguration.configure(builder, properties); + then(builder).should().virtualHost("stream-virtual-host"); + } + + @Test + void whenStreamVirtualHostIsNotSetButDefaultVirtualHostIsSetThenEnvironmentUsesDefaultVirtualHost() { + EnvironmentBuilder builder = mock(EnvironmentBuilder.class); + RabbitProperties properties = new RabbitProperties(); + properties.setVirtualHost("default-virtual-host"); + RabbitStreamConfiguration.configure(builder, properties); + then(builder).should().virtualHost("default-virtual-host"); + } + @Test void whenStreamCredentialsAreNotSetThenEnvironmentUsesRabbitCredentials() { EnvironmentBuilder builder = mock(EnvironmentBuilder.class); From d84c81d18fe6b5f59cff70496582d80a437fc6d9 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 6 Sep 2023 14:23:27 +0200 Subject: [PATCH 0374/1215] Reduce logging in WelcomePageHandlerMapping on invalid Accept headers Closes gh-37118 --- .../web/servlet/WelcomePageHandlerMapping.java | 10 +++++++++- .../servlet/WelcomePageHandlerMappingTests.java | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WelcomePageHandlerMapping.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WelcomePageHandlerMapping.java index 16661f20dbe4..fd669855a299 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WelcomePageHandlerMapping.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WelcomePageHandlerMapping.java @@ -28,6 +28,7 @@ import org.springframework.core.io.Resource; import org.springframework.core.log.LogMessage; import org.springframework.http.HttpHeaders; +import org.springframework.http.InvalidMediaTypeException; import org.springframework.http.MediaType; import org.springframework.util.StringUtils; import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping; @@ -40,6 +41,7 @@ * * @author Andy Wilkinson * @author Bruce Brouwer + * @author Moritz Halbritter * @see WelcomePageNotAcceptableHandlerMapping */ final class WelcomePageHandlerMapping extends AbstractUrlHandlerMapping { @@ -79,7 +81,13 @@ private boolean isHtmlTextAccepted(HttpServletRequest request) { private List getAcceptedMediaTypes(HttpServletRequest request) { String acceptHeader = request.getHeader(HttpHeaders.ACCEPT); if (StringUtils.hasText(acceptHeader)) { - return MediaType.parseMediaTypes(acceptHeader); + try { + return MediaType.parseMediaTypes(acceptHeader); + } + catch (InvalidMediaTypeException ex) { + logger.warn("Received invalid Accept header. Assuming all media types are accepted", + logger.isDebugEnabled() ? ex : null); + } } return MEDIA_TYPES_ALL; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WelcomePageHandlerMappingTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WelcomePageHandlerMappingTests.java index b706b5e68b2b..96462cf037b8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WelcomePageHandlerMappingTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/WelcomePageHandlerMappingTests.java @@ -22,6 +22,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Value; @@ -30,6 +31,8 @@ import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -53,7 +56,9 @@ * Tests for {@link WelcomePageHandlerMapping}. * * @author Andy Wilkinson + * @author Moritz Halbritter */ +@ExtendWith(OutputCaptureExtension.class) class WelcomePageHandlerMappingTests { private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() @@ -163,6 +168,17 @@ void prefersAStaticResourceToATemplate() { }); } + @Test + void logsInvalidAcceptHeader(CapturedOutput output) { + this.contextRunner.withUserConfiguration(TemplateConfiguration.class).run((context) -> { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context).build(); + mockMvc.perform(get("/").accept("*/*q=0.8")) + .andExpect(status().isOk()) + .andExpect(content().string("index template")); + }); + assertThat(output).contains("Received invalid Accept header. Assuming all media types are accepted"); + } + @Configuration(proxyBeanMethods = false) static class HandlerMappingConfiguration { From 089fef039205f14c19d37d0d7fad9597cd71a584 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Mon, 22 May 2023 00:15:30 -0500 Subject: [PATCH 0375/1215] Add Pulsar ConnectionDetails support Add `ConnectionDetails` support for Apache Pulsar and provide adapters for Docker Compose and Testcontainers. See gh-37197 --- .../spring-boot-autoconfigure/build.gradle | 1 + .../PropertiesPulsarConnectionDetails.java | 42 ++++++++ .../pulsar/PulsarConfiguration.java | 25 ++++- .../pulsar/PulsarConnectionDetails.java | 41 ++++++++ .../pulsar/PulsarPropertiesMapper.java | 1 + ...ropertiesPulsarConnectionDetailsTests.java | 46 +++++++++ .../pulsar/PulsarAutoConfigurationTests.java | 1 + .../pulsar/PulsarConfigurationTests.java | 53 ++++++++++- ...DockerComposeConnectionDetailsFactory.java | 74 +++++++++++++++ .../connection/pulsar/package-info.java | 20 ++++ .../main/resources/META-INF/spring.factories | 2 +- ...nectionDetailsFactoryIntegrationTests.java | 46 +++++++++ .../connection/pulsar/pulsar-compose.yaml | 9 ++ .../asciidoc/features/docker-compose.adoc | 3 + .../src/docs/asciidoc/features/testing.adoc | 3 + .../spring-boot-testcontainers/build.gradle | 6 ++ ...lsarContainerConnectionDetailsFactory.java | 62 ++++++++++++ .../connection/pulsar/package-info.java | 20 ++++ .../main/resources/META-INF/spring.factories | 1 + ...nectionDetailsFactoryIntegrationTests.java | 95 +++++++++++++++++++ .../build.gradle | 1 + .../pulsar/SamplePulsarApplicationTests.java | 11 +-- 22 files changed, 549 insertions(+), 14 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PropertiesPulsarConnectionDetails.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarConnectionDetails.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PropertiesPulsarConnectionDetailsTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/pulsar/PulsarDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/pulsar/package-info.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/pulsar/PulsarDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/pulsar/pulsar-compose.yaml create mode 100644 spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/pulsar/PulsarContainerConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/pulsar/package-info.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/pulsar/PulsarContainerConnectionDetailsFactoryIntegrationTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index b48da7b7bf45..f88acc52eed2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -277,4 +277,5 @@ tasks.named("checkSpringConfigurationMetadata").configure { test { jvmArgs += "--add-opens=java.base/java.net=ALL-UNNAMED" + jvmArgs += "--add-opens=java.base/sun.net=ALL-UNNAMED" } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PropertiesPulsarConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PropertiesPulsarConnectionDetails.java new file mode 100644 index 000000000000..6865e3a5e6b0 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PropertiesPulsarConnectionDetails.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2023 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.pulsar; + +/** + * Adapts {@link PulsarProperties} to {@link PulsarConnectionDetails}. + * + * @author Chris Bono + */ +class PropertiesPulsarConnectionDetails implements PulsarConnectionDetails { + + private final PulsarProperties pulsarProperties; + + PropertiesPulsarConnectionDetails(PulsarProperties pulsarProperties) { + this.pulsarProperties = pulsarProperties; + } + + @Override + public String getPulsarBrokerUrl() { + return this.pulsarProperties.getClient().getServiceUrl(); + } + + @Override + public String getPulsarAdminUrl() { + return this.pulsarProperties.getAdmin().getServiceUrl(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfiguration.java index 14551bed70d3..6a5baf469fd0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfiguration.java @@ -71,17 +71,31 @@ class PulsarConfiguration { this.propertiesMapper = new PulsarPropertiesMapper(properties); } + @Bean + @ConditionalOnMissingBean(PulsarConnectionDetails.class) + PropertiesPulsarConnectionDetails pulsarConnectionDetails() { + return new PropertiesPulsarConnectionDetails(this.properties); + } + @Bean @ConditionalOnMissingBean(PulsarClientFactory.class) - DefaultPulsarClientFactory pulsarClientFactory(ObjectProvider customizersProvider) { + DefaultPulsarClientFactory pulsarClientFactory(PulsarConnectionDetails connectionDetails, + ObjectProvider customizersProvider) { List allCustomizers = new ArrayList<>(); allCustomizers.add(this.propertiesMapper::customizeClientBuilder); + allCustomizers.add((clientBuilder) -> this.applyConnectionDetails(connectionDetails, clientBuilder)); allCustomizers.addAll(customizersProvider.orderedStream().toList()); DefaultPulsarClientFactory clientFactory = new DefaultPulsarClientFactory( (clientBuilder) -> applyClientBuilderCustomizers(allCustomizers, clientBuilder)); return clientFactory; } + private void applyConnectionDetails(PulsarConnectionDetails connectionDetails, ClientBuilder clientBuilder) { + if (connectionDetails.getPulsarBrokerUrl() != null) { + clientBuilder.serviceUrl(connectionDetails.getPulsarBrokerUrl()); + } + } + private void applyClientBuilderCustomizers(List customizers, ClientBuilder clientBuilder) { customizers.forEach((customizer) -> customizer.customize(clientBuilder)); @@ -95,14 +109,21 @@ PulsarClient pulsarClient(PulsarClientFactory clientFactory) throws PulsarClient @Bean @ConditionalOnMissingBean - PulsarAdministration pulsarAdministration( + PulsarAdministration pulsarAdministration(PulsarConnectionDetails connectionDetails, ObjectProvider pulsarAdminBuilderCustomizers) { List allCustomizers = new ArrayList<>(); allCustomizers.add(this.propertiesMapper::customizeAdminBuilder); + allCustomizers.add((adminBuilder) -> this.applyConnectionDetails(connectionDetails, adminBuilder)); allCustomizers.addAll(pulsarAdminBuilderCustomizers.orderedStream().toList()); return new PulsarAdministration((adminBuilder) -> applyAdminBuilderCustomizers(allCustomizers, adminBuilder)); } + private void applyConnectionDetails(PulsarConnectionDetails connectionDetails, PulsarAdminBuilder adminBuilder) { + if (connectionDetails.getPulsarAdminUrl() != null) { + adminBuilder.serviceHttpUrl(connectionDetails.getPulsarAdminUrl()); + } + } + private void applyAdminBuilderCustomizers(List customizers, PulsarAdminBuilder adminBuilder) { customizers.forEach((customizer) -> customizer.customize(adminBuilder)); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarConnectionDetails.java new file mode 100644 index 000000000000..567134b77a04 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarConnectionDetails.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2023 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.pulsar; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to a Pulsar service. + * + * @author Chris Bono + * @since 3.2.0 + */ +public interface PulsarConnectionDetails extends ConnectionDetails { + + /** + * Returns the Pulsar service URL for the broker. + * @return the Pulsar service URL for the broker + */ + String getPulsarBrokerUrl(); + + /** + * Returns the Pulsar web URL for the admin endpoint. + * @return the Pulsar web URL for the admin endpoint + */ + String getPulsarAdminUrl(); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java index 10c3a77597bb..77a6b6321269 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java @@ -53,6 +53,7 @@ void customizeClientBuilder(ClientBuilder clientBuilder) { PulsarProperties.Client properties = this.properties.getClient(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(properties::getServiceUrl).to(clientBuilder::serviceUrl); + map.from(properties::getConnectionTimeout).to(timeoutProperty(clientBuilder::connectionTimeout)); map.from(properties::getOperationTimeout).to(timeoutProperty(clientBuilder::operationTimeout)); map.from(properties::getLookupTimeout).to(timeoutProperty(clientBuilder::lookupTimeout)); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PropertiesPulsarConnectionDetailsTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PropertiesPulsarConnectionDetailsTests.java new file mode 100644 index 000000000000..8fed356282f6 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PropertiesPulsarConnectionDetailsTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2023 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.pulsar; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PropertiesPulsarConnectionDetails}. + * + * @author Chris Bono + */ +class PropertiesPulsarConnectionDetailsTests { + + @Test + void pulsarBrokerUrlIsObtainedFromPulsarProperties() { + var pulsarProps = new PulsarProperties(); + pulsarProps.getClient().setServiceUrl("foo"); + var connectionDetails = new PropertiesPulsarConnectionDetails(pulsarProps); + assertThat(connectionDetails.getPulsarBrokerUrl()).isEqualTo("foo"); + } + + @Test + void pulsarAdminUrlIsObtainedFromPulsarProperties() { + var pulsarProps = new PulsarProperties(); + pulsarProps.getAdmin().setServiceUrl("foo"); + var connectionDetails = new PropertiesPulsarConnectionDetails(pulsarProps); + assertThat(connectionDetails.getPulsarAdminUrl()).isEqualTo("foo"); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java index 3710c9313cb7..ca05f2edde3e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java @@ -114,6 +114,7 @@ void whenCustomPulsarReaderAnnotationProcessorDefinedAutoConfigurationIsSkipped( @Test void autoConfiguresBeans() { this.contextRunner.run((context) -> assertThat(context).hasSingleBean(PulsarConfiguration.class) + .hasSingleBean(PulsarConnectionDetails.class) .hasSingleBean(DefaultPulsarClientFactory.class) .hasSingleBean(PulsarClient.class) .hasSingleBean(PulsarAdministration.class) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfigurationTests.java index 70536effacac..e014eabd9df3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfigurationTests.java @@ -51,6 +51,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** @@ -67,6 +68,15 @@ class PulsarConfigurationTests { .withConfiguration(AutoConfigurations.of(PulsarConfiguration.class)) .withBean(PulsarClient.class, () -> mock(PulsarClient.class)); + @Test + void whenHasUserDefinedConnectionDetailsBeanDoesNotAutoConfigureBean() { + PulsarConnectionDetails customConnectionDetails = mock(PulsarConnectionDetails.class); + this.contextRunner + .withBean("customPulsarConnectionDetails", PulsarConnectionDetails.class, () -> customConnectionDetails) + .run((context) -> assertThat(context).getBean(PulsarConnectionDetails.class) + .isSameAs(customConnectionDetails)); + } + @Nested class ClientTests { @@ -86,17 +96,36 @@ void whenHasUserDefinedClientBeanDoesNotAutoConfigureBean() { .run((context) -> assertThat(context).getBean(PulsarClient.class).isSameAs(customClient)); } + @Test + void whenConnectionDetailsAreNullTheyAreNotApplied() { + PulsarConnectionDetails connectionDetails = mock(PulsarConnectionDetails.class); + given(connectionDetails.getPulsarBrokerUrl()).willReturn(null); + PulsarConfigurationTests.this.contextRunner.withBean(PulsarConnectionDetails.class, () -> connectionDetails) + .withPropertyValues("spring.pulsar.client.service-url=fromPropsCustomizer") + .run((context) -> { + DefaultPulsarClientFactory clientFactory = context.getBean(DefaultPulsarClientFactory.class); + Customizers customizers = Customizers + .of(ClientBuilder.class, PulsarClientBuilderCustomizer::customize); + assertThat(customizers.fromField(clientFactory, "customizer")) + .callsInOrder(ClientBuilder::serviceUrl, "fromPropsCustomizer"); + }); + } + @Test void whenHasUserDefinedCustomizersAppliesInCorrectOrder() { + PulsarConnectionDetails connectionDetails = mock(PulsarConnectionDetails.class); + given(connectionDetails.getPulsarBrokerUrl()).willReturn("fromConnectionDetailsCustomizer"); PulsarConfigurationTests.this.contextRunner .withUserConfiguration(PulsarClientBuilderCustomizersConfig.class) + .withBean(PulsarConnectionDetails.class, () -> connectionDetails) .withPropertyValues("spring.pulsar.client.service-url=fromPropsCustomizer") .run((context) -> { DefaultPulsarClientFactory clientFactory = context.getBean(DefaultPulsarClientFactory.class); Customizers customizers = Customizers .of(ClientBuilder.class, PulsarClientBuilderCustomizer::customize); assertThat(customizers.fromField(clientFactory, "customizer")).callsInOrder( - ClientBuilder::serviceUrl, "fromPropsCustomizer", "fromCustomizer1", "fromCustomizer2"); + ClientBuilder::serviceUrl, "fromPropsCustomizer", "fromConnectionDetailsCustomizer", + "fromCustomizer1", "fromCustomizer2"); }); } @@ -133,17 +162,35 @@ void whenHasUserDefinedBeanDoesNotAutoConfigureBean() { .isSameAs(pulsarAdministration)); } + @Test + void whenConnectionDetailsAreNullTheyAreNotApplied() { + PulsarConnectionDetails connectionDetails = mock(PulsarConnectionDetails.class); + given(connectionDetails.getPulsarAdminUrl()).willReturn(null); + PulsarConfigurationTests.this.contextRunner.withBean(PulsarConnectionDetails.class, () -> connectionDetails) + .withPropertyValues("spring.pulsar.admin.service-url=fromPropsCustomizer") + .run((context) -> { + PulsarAdministration pulsarAdmin = context.getBean(PulsarAdministration.class); + Customizers customizers = Customizers + .of(PulsarAdminBuilder.class, PulsarAdminBuilderCustomizer::customize); + assertThat(customizers.fromField(pulsarAdmin, "adminCustomizers")) + .callsInOrder(PulsarAdminBuilder::serviceHttpUrl, "fromPropsCustomizer"); + }); + } + @Test void whenHasUserDefinedCustomizersAppliesInCorrectOrder() { + PulsarConnectionDetails connectionDetails = mock(PulsarConnectionDetails.class); + given(connectionDetails.getPulsarAdminUrl()).willReturn("fromConnectionDetailsCustomizer"); this.contextRunner.withUserConfiguration(PulsarAdminBuilderCustomizersConfig.class) + .withBean(PulsarConnectionDetails.class, () -> connectionDetails) .withPropertyValues("spring.pulsar.admin.service-url=fromPropsCustomizer") .run((context) -> { PulsarAdministration pulsarAdmin = context.getBean(PulsarAdministration.class); Customizers customizers = Customizers .of(PulsarAdminBuilder.class, PulsarAdminBuilderCustomizer::customize); assertThat(customizers.fromField(pulsarAdmin, "adminCustomizers")).callsInOrder( - PulsarAdminBuilder::serviceHttpUrl, "fromPropsCustomizer", "fromCustomizer1", - "fromCustomizer2"); + PulsarAdminBuilder::serviceHttpUrl, "fromPropsCustomizer", + "fromConnectionDetailsCustomizer", "fromCustomizer1", "fromCustomizer2"); }); } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/pulsar/PulsarDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/pulsar/PulsarDockerComposeConnectionDetailsFactory.java new file mode 100644 index 000000000000..c9817d348519 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/pulsar/PulsarDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.pulsar; + +import org.springframework.boot.autoconfigure.pulsar.PulsarConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link PulsarConnectionDetails} + * for a {@code pulsar} service. + * + * @author Chris Bono + */ +class PulsarDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + private static final int PULSAR_BROKER_PORT = 6650; + + private static final int PULSAR_ADMIN_PORT = 8080; + + PulsarDockerComposeConnectionDetailsFactory() { + super("apachepulsar/pulsar"); + } + + @Override + protected PulsarConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new PulsarDockerComposeConnectionDetails(source.getRunningService()); + } + + /** + * {@link PulsarConnectionDetails} backed by a {@code pulsar} {@link RunningService}. + */ + static class PulsarDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements PulsarConnectionDetails { + + private final String brokerUrl; + + private final String adminUrl; + + PulsarDockerComposeConnectionDetails(RunningService service) { + super(service); + this.brokerUrl = "pulsar://%s:%s".formatted(service.host(), service.ports().get(PULSAR_BROKER_PORT)); + this.adminUrl = "http://%s:%s".formatted(service.host(), service.ports().get(PULSAR_ADMIN_PORT)); + } + + @Override + public String getPulsarBrokerUrl() { + return this.brokerUrl; + } + + @Override + public String getPulsarAdminUrl() { + return this.adminUrl; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/pulsar/package-info.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/pulsar/package-info.java new file mode 100644 index 000000000000..7d8c4d1b1a56 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/pulsar/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Auto-configuration for docker compose Pulsar service connections. + */ +package org.springframework.boot.docker.compose.service.connection.pulsar; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories index cd5dc75bb4cb..cf5ad6c25a22 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories @@ -19,9 +19,9 @@ org.springframework.boot.docker.compose.service.connection.oracle.OracleJdbcDock org.springframework.boot.docker.compose.service.connection.oracle.OracleR2dbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.postgres.PostgresJdbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.postgres.PostgresR2dbcDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.pulsar.PulsarDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.rabbit.RabbitDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.redis.RedisDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.sqlserver.SqlServerJdbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.sqlserver.SqlServerR2dbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.zipkin.ZipkinDockerComposeConnectionDetailsFactory - diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/pulsar/PulsarDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/pulsar/PulsarDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..ed509613f5d4 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/pulsar/PulsarDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2023-2023 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.docker.compose.service.connection.pulsar; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.pulsar.PulsarConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test for {@link PulsarDockerComposeConnectionDetailsFactory}. + * + * @author Chris Bono + */ +class PulsarDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { + + PulsarDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("pulsar-compose.yaml", DockerImageNames.pulsar()); + } + + @Test + void runCreatesConnectionDetails() { + PulsarConnectionDetails connectionDetails = run(PulsarConnectionDetails.class); + assertThat(connectionDetails).isNotNull(); + assertThat(connectionDetails.getPulsarBrokerUrl()).matches("^pulsar:\\/\\/\\S+:\\d+"); + assertThat(connectionDetails.getPulsarAdminUrl()).matches("^http:\\/\\/\\S+:\\d+"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/pulsar/pulsar-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/pulsar/pulsar-compose.yaml new file mode 100644 index 000000000000..76cdd274f431 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/pulsar/pulsar-compose.yaml @@ -0,0 +1,9 @@ +services: + pulsar: + image: '{imageName}' + ports: + - '8080' + - '6650' + command: bin/pulsar standalone + healthcheck: + test: curl http://127.0.0.1:8080/admin/v2/namespaces/public/default diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc index 06f34b69a988..4caedf396060 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc @@ -76,6 +76,9 @@ The following service connections are currently supported: | `MongoConnectionDetails` | Containers named "mongo" +| `PulsarConnectionDetails` +| Containers named "apachepulsar/pulsar" + | `R2dbcConnectionDetails` | Containers named "gvenzl/oracle-xe", "mariadb", "mssql/server", "mysql", or "postgres" diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc index f5ab2b2ac430..9af81d8d44d7 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc @@ -992,6 +992,9 @@ The following service connection factories are provided in the `spring-boot-test | `Neo4jConnectionDetails` | Containers of type `Neo4jContainer` +| `PulsarConnectionDetails` +| Containers of type `PulsarContainer` + | `R2dbcConnectionDetails` | Containers of type `MariaDBContainer`, `MSSQLServerContainer`, `MySQLContainer`, `OracleContainer`, or `PostgreSQLContainer` diff --git a/spring-boot-project/spring-boot-testcontainers/build.gradle b/spring-boot-project/spring-boot-testcontainers/build.gradle index 2d20062409a8..67fdb9860384 100644 --- a/spring-boot-project/spring-boot-testcontainers/build.gradle +++ b/spring-boot-project/spring-boot-testcontainers/build.gradle @@ -29,6 +29,7 @@ dependencies { optional("org.testcontainers:neo4j") optional("org.testcontainers:oracle-xe") optional("org.testcontainers:postgresql") + optional("org.testcontainers:pulsar") optional("org.testcontainers:rabbitmq") optional("org.testcontainers:redpanda") optional("org.testcontainers:r2dbc") @@ -50,8 +51,13 @@ dependencies { testImplementation("org.springframework:spring-r2dbc") testImplementation("org.springframework.amqp:spring-rabbit") testImplementation("org.springframework.kafka:spring-kafka") + testImplementation("org.springframework.pulsar:spring-pulsar") testImplementation("org.testcontainers:junit-jupiter") testRuntimeOnly("com.oracle.database.r2dbc:oracle-r2dbc") } +test { + jvmArgs += "--add-opens=java.base/java.net=ALL-UNNAMED" + jvmArgs += "--add-opens=java.base/sun.net=ALL-UNNAMED" +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/pulsar/PulsarContainerConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/pulsar/PulsarContainerConnectionDetailsFactory.java new file mode 100644 index 000000000000..505a8e564e1d --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/pulsar/PulsarContainerConnectionDetailsFactory.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2023 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.testcontainers.service.connection.pulsar; + +import org.testcontainers.containers.PulsarContainer; + +import org.springframework.boot.autoconfigure.pulsar.PulsarConnectionDetails; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; + +/** + * {@link ContainerConnectionDetailsFactory} to create {@link PulsarConnectionDetails} + * from a {@link ServiceConnection @ServiceConnection}-annotated {@link PulsarContainer}. + * + * @author Chris Bono + */ +class PulsarContainerConnectionDetailsFactory + extends ContainerConnectionDetailsFactory { + + @Override + protected PulsarConnectionDetails getContainerConnectionDetails(ContainerConnectionSource source) { + return new PulsarContainerConnectionDetails(source); + } + + /** + * {@link PulsarConnectionDetails} backed by a {@link ContainerConnectionSource}. + */ + private static final class PulsarContainerConnectionDetails extends ContainerConnectionDetails + implements PulsarConnectionDetails { + + private PulsarContainerConnectionDetails(ContainerConnectionSource source) { + super(source); + } + + @Override + public String getPulsarBrokerUrl() { + return getContainer().getPulsarBrokerUrl(); + } + + @Override + public String getPulsarAdminUrl() { + return getContainer().getHttpServiceUrl(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/pulsar/package-info.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/pulsar/package-info.java new file mode 100644 index 000000000000..4938ad863134 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/pulsar/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Support for testcontainers Pulsar service connections. + */ +package org.springframework.boot.testcontainers.service.connection.pulsar; diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories index f26cc7230f68..e005e992812a 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories @@ -19,6 +19,7 @@ org.springframework.boot.testcontainers.service.connection.kafka.KafkaContainerC org.springframework.boot.testcontainers.service.connection.liquibase.LiquibaseContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.mongo.MongoContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.neo4j.Neo4jContainerConnectionDetailsFactory,\ +org.springframework.boot.testcontainers.service.connection.pulsar.PulsarContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.r2dbc.MariaDbR2dbcContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.r2dbc.MySqlR2dbcContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.r2dbc.OracleR2dbcContainerConnectionDetailsFactory,\ diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/pulsar/PulsarContainerConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/pulsar/PulsarContainerConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..51f5ec2a1364 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/pulsar/PulsarContainerConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2012-2023 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.testcontainers.service.connection.pulsar; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import org.apache.pulsar.client.api.PulsarClientException; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.PulsarContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.pulsar.PulsarAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.pulsar.annotation.PulsarListener; +import org.springframework.pulsar.core.PulsarTemplate; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PulsarContainerConnectionDetailsFactory}. + * + * @author Chris Bono + */ +@SpringJUnitConfig +@Testcontainers(disabledWithoutDocker = true) +@TestPropertySource(properties = { "spring.pulsar.consumer.subscription.initial-position=earliest" }) +class PulsarContainerConnectionDetailsFactoryIntegrationTests { + + @Container + @ServiceConnection + @SuppressWarnings("unused") + static final PulsarContainer PULSAR = new PulsarContainer(DockerImageNames.pulsar()) + .withStartupTimeout(Duration.ofMinutes(3)); + + @Autowired + private PulsarTemplate pulsarTemplate; + + @Autowired + private TestListener listener; + + @Test + void connectionCanBeMadeToPulsarContainer() throws PulsarClientException { + this.pulsarTemplate.send("test-topic", "test-data"); + Awaitility.waitAtMost(Duration.ofSeconds(30)) + .untilAsserted(() -> assertThat(this.listener.messages).containsExactly("test-data")); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(PulsarAutoConfiguration.class) + static class TestConfiguration { + + @Bean + TestListener testListener() { + return new TestListener(); + } + + } + + static class TestListener { + + private final List messages = new ArrayList<>(); + + @PulsarListener(topics = "test-topic") + void processMessage(String message) { + this.messages.add(message); + } + + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/build.gradle index 6058d1127ae1..a0051d3f4ea1 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/build.gradle @@ -10,6 +10,7 @@ dependencies { implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-pulsar-reactive")) testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + testImplementation(project(":spring-boot-project:spring-boot-testcontainers")) testImplementation("org.awaitility:awaitility") testImplementation("org.testcontainers:junit-jupiter") testImplementation("org.testcontainers:pulsar") diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/test/java/smoketest/pulsar/SamplePulsarApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/test/java/smoketest/pulsar/SamplePulsarApplicationTests.java index b32be31dfe2f..a7e6734e0dbc 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/test/java/smoketest/pulsar/SamplePulsarApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-pulsar/src/test/java/smoketest/pulsar/SamplePulsarApplicationTests.java @@ -32,10 +32,9 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.testcontainers.DockerImageNames; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; import static org.assertj.core.api.Assertions.assertThat; @@ -44,15 +43,11 @@ class SamplePulsarApplicationTests { @Container + @ServiceConnection + @SuppressWarnings("unused") static final PulsarContainer container = new PulsarContainer(DockerImageNames.pulsar()).withStartupAttempts(2) .withStartupTimeout(Duration.ofMinutes(3)); - @DynamicPropertySource - static void pulsarProperties(DynamicPropertyRegistry registry) { - registry.add("spring.pulsar.client.service-url", container::getPulsarBrokerUrl); - registry.add("spring.pulsar.admin.service-url", container::getHttpServiceUrl); - } - abstract class PulsarApplication { private final String type; From 750c5972255161f9314b0fea5d2141f6f49bf4bb Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 6 Sep 2023 11:57:03 -0700 Subject: [PATCH 0376/1215] Polish 'Add Pulsar ConnectionDetails support' See gh-37197 --- .../spring-boot-autoconfigure/build.gradle | 1 - .../PropertiesPulsarConnectionDetails.java | 4 +- .../pulsar/PulsarConfiguration.java | 18 +------- .../pulsar/PulsarConnectionDetails.java | 12 ++--- .../pulsar/PulsarPropertiesMapper.java | 9 ++-- ...ropertiesPulsarConnectionDetailsTests.java | 20 ++++----- .../pulsar/PulsarConfigurationTests.java | 45 +++---------------- .../pulsar/PulsarPropertiesMapperTests.java | 35 +++++++++++++-- ...DockerComposeConnectionDetailsFactory.java | 14 +++--- ...nectionDetailsFactoryIntegrationTests.java | 4 +- .../spring-boot-testcontainers/build.gradle | 5 --- ...lsarContainerConnectionDetailsFactory.java | 4 +- .../pulsar/SamplePulsarApplicationTests.java | 1 - 13 files changed, 74 insertions(+), 98 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index f88acc52eed2..b48da7b7bf45 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -277,5 +277,4 @@ tasks.named("checkSpringConfigurationMetadata").configure { test { jvmArgs += "--add-opens=java.base/java.net=ALL-UNNAMED" - jvmArgs += "--add-opens=java.base/sun.net=ALL-UNNAMED" } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PropertiesPulsarConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PropertiesPulsarConnectionDetails.java index 6865e3a5e6b0..51ed0fadc322 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PropertiesPulsarConnectionDetails.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PropertiesPulsarConnectionDetails.java @@ -30,12 +30,12 @@ class PropertiesPulsarConnectionDetails implements PulsarConnectionDetails { } @Override - public String getPulsarBrokerUrl() { + public String getBrokerUrl() { return this.pulsarProperties.getClient().getServiceUrl(); } @Override - public String getPulsarAdminUrl() { + public String getAdminUrl() { return this.pulsarProperties.getAdmin().getServiceUrl(); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfiguration.java index 6a5baf469fd0..64efd410ccfa 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfiguration.java @@ -82,20 +82,13 @@ PropertiesPulsarConnectionDetails pulsarConnectionDetails() { DefaultPulsarClientFactory pulsarClientFactory(PulsarConnectionDetails connectionDetails, ObjectProvider customizersProvider) { List allCustomizers = new ArrayList<>(); - allCustomizers.add(this.propertiesMapper::customizeClientBuilder); - allCustomizers.add((clientBuilder) -> this.applyConnectionDetails(connectionDetails, clientBuilder)); + allCustomizers.add((builder) -> this.propertiesMapper.customizeClientBuilder(builder, connectionDetails)); allCustomizers.addAll(customizersProvider.orderedStream().toList()); DefaultPulsarClientFactory clientFactory = new DefaultPulsarClientFactory( (clientBuilder) -> applyClientBuilderCustomizers(allCustomizers, clientBuilder)); return clientFactory; } - private void applyConnectionDetails(PulsarConnectionDetails connectionDetails, ClientBuilder clientBuilder) { - if (connectionDetails.getPulsarBrokerUrl() != null) { - clientBuilder.serviceUrl(connectionDetails.getPulsarBrokerUrl()); - } - } - private void applyClientBuilderCustomizers(List customizers, ClientBuilder clientBuilder) { customizers.forEach((customizer) -> customizer.customize(clientBuilder)); @@ -112,18 +105,11 @@ PulsarClient pulsarClient(PulsarClientFactory clientFactory) throws PulsarClient PulsarAdministration pulsarAdministration(PulsarConnectionDetails connectionDetails, ObjectProvider pulsarAdminBuilderCustomizers) { List allCustomizers = new ArrayList<>(); - allCustomizers.add(this.propertiesMapper::customizeAdminBuilder); - allCustomizers.add((adminBuilder) -> this.applyConnectionDetails(connectionDetails, adminBuilder)); + allCustomizers.add((builder) -> this.propertiesMapper.customizeAdminBuilder(builder, connectionDetails)); allCustomizers.addAll(pulsarAdminBuilderCustomizers.orderedStream().toList()); return new PulsarAdministration((adminBuilder) -> applyAdminBuilderCustomizers(allCustomizers, adminBuilder)); } - private void applyConnectionDetails(PulsarConnectionDetails connectionDetails, PulsarAdminBuilder adminBuilder) { - if (connectionDetails.getPulsarAdminUrl() != null) { - adminBuilder.serviceHttpUrl(connectionDetails.getPulsarAdminUrl()); - } - } - private void applyAdminBuilderCustomizers(List customizers, PulsarAdminBuilder adminBuilder) { customizers.forEach((customizer) -> customizer.customize(adminBuilder)); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarConnectionDetails.java index 567134b77a04..1d21f5802e46 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarConnectionDetails.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarConnectionDetails.java @@ -27,15 +27,15 @@ public interface PulsarConnectionDetails extends ConnectionDetails { /** - * Returns the Pulsar service URL for the broker. - * @return the Pulsar service URL for the broker + * URL used to connect to the broker. + * @return the service URL */ - String getPulsarBrokerUrl(); + String getBrokerUrl(); /** - * Returns the Pulsar web URL for the admin endpoint. - * @return the Pulsar web URL for the admin endpoint + * URL user to connect to the admin endpoint. + * @return the admin URL */ - String getPulsarAdminUrl(); + String getAdminUrl(); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java index 77a6b6321269..04246d0e91c2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java @@ -49,21 +49,20 @@ final class PulsarPropertiesMapper { this.properties = properties; } - void customizeClientBuilder(ClientBuilder clientBuilder) { + void customizeClientBuilder(ClientBuilder clientBuilder, PulsarConnectionDetails connectionDetails) { PulsarProperties.Client properties = this.properties.getClient(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(properties::getServiceUrl).to(clientBuilder::serviceUrl); - + map.from(connectionDetails::getBrokerUrl).to(clientBuilder::serviceUrl); map.from(properties::getConnectionTimeout).to(timeoutProperty(clientBuilder::connectionTimeout)); map.from(properties::getOperationTimeout).to(timeoutProperty(clientBuilder::operationTimeout)); map.from(properties::getLookupTimeout).to(timeoutProperty(clientBuilder::lookupTimeout)); customizeAuthentication(clientBuilder::authentication, properties.getAuthentication()); } - void customizeAdminBuilder(PulsarAdminBuilder adminBuilder) { + void customizeAdminBuilder(PulsarAdminBuilder adminBuilder, PulsarConnectionDetails connectionDetails) { PulsarProperties.Admin properties = this.properties.getAdmin(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(properties::getServiceUrl).to(adminBuilder::serviceHttpUrl); + map.from(connectionDetails::getAdminUrl).to(adminBuilder::serviceHttpUrl); map.from(properties::getConnectionTimeout).to(timeoutProperty(adminBuilder::connectionTimeout)); map.from(properties::getReadTimeout).to(timeoutProperty(adminBuilder::readTimeout)); map.from(properties::getRequestTimeout).to(timeoutProperty(adminBuilder::requestTimeout)); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PropertiesPulsarConnectionDetailsTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PropertiesPulsarConnectionDetailsTests.java index 8fed356282f6..3abff9be7346 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PropertiesPulsarConnectionDetailsTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PropertiesPulsarConnectionDetailsTests.java @@ -28,19 +28,19 @@ class PropertiesPulsarConnectionDetailsTests { @Test - void pulsarBrokerUrlIsObtainedFromPulsarProperties() { - var pulsarProps = new PulsarProperties(); - pulsarProps.getClient().setServiceUrl("foo"); - var connectionDetails = new PropertiesPulsarConnectionDetails(pulsarProps); - assertThat(connectionDetails.getPulsarBrokerUrl()).isEqualTo("foo"); + void getClientServiceUrlReturnsValueFromProperties() { + PulsarProperties properties = new PulsarProperties(); + properties.getClient().setServiceUrl("foo"); + PulsarConnectionDetails connectionDetails = new PropertiesPulsarConnectionDetails(properties); + assertThat(connectionDetails.getBrokerUrl()).isEqualTo("foo"); } @Test - void pulsarAdminUrlIsObtainedFromPulsarProperties() { - var pulsarProps = new PulsarProperties(); - pulsarProps.getAdmin().setServiceUrl("foo"); - var connectionDetails = new PropertiesPulsarConnectionDetails(pulsarProps); - assertThat(connectionDetails.getPulsarAdminUrl()).isEqualTo("foo"); + void getAdminServiceHttpUrlReturnsValueFromProperties() { + PulsarProperties properties = new PulsarProperties(); + properties.getAdmin().setServiceUrl("foo"); + PulsarConnectionDetails connectionDetails = new PropertiesPulsarConnectionDetails(properties); + assertThat(connectionDetails.getAdminUrl()).isEqualTo("foo"); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfigurationTests.java index e014eabd9df3..a1136b11ba29 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfigurationTests.java @@ -96,36 +96,20 @@ void whenHasUserDefinedClientBeanDoesNotAutoConfigureBean() { .run((context) -> assertThat(context).getBean(PulsarClient.class).isSameAs(customClient)); } - @Test - void whenConnectionDetailsAreNullTheyAreNotApplied() { - PulsarConnectionDetails connectionDetails = mock(PulsarConnectionDetails.class); - given(connectionDetails.getPulsarBrokerUrl()).willReturn(null); - PulsarConfigurationTests.this.contextRunner.withBean(PulsarConnectionDetails.class, () -> connectionDetails) - .withPropertyValues("spring.pulsar.client.service-url=fromPropsCustomizer") - .run((context) -> { - DefaultPulsarClientFactory clientFactory = context.getBean(DefaultPulsarClientFactory.class); - Customizers customizers = Customizers - .of(ClientBuilder.class, PulsarClientBuilderCustomizer::customize); - assertThat(customizers.fromField(clientFactory, "customizer")) - .callsInOrder(ClientBuilder::serviceUrl, "fromPropsCustomizer"); - }); - } - @Test void whenHasUserDefinedCustomizersAppliesInCorrectOrder() { PulsarConnectionDetails connectionDetails = mock(PulsarConnectionDetails.class); - given(connectionDetails.getPulsarBrokerUrl()).willReturn("fromConnectionDetailsCustomizer"); + given(connectionDetails.getBrokerUrl()).willReturn("connectiondetails"); PulsarConfigurationTests.this.contextRunner .withUserConfiguration(PulsarClientBuilderCustomizersConfig.class) .withBean(PulsarConnectionDetails.class, () -> connectionDetails) - .withPropertyValues("spring.pulsar.client.service-url=fromPropsCustomizer") + .withPropertyValues("spring.pulsar.client.service-url=properties") .run((context) -> { DefaultPulsarClientFactory clientFactory = context.getBean(DefaultPulsarClientFactory.class); Customizers customizers = Customizers .of(ClientBuilder.class, PulsarClientBuilderCustomizer::customize); assertThat(customizers.fromField(clientFactory, "customizer")).callsInOrder( - ClientBuilder::serviceUrl, "fromPropsCustomizer", "fromConnectionDetailsCustomizer", - "fromCustomizer1", "fromCustomizer2"); + ClientBuilder::serviceUrl, "connectiondetails", "fromCustomizer1", "fromCustomizer2"); }); } @@ -162,35 +146,20 @@ void whenHasUserDefinedBeanDoesNotAutoConfigureBean() { .isSameAs(pulsarAdministration)); } - @Test - void whenConnectionDetailsAreNullTheyAreNotApplied() { - PulsarConnectionDetails connectionDetails = mock(PulsarConnectionDetails.class); - given(connectionDetails.getPulsarAdminUrl()).willReturn(null); - PulsarConfigurationTests.this.contextRunner.withBean(PulsarConnectionDetails.class, () -> connectionDetails) - .withPropertyValues("spring.pulsar.admin.service-url=fromPropsCustomizer") - .run((context) -> { - PulsarAdministration pulsarAdmin = context.getBean(PulsarAdministration.class); - Customizers customizers = Customizers - .of(PulsarAdminBuilder.class, PulsarAdminBuilderCustomizer::customize); - assertThat(customizers.fromField(pulsarAdmin, "adminCustomizers")) - .callsInOrder(PulsarAdminBuilder::serviceHttpUrl, "fromPropsCustomizer"); - }); - } - @Test void whenHasUserDefinedCustomizersAppliesInCorrectOrder() { PulsarConnectionDetails connectionDetails = mock(PulsarConnectionDetails.class); - given(connectionDetails.getPulsarAdminUrl()).willReturn("fromConnectionDetailsCustomizer"); + given(connectionDetails.getAdminUrl()).willReturn("connectiondetails"); this.contextRunner.withUserConfiguration(PulsarAdminBuilderCustomizersConfig.class) .withBean(PulsarConnectionDetails.class, () -> connectionDetails) - .withPropertyValues("spring.pulsar.admin.service-url=fromPropsCustomizer") + .withPropertyValues("spring.pulsar.admin.service-url=property") .run((context) -> { PulsarAdministration pulsarAdmin = context.getBean(PulsarAdministration.class); Customizers customizers = Customizers .of(PulsarAdminBuilder.class, PulsarAdminBuilderCustomizer::customize); assertThat(customizers.fromField(pulsarAdmin, "adminCustomizers")).callsInOrder( - PulsarAdminBuilder::serviceHttpUrl, "fromPropsCustomizer", - "fromConnectionDetailsCustomizer", "fromCustomizer1", "fromCustomizer2"); + PulsarAdminBuilder::serviceHttpUrl, "connectiondetails", "fromCustomizer1", + "fromCustomizer2"); }); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java index 283e06c1d45c..b168d4f71306 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java @@ -41,6 +41,7 @@ import org.springframework.pulsar.listener.PulsarContainerProperties; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; @@ -60,7 +61,8 @@ void customizeClientBuilderWhenHasNoAuthentication() { properties.getClient().setOperationTimeout(Duration.ofSeconds(2)); properties.getClient().setLookupTimeout(Duration.ofSeconds(3)); ClientBuilder builder = mock(ClientBuilder.class); - new PulsarPropertiesMapper(properties).customizeClientBuilder(builder); + new PulsarPropertiesMapper(properties).customizeClientBuilder(builder, + new PropertiesPulsarConnectionDetails(properties)); then(builder).should().serviceUrl("https://example.com"); then(builder).should().connectionTimeout(1000, TimeUnit.MILLISECONDS); then(builder).should().operationTimeout(2000, TimeUnit.MILLISECONDS); @@ -74,10 +76,22 @@ void customizeClientBuilderWhenHasAuthentication() throws UnsupportedAuthenticat properties.getClient().getAuthentication().setPluginClassName("myclass"); properties.getClient().getAuthentication().setParam(params); ClientBuilder builder = mock(ClientBuilder.class); - new PulsarPropertiesMapper(properties).customizeClientBuilder(builder); + new PulsarPropertiesMapper(properties).customizeClientBuilder(builder, + new PropertiesPulsarConnectionDetails(properties)); then(builder).should().authentication("myclass", params); } + @Test + void customizeClientBuilderWhenHasConnectionDetails() { + PulsarProperties properties = new PulsarProperties(); + properties.getClient().setServiceUrl("https://ignored.example.com"); + ClientBuilder builder = mock(ClientBuilder.class); + PulsarConnectionDetails connectionDetails = mock(PulsarConnectionDetails.class); + given(connectionDetails.getBrokerUrl()).willReturn("https://used.example.com"); + new PulsarPropertiesMapper(properties).customizeClientBuilder(builder, connectionDetails); + then(builder).should().serviceUrl("https://used.example.com"); + } + @Test void customizeAdminBuilderWhenHasNoAuthentication() { PulsarProperties properties = new PulsarProperties(); @@ -86,7 +100,8 @@ void customizeAdminBuilderWhenHasNoAuthentication() { properties.getAdmin().setReadTimeout(Duration.ofSeconds(2)); properties.getAdmin().setRequestTimeout(Duration.ofSeconds(3)); PulsarAdminBuilder builder = mock(PulsarAdminBuilder.class); - new PulsarPropertiesMapper(properties).customizeAdminBuilder(builder); + new PulsarPropertiesMapper(properties).customizeAdminBuilder(builder, + new PropertiesPulsarConnectionDetails(properties)); then(builder).should().serviceHttpUrl("https://example.com"); then(builder).should().connectionTimeout(1000, TimeUnit.MILLISECONDS); then(builder).should().readTimeout(2000, TimeUnit.MILLISECONDS); @@ -100,10 +115,22 @@ void customizeAdminBuilderWhenHasAuthentication() throws UnsupportedAuthenticati properties.getAdmin().getAuthentication().setPluginClassName("myclass"); properties.getAdmin().getAuthentication().setParam(params); PulsarAdminBuilder builder = mock(PulsarAdminBuilder.class); - new PulsarPropertiesMapper(properties).customizeAdminBuilder(builder); + new PulsarPropertiesMapper(properties).customizeAdminBuilder(builder, + new PropertiesPulsarConnectionDetails(properties)); then(builder).should().authentication("myclass", params); } + @Test + void customizeAdminBuilderWhenHasConnectionDetails() { + PulsarProperties properties = new PulsarProperties(); + properties.getAdmin().setServiceUrl("https://ignored.example.com"); + PulsarAdminBuilder builder = mock(PulsarAdminBuilder.class); + PulsarConnectionDetails connectionDetails = mock(PulsarConnectionDetails.class); + given(connectionDetails.getAdminUrl()).willReturn("https://used.example.com"); + new PulsarPropertiesMapper(properties).customizeAdminBuilder(builder, connectionDetails); + then(builder).should().serviceHttpUrl("https://used.example.com"); + } + @Test @SuppressWarnings("unchecked") void customizeProducerBuilder() { diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/pulsar/PulsarDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/pulsar/PulsarDockerComposeConnectionDetailsFactory.java index c9817d348519..0568a9811933 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/pulsar/PulsarDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/pulsar/PulsarDockerComposeConnectionDetailsFactory.java @@ -17,6 +17,7 @@ package org.springframework.boot.docker.compose.service.connection.pulsar; import org.springframework.boot.autoconfigure.pulsar.PulsarConnectionDetails; +import org.springframework.boot.docker.compose.core.ConnectionPorts; import org.springframework.boot.docker.compose.core.RunningService; import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; @@ -30,9 +31,9 @@ class PulsarDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { - private static final int PULSAR_BROKER_PORT = 6650; + private static final int BROKER_PORT = 6650; - private static final int PULSAR_ADMIN_PORT = 8080; + private static final int ADMIN_PORT = 8080; PulsarDockerComposeConnectionDetailsFactory() { super("apachepulsar/pulsar"); @@ -55,17 +56,18 @@ static class PulsarDockerComposeConnectionDetails extends DockerComposeConnectio PulsarDockerComposeConnectionDetails(RunningService service) { super(service); - this.brokerUrl = "pulsar://%s:%s".formatted(service.host(), service.ports().get(PULSAR_BROKER_PORT)); - this.adminUrl = "http://%s:%s".formatted(service.host(), service.ports().get(PULSAR_ADMIN_PORT)); + ConnectionPorts ports = service.ports(); + this.brokerUrl = "pulsar://%s:%s".formatted(service.host(), ports.get(BROKER_PORT)); + this.adminUrl = "http://%s:%s".formatted(service.host(), ports.get(ADMIN_PORT)); } @Override - public String getPulsarBrokerUrl() { + public String getBrokerUrl() { return this.brokerUrl; } @Override - public String getPulsarAdminUrl() { + public String getAdminUrl() { return this.adminUrl; } diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/pulsar/PulsarDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/pulsar/PulsarDockerComposeConnectionDetailsFactoryIntegrationTests.java index ed509613f5d4..c4c18d55ba0e 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/pulsar/PulsarDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/pulsar/PulsarDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -39,8 +39,8 @@ class PulsarDockerComposeConnectionDetailsFactoryIntegrationTests extends Abstra void runCreatesConnectionDetails() { PulsarConnectionDetails connectionDetails = run(PulsarConnectionDetails.class); assertThat(connectionDetails).isNotNull(); - assertThat(connectionDetails.getPulsarBrokerUrl()).matches("^pulsar:\\/\\/\\S+:\\d+"); - assertThat(connectionDetails.getPulsarAdminUrl()).matches("^http:\\/\\/\\S+:\\d+"); + assertThat(connectionDetails.getBrokerUrl()).matches("^pulsar:\\/\\/\\S+:\\d+"); + assertThat(connectionDetails.getAdminUrl()).matches("^http:\\/\\/\\S+:\\d+"); } } diff --git a/spring-boot-project/spring-boot-testcontainers/build.gradle b/spring-boot-project/spring-boot-testcontainers/build.gradle index 67fdb9860384..d09e6d485f29 100644 --- a/spring-boot-project/spring-boot-testcontainers/build.gradle +++ b/spring-boot-project/spring-boot-testcontainers/build.gradle @@ -56,8 +56,3 @@ dependencies { testRuntimeOnly("com.oracle.database.r2dbc:oracle-r2dbc") } - -test { - jvmArgs += "--add-opens=java.base/java.net=ALL-UNNAMED" - jvmArgs += "--add-opens=java.base/sun.net=ALL-UNNAMED" -} diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/pulsar/PulsarContainerConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/pulsar/PulsarContainerConnectionDetailsFactory.java index 505a8e564e1d..836c1a127d23 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/pulsar/PulsarContainerConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/pulsar/PulsarContainerConnectionDetailsFactory.java @@ -48,12 +48,12 @@ private PulsarContainerConnectionDetails(ContainerConnectionSource Date: Thu, 7 Sep 2023 10:17:18 +0100 Subject: [PATCH 0377/1215] Start building against Micrometer 1.12.0 snapshots See gh-37226 --- .../export/dynatrace/DynatraceProperties.java | 16 +++++++++++++++- .../DynatracePropertiesConfigAdapter.java | 7 ++++++- .../dynatrace/DynatracePropertiesTests.java | 1 + .../spring-boot-dependencies/build.gradle | 2 +- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatraceProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatraceProperties.java index 91c710100843..189ef09a86b5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatraceProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatraceProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -140,6 +140,12 @@ public static class V2 { */ private boolean useDynatraceSummaryInstruments = true; + /** + * Whether to export meter metadata (unit and description) to the Dynatrace + * backend. + */ + private boolean exportMeterMetadata = true; + public Map getDefaultDimensions() { return this.defaultDimensions; } @@ -172,6 +178,14 @@ public void setUseDynatraceSummaryInstruments(boolean useDynatraceSummaryInstrum this.useDynatraceSummaryInstruments = useDynatraceSummaryInstruments; } + public boolean isExportMeterMetadata() { + return this.exportMeterMetadata; + } + + public void setExportMeterMetadata(boolean exportMeterMetadata) { + this.exportMeterMetadata = exportMeterMetadata; + } + } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatracePropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatracePropertiesConfigAdapter.java index 82135f989860..74d74affc96f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatracePropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatracePropertiesConfigAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -95,6 +95,11 @@ public boolean useDynatraceSummaryInstruments() { return get(v2(V2::isUseDynatraceSummaryInstruments), DynatraceConfig.super::useDynatraceSummaryInstruments); } + @Override + public boolean exportMeterMetadata() { + return (get(v2(V2::isExportMeterMetadata), DynatraceConfig.super::exportMeterMetadata)); + } + private Function v1(Function getter) { return (properties) -> getter.apply(properties.getV1()); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatracePropertiesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatracePropertiesTests.java index 69f3651bc30b..5a784ead6dd1 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatracePropertiesTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/dynatrace/DynatracePropertiesTests.java @@ -38,6 +38,7 @@ void defaultValuesAreConsistent() { assertThat(properties.getV1().getTechnologyType()).isEqualTo(config.technologyType()); assertThat(properties.getV2().isUseDynatraceSummaryInstruments()) .isEqualTo(config.useDynatraceSummaryInstruments()); + assertThat(properties.getV2().isExportMeterMetadata()).isEqualTo(config.exportMeterMetadata()); } } diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 25c059f1318e..edc037d50a25 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -988,7 +988,7 @@ bom { ] } } - library("Micrometer", "1.12.0-M2") { + library("Micrometer", "1.12.0-SNAPSHOT") { considerSnapshots() group("io.micrometer") { modules = [ From dfa5414486310a70d4838c96b477ba70f9c73df2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 Sep 2023 10:17:23 +0100 Subject: [PATCH 0378/1215] Start building against Reactor Bom 2023.0.0 snapshots See gh-37227 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index edc037d50a25..0a57cbc98ff8 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1319,7 +1319,7 @@ bom { ] } } - library("Reactor Bom", "2023.0.0-M2") { + library("Reactor Bom", "2023.0.0-SNAPSHOT") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From 318ac7adc5080ad50ac0239e315274a8e7183df7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 Sep 2023 10:17:28 +0100 Subject: [PATCH 0379/1215] Start building against Spring AMQP 3.1.0 snapshots See gh-37228 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 0a57cbc98ff8..fb4a255e9e5d 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1484,7 +1484,7 @@ bom { ] } } - library("Spring AMQP", "3.0.8") { + library("Spring AMQP", "3.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.amqp") { imports = [ From d88b999fa274221d62d602a3792e8b6bd8a7bee8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 Sep 2023 10:17:33 +0100 Subject: [PATCH 0380/1215] Start building against Spring Authorization Server 1.2.0 snapshots See gh-37229 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index fb4a255e9e5d..f3e3740bcd23 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1492,7 +1492,7 @@ bom { ] } } - library("Spring Authorization Server", "1.1.2") { + library("Spring Authorization Server", "1.2.0-SNAPSHOT") { considerSnapshots() group("org.springframework.security") { modules = [ From 4b0c8eba4cd1cfb90afdab79f9c300f18498a7fe Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 Sep 2023 10:17:37 +0100 Subject: [PATCH 0381/1215] Start building against Spring Batch 5.1.0 snapshots See gh-37230 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f3e3740bcd23..eef9a4f5c9fa 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1500,7 +1500,7 @@ bom { ] } } - library("Spring Batch", "5.1.0-M2") { + library("Spring Batch", "5.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.batch") { imports = [ From 02fd570b7d4cf730f13a992248367e866f83b958 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 Sep 2023 10:17:42 +0100 Subject: [PATCH 0382/1215] Start building against Spring Framework 6.1.0 snapshots See gh-37231 --- gradle.properties | 2 +- .../boot/context/config/ConfigDataLocationRuntimeHints.java | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index db1bd02fa44c..e9a6a13fef71 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 kotlinVersion=1.9.10 nativeBuildToolsVersion=0.9.24 -springFrameworkVersion=6.1.0-M4 +springFrameworkVersion=6.1.0-SNAPSHOT tomcatVersion=10.1.12 kotlin.stdlib.default.dependency=false diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationRuntimeHints.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationRuntimeHints.java index a3920df086a4..c914f935c260 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationRuntimeHints.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationRuntimeHints.java @@ -49,8 +49,10 @@ public void registerHints(RuntimeHints hints, ClassLoader classLoader) { logger.debug("Registering application configuration hints for " + fileNames + "(" + extensions + ") at " + locations); } - new FilePatternResourceHintsRegistrar(fileNames, locations, extensions).registerHints(hints.resources(), - classLoader); + FilePatternResourceHintsRegistrar.forClassPathLocations(locations.toArray(new String[0])) + .withFilePrefixes(fileNames.toArray(new String[0])) + .withFileExtensions(extensions.toArray(new String[0])) + .registerHints(hints.resources(), classLoader); } /** From ed5d16de8408be54b19134e00f24fb1bd2ab9d85 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Jan 2023 11:11:45 +0000 Subject: [PATCH 0383/1215] Upgrade to Jetty 12 Closes gh-36073 --- .../build.gradle | 4 +- .../JettyMetricsAutoConfigurationTests.java | 2 - .../spring-boot-autoconfigure/build.gradle | 16 +- ...verFactoryCustomizerAutoConfiguration.java | 2 +- .../JettyWebServerFactoryCustomizer.java | 29 +- ...ReactiveWebServerFactoryConfiguration.java | 12 +- .../ClientHttpConnectorAutoConfiguration.java | 1 - ...ientHttpConnectorFactoryConfiguration.java | 19 -- .../JettyClientHttpConnectorFactory.java | 64 ---- .../ServletWebServerFactoryConfiguration.java | 2 +- ...yWebSocketReactiveWebServerCustomizer.java | 20 +- .../WebSocketReactiveAutoConfiguration.java | 2 +- ...tyWebSocketServletWebServerCustomizer.java | 20 +- .../WebSocketServletAutoConfiguration.java | 4 +- .../jta/JtaAutoConfigurationTests.java | 2 + .../web/ServerPropertiesTests.java | 78 +---- .../JettyWebServerFactoryCustomizerTests.java | 2 - ...ebServerFactoryAutoConfigurationTests.java | 3 - ...ntHttpConnectorAutoConfigurationTests.java | 25 +- ...ttpConnectorFactoryConfigurationTests.java | 43 --- .../JettyClientHttpConnectorFactoryTests.java | 34 --- .../MultipartAutoConfigurationTests.java | 3 - ...ebServerFactoryAutoConfigurationTests.java | 3 - ...tWebServerServletContextListenerTests.java | 2 - ...bSocketReactiveAutoConfigurationTests.java | 4 +- ...ebSocketServletAutoConfigurationTests.java | 4 +- .../spring-boot-dependencies/build.gradle | 7 +- .../spring-boot-devtools/build.gradle | 4 +- .../getting-started/system-requirements.adoc | 4 +- .../spring-boot-starter-jetty/build.gradle | 16 +- .../servlet/Servlet5ClassPathOverrides.java | 41 --- spring-boot-project/spring-boot/build.gradle | 20 +- .../client/ClientHttpRequestFactories.java | 2 +- .../web/embedded/jetty/GracefulShutdown.java | 1 + .../web/embedded/jetty/JasperInitializer.java | 16 +- .../jetty/JettyEmbeddedErrorHandler.java | 24 +- .../jetty/JettyEmbeddedWebAppContext.java | 12 +- .../embedded/jetty/JettyHandlerWrappers.java | 30 +- .../jetty/JettyReactiveWebServerFactory.java | 9 +- .../jetty/JettyServletWebServerFactory.java | 273 ++++++++++++------ .../web/embedded/jetty/JettyWebServer.java | 34 ++- ...ervletContextInitializerConfiguration.java | 22 +- .../embedded/jetty/SslServerCustomizer.java | 14 +- .../JettyReactiveWebServerFactoryTests.java | 20 +- .../JettyServletWebServerFactoryTests.java | 46 +-- ...AbstractReactiveWebServerFactoryTests.java | 6 +- .../ServletComponentScanIntegrationTests.java | 4 +- .../ServletWebServerMvcIntegrationTests.java | 3 - .../AbstractServletWebServerFactoryTests.java | 19 +- .../spring-boot-server-tests-app/build.gradle | 10 +- .../build.gradle | 9 +- .../src/main/resources/application.properties | 3 +- .../build.gradle | 4 - .../spring-boot-smoke-test-jetty/build.gradle | 4 - .../src/main/resources/application.properties | 2 +- .../jetty/SampleJettyApplicationTests.java | 7 +- .../build.gradle | 4 - 57 files changed, 376 insertions(+), 694 deletions(-) delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/JettyClientHttpConnectorFactory.java delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/JettyClientHttpConnectorFactoryTests.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/web/servlet/Servlet5ClassPathOverrides.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index 777cab24fef4..6992d4185092 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -161,9 +161,7 @@ dependencies { testImplementation("org.assertj:assertj-core") testImplementation("org.awaitility:awaitility") testImplementation("org.cache2k:cache2k-api") - testImplementation("org.eclipse.jetty:jetty-webapp") { - exclude group: "org.eclipse.jetty.toolchain", module: "jetty-jakarta-servlet-api" - } + testImplementation("org.eclipse.jetty.ee10:jetty-ee10-webapp") testImplementation("org.glassfish.jersey.ext:jersey-spring6") testImplementation("org.glassfish.jersey.media:jersey-media-json-jackson") testImplementation("org.hamcrest:hamcrest") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/jetty/JettyMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/jetty/JettyMetricsAutoConfigurationTests.java index d53632c54410..0e8437a192be 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/jetty/JettyMetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/jetty/JettyMetricsAutoConfigurationTests.java @@ -31,7 +31,6 @@ import org.springframework.boot.context.event.ApplicationStartedEvent; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; -import org.springframework.boot.testsupport.web.servlet.Servlet5ClassPathOverrides; import org.springframework.boot.web.embedded.jetty.JettyReactiveWebServerFactory; import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext; @@ -50,7 +49,6 @@ * @author Andy Wilkinson * @author Chris Bono */ -@Servlet5ClassPathOverrides class JettyMetricsAutoConfigurationTests { @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index b48da7b7bf45..ed61c0213dd6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -78,20 +78,10 @@ dependencies { optional("nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect") optional("org.aspectj:aspectjweaver") optional("org.cache2k:cache2k-spring") - optional("org.eclipse.jetty:jetty-webapp") { - exclude(group: "org.eclipse.jetty", module: "jetty-jndi") - exclude(group: "org.eclipse.jetty.toolchain", module: "jetty-jakarta-servlet-api") - } + optional("org.eclipse.jetty.ee10:jetty-ee10-webapp") optional("org.eclipse.jetty:jetty-reactive-httpclient") - optional("org.eclipse.jetty.websocket:websocket-jakarta-server") { - exclude(group: "org.eclipse.jetty", module: "jetty-jndi") - exclude(group: "org.eclipse.jetty.toolchain", module: "jetty-jakarta-servlet-api") - exclude(group: "org.eclipse.jetty.toolchain", module: "jetty-jakarta-websocket-api") - } - optional("org.eclipse.jetty.websocket:websocket-jetty-server") { - exclude(group: "org.eclipse.jetty", module: "jetty-jndi") - exclude(group: "org.eclipse.jetty.toolchain", module: "jetty-jakarta-servlet-api") - } + optional("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-server") + optional("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jetty-server") optional("org.ehcache:ehcache") { artifact { classifier = 'jakarta' diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java index eb7be8fe2ff2..371ab5034526 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java @@ -19,9 +19,9 @@ import io.undertow.Undertow; import org.apache.catalina.startup.Tomcat; import org.apache.coyote.UpgradeProtocol; +import org.eclipse.jetty.ee10.webapp.WebAppContext; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.Loader; -import org.eclipse.jetty.webapp.WebAppContext; import org.xnio.SslClientAuthMode; import reactor.netty.http.server.HttpServer; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java index e53118c5d9af..c12333dc9cfc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizer.java @@ -18,7 +18,9 @@ import java.time.Duration; import java.util.Arrays; +import java.util.List; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.CustomRequestLog; @@ -26,9 +28,6 @@ import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.RequestLogWriter; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.handler.ContextHandler; -import org.eclipse.jetty.server.handler.HandlerCollection; -import org.eclipse.jetty.server.handler.HandlerWrapper; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.cloud.CloudPlatform; @@ -131,17 +130,21 @@ public void customize(Server server) { setHandlerMaxHttpFormPostSize(server.getHandlers()); } - private void setHandlerMaxHttpFormPostSize(Handler... handlers) { + private void setHandlerMaxHttpFormPostSize(List handlers) { for (Handler handler : handlers) { - if (handler instanceof ContextHandler contextHandler) { - contextHandler.setMaxFormContentSize(maxHttpFormPostSize); - } - else if (handler instanceof HandlerWrapper wrapper) { - setHandlerMaxHttpFormPostSize(wrapper.getHandler()); - } - else if (handler instanceof HandlerCollection collection) { - setHandlerMaxHttpFormPostSize(collection.getHandlers()); - } + setHandlerMaxHttpFormPostSize(handler); + } + } + + private void setHandlerMaxHttpFormPostSize(Handler handler) { + if (handler instanceof ServletContextHandler contextHandler) { + contextHandler.setMaxFormContentSize(maxHttpFormPostSize); + } + else if (handler instanceof Handler.Wrapper wrapper) { + setHandlerMaxHttpFormPostSize(wrapper.getHandler()); + } + else if (handler instanceof Handler.Collection collection) { + setHandlerMaxHttpFormPostSize(collection.getHandlers()); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryConfiguration.java index 2d92ced3d6e8..8752f5025fc4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryConfiguration.java @@ -17,7 +17,7 @@ package org.springframework.boot.autoconfigure.web.reactive; import io.undertow.Undertow; -import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.ee10.servlet.ServletHolder; import reactor.netty.http.server.HttpServer; import org.springframework.beans.factory.ObjectProvider; @@ -39,7 +39,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.http.client.reactive.JettyResourceFactory; import org.springframework.http.client.reactive.ReactorResourceFactory; /** @@ -97,17 +96,10 @@ TomcatReactiveWebServerFactory tomcatReactiveWebServerFactory( static class EmbeddedJetty { @Bean - @ConditionalOnMissingBean - JettyResourceFactory jettyServerResourceFactory() { - return new JettyResourceFactory(); - } - - @Bean - JettyReactiveWebServerFactory jettyReactiveWebServerFactory(JettyResourceFactory resourceFactory, + JettyReactiveWebServerFactory jettyReactiveWebServerFactory( ObjectProvider serverCustomizers) { JettyReactiveWebServerFactory serverFactory = new JettyReactiveWebServerFactory(); serverFactory.getServerCustomizers().addAll(serverCustomizers.orderedStream().toList()); - serverFactory.setResourceFactory(resourceFactory); return serverFactory; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorAutoConfiguration.java index 34bf67e605a8..308ac80f5031 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorAutoConfiguration.java @@ -46,7 +46,6 @@ @ConditionalOnClass(WebClient.class) @AutoConfigureAfter(SslAutoConfiguration.class) @Import({ ClientHttpConnectorFactoryConfiguration.ReactorNetty.class, - ClientHttpConnectorFactoryConfiguration.JettyClient.class, ClientHttpConnectorFactoryConfiguration.HttpClient5.class, ClientHttpConnectorFactoryConfiguration.JdkClient.class }) public class ClientHttpConnectorAutoConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorFactoryConfiguration.java index cf0769bc0cda..e5ea5cca20ba 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorFactoryConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorFactoryConfiguration.java @@ -27,7 +27,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.http.client.reactive.JettyResourceFactory; import org.springframework.http.client.reactive.ReactorResourceFactory; /** @@ -55,24 +54,6 @@ ReactorClientHttpConnectorFactory reactorClientHttpConnectorFactory( } - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(org.eclipse.jetty.reactive.client.ReactiveRequest.class) - @ConditionalOnMissingBean(ClientHttpConnectorFactory.class) - static class JettyClient { - - @Bean - @ConditionalOnMissingBean - JettyResourceFactory jettyClientResourceFactory() { - return new JettyResourceFactory(); - } - - @Bean - JettyClientHttpConnectorFactory jettyClientHttpConnectorFactory(JettyResourceFactory jettyResourceFactory) { - return new JettyClientHttpConnectorFactory(jettyResourceFactory); - } - - } - @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ HttpAsyncClients.class, AsyncRequestProducer.class, ReactiveResponseConsumer.class }) @ConditionalOnMissingBean(ClientHttpConnectorFactory.class) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/JettyClientHttpConnectorFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/JettyClientHttpConnectorFactory.java deleted file mode 100644 index 5824abf1ca92..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/JettyClientHttpConnectorFactory.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2012-2023 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.web.reactive.function.client; - -import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; -import org.eclipse.jetty.io.ClientConnector; -import org.eclipse.jetty.util.ssl.SslContextFactory; - -import org.springframework.boot.ssl.SslBundle; -import org.springframework.boot.ssl.SslOptions; -import org.springframework.http.client.reactive.JettyClientHttpConnector; -import org.springframework.http.client.reactive.JettyResourceFactory; - -/** - * {@link ClientHttpConnectorFactory} for {@link JettyClientHttpConnector}. - * - * @author Phillip Webb - */ -class JettyClientHttpConnectorFactory implements ClientHttpConnectorFactory { - - private final JettyResourceFactory jettyResourceFactory; - - JettyClientHttpConnectorFactory(JettyResourceFactory jettyResourceFactory) { - this.jettyResourceFactory = jettyResourceFactory; - } - - @Override - public JettyClientHttpConnector createClientHttpConnector(SslBundle sslBundle) { - SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); - if (sslBundle != null) { - SslOptions options = sslBundle.getOptions(); - if (options.getCiphers() != null) { - sslContextFactory.setIncludeCipherSuites(options.getCiphers()); - sslContextFactory.setExcludeCipherSuites(); - } - if (options.getEnabledProtocols() != null) { - sslContextFactory.setIncludeProtocols(options.getEnabledProtocols()); - sslContextFactory.setExcludeProtocols(); - } - sslContextFactory.setSslContext(sslBundle.createSslContext()); - } - ClientConnector connector = new ClientConnector(); - connector.setSslContextFactory(sslContextFactory); - HttpClientTransportOverHTTP transport = new HttpClientTransportOverHTTP(connector); - HttpClient httpClient = new HttpClient(transport); - return new JettyClientHttpConnector(httpClient, this.jettyResourceFactory); - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryConfiguration.java index b33e8eaf4ced..266b298eeb25 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryConfiguration.java @@ -20,9 +20,9 @@ import jakarta.servlet.Servlet; import org.apache.catalina.startup.Tomcat; import org.apache.coyote.UpgradeProtocol; +import org.eclipse.jetty.ee10.webapp.WebAppContext; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.Loader; -import org.eclipse.jetty.webapp.WebAppContext; import org.xnio.SslClientAuthMode; import org.springframework.beans.factory.ObjectProvider; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/reactive/JettyWebSocketReactiveWebServerCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/reactive/JettyWebSocketReactiveWebServerCustomizer.java index 4c050c4237ed..00ca570b9d6c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/reactive/JettyWebSocketReactiveWebServerCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/reactive/JettyWebSocketReactiveWebServerCustomizer.java @@ -17,15 +17,13 @@ package org.springframework.boot.autoconfigure.websocket.reactive; import jakarta.servlet.ServletContext; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.websocket.jakarta.server.JakartaWebSocketServerContainer; +import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServerContainer; +import org.eclipse.jetty.ee10.websocket.servlet.WebSocketUpgradeFilter; import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.handler.HandlerCollection; -import org.eclipse.jetty.server.handler.HandlerWrapper; -import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.websocket.core.server.WebSocketMappings; import org.eclipse.jetty.websocket.core.server.WebSocketServerComponents; -import org.eclipse.jetty.websocket.jakarta.server.internal.JakartaWebSocketServerContainer; -import org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer; -import org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter; import org.springframework.boot.web.embedded.jetty.JettyReactiveWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; @@ -47,13 +45,13 @@ public void customize(JettyReactiveWebServerFactory factory) { if (servletContextHandler != null) { ServletContext servletContext = servletContextHandler.getServletContext(); if (JettyWebSocketServerContainer.getContainer(servletContext) == null) { - WebSocketServerComponents.ensureWebSocketComponents(server, servletContext); + WebSocketServerComponents.ensureWebSocketComponents(server, servletContextHandler); JettyWebSocketServerContainer.ensureContainer(servletContext); } if (JakartaWebSocketServerContainer.getContainer(servletContext) == null) { - WebSocketServerComponents.ensureWebSocketComponents(server, servletContext); + WebSocketServerComponents.ensureWebSocketComponents(server, servletContextHandler); WebSocketUpgradeFilter.ensureFilter(servletContext); - WebSocketMappings.ensureMappings(servletContext); + WebSocketMappings.ensureMappings(servletContextHandler); JakartaWebSocketServerContainer.ensureContainer(servletContext); } } @@ -64,10 +62,10 @@ private ServletContextHandler findServletContextHandler(Handler handler) { if (handler instanceof ServletContextHandler servletContextHandler) { return servletContextHandler; } - if (handler instanceof HandlerWrapper handlerWrapper) { + if (handler instanceof Handler.Wrapper handlerWrapper) { return findServletContextHandler(handlerWrapper.getHandler()); } - if (handler instanceof HandlerCollection handlerCollection) { + if (handler instanceof Handler.Collection handlerCollection) { for (Handler contained : handlerCollection.getHandlers()) { ServletContextHandler servletContextHandler = findServletContextHandler(contained); if (servletContextHandler != null) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/reactive/WebSocketReactiveAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/reactive/WebSocketReactiveAutoConfiguration.java index 2f26b1698582..3592ba14b3cd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/reactive/WebSocketReactiveAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/reactive/WebSocketReactiveAutoConfiguration.java @@ -20,7 +20,7 @@ import jakarta.websocket.server.ServerContainer; import org.apache.catalina.startup.Tomcat; import org.apache.tomcat.websocket.server.WsSci; -import org.eclipse.jetty.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer; +import org.eclipse.jetty.ee10.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/JettyWebSocketServletWebServerCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/JettyWebSocketServletWebServerCustomizer.java index 8ee41bc966ca..ccf0ef8f379e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/JettyWebSocketServletWebServerCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/JettyWebSocketServletWebServerCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -16,13 +16,13 @@ package org.springframework.boot.autoconfigure.websocket.servlet; -import org.eclipse.jetty.webapp.AbstractConfiguration; -import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.ee10.webapp.AbstractConfiguration; +import org.eclipse.jetty.ee10.webapp.WebAppContext; +import org.eclipse.jetty.ee10.websocket.jakarta.server.JakartaWebSocketServerContainer; +import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServerContainer; +import org.eclipse.jetty.ee10.websocket.servlet.WebSocketUpgradeFilter; import org.eclipse.jetty.websocket.core.server.WebSocketMappings; import org.eclipse.jetty.websocket.core.server.WebSocketServerComponents; -import org.eclipse.jetty.websocket.jakarta.server.internal.JakartaWebSocketServerContainer; -import org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer; -import org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter; import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; @@ -41,20 +41,20 @@ public class JettyWebSocketServletWebServerCustomizer @Override public void customize(JettyServletWebServerFactory factory) { - factory.addConfigurations(new AbstractConfiguration() { + factory.addConfigurations(new AbstractConfiguration(new AbstractConfiguration.Builder()) { @Override public void configure(WebAppContext context) throws Exception { if (JettyWebSocketServerContainer.getContainer(context.getServletContext()) == null) { WebSocketServerComponents.ensureWebSocketComponents(context.getServer(), - context.getServletContext()); + context.getContext().getContextHandler()); JettyWebSocketServerContainer.ensureContainer(context.getServletContext()); } if (JakartaWebSocketServerContainer.getContainer(context.getServletContext()) == null) { WebSocketServerComponents.ensureWebSocketComponents(context.getServer(), - context.getServletContext()); + context.getContext().getContextHandler()); WebSocketUpgradeFilter.ensureFilter(context.getServletContext()); - WebSocketMappings.ensureMappings(context.getServletContext()); + WebSocketMappings.ensureMappings(context.getContext().getContextHandler()); JakartaWebSocketServerContainer.ensureContainer(context.getServletContext()); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketServletAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketServletAutoConfiguration.java index 48d3379ef3f0..a84c02eb1afa 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketServletAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketServletAutoConfiguration.java @@ -23,8 +23,8 @@ import jakarta.websocket.server.ServerContainer; import org.apache.catalina.startup.Tomcat; import org.apache.tomcat.websocket.server.WsSci; -import org.eclipse.jetty.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer; -import org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter; +import org.eclipse.jetty.ee10.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer; +import org.eclipse.jetty.ee10.websocket.servlet.WebSocketUpgradeFilter; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfigurationTests.java index e70fb24e4775..a2e0647780cc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfigurationTests.java @@ -43,6 +43,7 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -62,6 +63,7 @@ * @author Kazuki Shimizu * @author Nishant Raut */ +@ClassPathExclusions("jetty-jndi-*.jar") class JtaAutoConfigurationTests { private AnnotationConfigApplicationContext context; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java index 07b26273054f..ff8b377d2d95 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java @@ -16,21 +16,14 @@ package org.springframework.boot.autoconfigure.web; -import java.io.IOException; import java.net.InetAddress; -import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; import io.undertow.UndertowOptions; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import org.apache.catalina.connector.Connector; import org.apache.catalina.core.StandardContext; import org.apache.catalina.core.StandardEngine; @@ -38,8 +31,7 @@ import org.apache.catalina.valves.RemoteIpValve; import org.apache.coyote.AbstractProtocol; import org.apache.tomcat.util.net.AbstractEndpoint; -import org.eclipse.jetty.server.HttpChannel; -import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.jupiter.api.Test; @@ -51,21 +43,11 @@ import org.springframework.boot.context.properties.source.ConfigurationPropertySource; import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories; -import org.springframework.boot.testsupport.web.servlet.Servlet5ClassPathOverrides; import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; import org.springframework.boot.web.embedded.jetty.JettyWebServer; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; -import org.springframework.boot.web.servlet.ServletContextInitializer; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.client.ClientHttpResponse; import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; import org.springframework.util.unit.DataSize; -import org.springframework.web.client.ResponseErrorHandler; -import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; @@ -444,7 +426,6 @@ void tomcatMaxKeepAliveRequestsDefault() throws Exception { } @Test - @Servlet5ClassPathOverrides void jettyThreadPoolPropertyDefaultsShouldMatchServerDefault() { JettyServletWebServerFactory jettyFactory = new JettyServletWebServerFactory(0); JettyWebServer jetty = (JettyWebServer) jettyFactory.getWebServer(); @@ -459,61 +440,12 @@ void jettyThreadPoolPropertyDefaultsShouldMatchServerDefault() { } @Test - @Servlet5ClassPathOverrides void jettyMaxHttpFormPostSizeMatchesDefault() { JettyServletWebServerFactory jettyFactory = new JettyServletWebServerFactory(0); - JettyWebServer jetty = (JettyWebServer) jettyFactory - .getWebServer((ServletContextInitializer) (servletContext) -> servletContext - .addServlet("formPost", new HttpServlet() { - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) - throws ServletException, IOException { - req.getParameterMap(); - } - - }) - .addMapping("/form")); - jetty.start(); - org.eclipse.jetty.server.Connector connector = jetty.getServer().getConnectors()[0]; - final AtomicReference failure = new AtomicReference<>(); - connector.addBean(new HttpChannel.Listener() { - - @Override - public void onDispatchFailure(Request request, Throwable ex) { - failure.set(ex); - } - - }); - try { - RestTemplate template = new RestTemplate(); - template.setErrorHandler(new ResponseErrorHandler() { - - @Override - public boolean hasError(ClientHttpResponse response) throws IOException { - return false; - } - - @Override - public void handleError(ClientHttpResponse response) throws IOException { - - } - - }); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - MultiValueMap body = new LinkedMultiValueMap<>(); - body.add("data", "a".repeat(250000)); - HttpEntity> entity = new HttpEntity<>(body, headers); - template.postForEntity(URI.create("http://localhost:" + jetty.getPort() + "/form"), entity, Void.class); - assertThat(failure.get()).isNotNull(); - String message = failure.get().getCause().getMessage(); - int defaultMaxPostSize = Integer.parseInt(message.substring(message.lastIndexOf(' ')).trim()); - assertThat(this.properties.getJetty().getMaxHttpFormPostSize().toBytes()).isEqualTo(defaultMaxPostSize); - } - finally { - jetty.stop(); - } + JettyWebServer jetty = (JettyWebServer) jettyFactory.getWebServer(); + Server server = jetty.getServer(); + assertThat(this.properties.getJetty().getMaxHttpFormPostSize().toBytes()) + .isEqualTo(((ServletContextHandler) server.getHandler()).getMaxFormContentSize()); } @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java index a4367e150642..eef94bb88a20 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/JettyWebServerFactoryCustomizerTests.java @@ -47,7 +47,6 @@ import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories; -import org.springframework.boot.testsupport.web.servlet.Servlet5ClassPathOverrides; import org.springframework.boot.web.embedded.jetty.ConfigurableJettyWebServerFactory; import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; import org.springframework.boot.web.embedded.jetty.JettyWebServer; @@ -67,7 +66,6 @@ * @author HaiTao Zhang */ @DirtiesUrlFactories -@Servlet5ClassPathOverrides class JettyWebServerFactoryCustomizerTests { private MockEnvironment environment; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfigurationTests.java index 7b1b38cf2678..044c1bfff9f1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryAutoConfigurationTests.java @@ -31,7 +31,6 @@ import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories; -import org.springframework.boot.testsupport.web.servlet.Servlet5ClassPathOverrides; import org.springframework.boot.web.embedded.jetty.JettyReactiveWebServerFactory; import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer; import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; @@ -231,7 +230,6 @@ void tomcatProtocolHandlerCustomizerRegisteredAsBeanAndViaFactoryIsOnlyCalledOnc } @Test - @Servlet5ClassPathOverrides void jettyServerCustomizerBeanIsAddedToFactory() { new ReactiveWebApplicationContextRunner(AnnotationConfigReactiveWebApplicationContext::new) .withConfiguration(AutoConfigurations.of(ReactiveWebServerFactoryAutoConfiguration.class)) @@ -244,7 +242,6 @@ void jettyServerCustomizerBeanIsAddedToFactory() { } @Test - @Servlet5ClassPathOverrides void jettyServerCustomizerRegisteredAsBeanAndViaFactoryIsOnlyCalledOnce() { new ReactiveWebApplicationContextRunner(AnnotationConfigReactiveWebServerApplicationContext::new) .withConfiguration(AutoConfigurations.of(ReactiveWebServerFactoryAutoConfiguration.class)) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorAutoConfigurationTests.java index 2a05496baf18..b3fe66cea5ce 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorAutoConfigurationTests.java @@ -17,7 +17,6 @@ package org.springframework.boot.autoconfigure.web.reactive.function.client; import org.apache.hc.client5.http.impl.async.HttpAsyncClients; -import org.eclipse.jetty.reactive.client.ReactiveRequest; import org.junit.jupiter.api.Test; import reactor.netty.http.client.HttpClient; @@ -62,36 +61,20 @@ void whenReactorIsAvailableThenReactorBeansAreDefined() { } @Test - void whenReactorIsUnavailableThenJettyBeansAreDefined() { + void whenReactorIsUnavailableThenHttpClientBeansAreDefined() { this.contextRunner.withClassLoader(new FilteredClassLoader(HttpClient.class)).run((context) -> { BeanDefinition customizerDefinition = context.getBeanFactory() .getBeanDefinition("webClientHttpConnectorCustomizer"); assertThat(customizerDefinition.isLazyInit()).isTrue(); BeanDefinition connectorDefinition = context.getBeanFactory().getBeanDefinition("webClientHttpConnector"); assertThat(connectorDefinition.isLazyInit()).isTrue(); - assertThat(context).hasBean("jettyClientResourceFactory"); - assertThat(context).hasBean("jettyClientHttpConnectorFactory"); + assertThat(context).hasBean("httpComponentsClientHttpConnectorFactory"); }); } @Test - void whenReactorAndJettyAreUnavailableThenHttpClientBeansAreDefined() { - this.contextRunner.withClassLoader(new FilteredClassLoader(HttpClient.class, ReactiveRequest.class)) - .run((context) -> { - BeanDefinition customizerDefinition = context.getBeanFactory() - .getBeanDefinition("webClientHttpConnectorCustomizer"); - assertThat(customizerDefinition.isLazyInit()).isTrue(); - BeanDefinition connectorDefinition = context.getBeanFactory() - .getBeanDefinition("webClientHttpConnector"); - assertThat(connectorDefinition.isLazyInit()).isTrue(); - assertThat(context).hasBean("httpComponentsClientHttpConnectorFactory"); - }); - } - - @Test - void whenReactorJettyAndHttpClientBeansAreUnavailableThenJdkClientBeansAreDefined() { - this.contextRunner - .withClassLoader(new FilteredClassLoader(HttpClient.class, ReactiveRequest.class, HttpAsyncClients.class)) + void whenReactorAndHttpClientBeansAreUnavailableThenJdkClientBeansAreDefined() { + this.contextRunner.withClassLoader(new FilteredClassLoader(HttpClient.class, HttpAsyncClients.class)) .run((context) -> { BeanDefinition customizerDefinition = context.getBeanFactory() .getBeanDefinition("webClientHttpConnectorCustomizer"); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorFactoryConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorFactoryConfigurationTests.java index 79991a079b64..5d7fd0fab66e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorFactoryConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorFactoryConfigurationTests.java @@ -16,11 +16,6 @@ package org.springframework.boot.autoconfigure.web.reactive.function.client; -import java.util.concurrent.Executor; - -import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.io.ByteBufferPool; -import org.eclipse.jetty.util.thread.Scheduler; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -32,13 +27,9 @@ import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector; -import org.springframework.http.client.reactive.JettyClientHttpConnector; -import org.springframework.http.client.reactive.JettyResourceFactory; -import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; /** @@ -50,40 +41,6 @@ */ class ClientHttpConnectorFactoryConfigurationTests { - @Test - void jettyClientHttpConnectorAppliesJettyResourceFactory() { - Executor executor = mock(Executor.class); - ByteBufferPool byteBufferPool = mock(ByteBufferPool.class); - Scheduler scheduler = mock(Scheduler.class); - JettyResourceFactory jettyResourceFactory = new JettyResourceFactory(); - jettyResourceFactory.setExecutor(executor); - jettyResourceFactory.setByteBufferPool(byteBufferPool); - jettyResourceFactory.setScheduler(scheduler); - JettyClientHttpConnectorFactory connectorFactory = getJettyClientHttpConnectorFactory(jettyResourceFactory); - JettyClientHttpConnector connector = connectorFactory.createClientHttpConnector(); - HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(connector, "httpClient"); - assertThat(httpClient.getExecutor()).isSameAs(executor); - assertThat(httpClient.getByteBufferPool()).isSameAs(byteBufferPool); - assertThat(httpClient.getScheduler()).isSameAs(scheduler); - } - - @Test - void JettyResourceFactoryHasSslContextFactory() { - // gh-16810 - JettyResourceFactory jettyResourceFactory = new JettyResourceFactory(); - JettyClientHttpConnectorFactory connectorFactory = getJettyClientHttpConnectorFactory(jettyResourceFactory); - JettyClientHttpConnector connector = connectorFactory.createClientHttpConnector(); - HttpClient httpClient = (HttpClient) ReflectionTestUtils.getField(connector, "httpClient"); - assertThat(httpClient.getSslContextFactory()).isNotNull(); - } - - private JettyClientHttpConnectorFactory getJettyClientHttpConnectorFactory( - JettyResourceFactory jettyResourceFactory) { - ClientHttpConnectorFactoryConfiguration.JettyClient jettyClient = new ClientHttpConnectorFactoryConfiguration.JettyClient(); - // We shouldn't usually call this method directly since it's on a non-proxy config - return ReflectionTestUtils.invokeMethod(jettyClient, "jettyClientHttpConnectorFactory", jettyResourceFactory); - } - @Test void shouldApplyHttpClientMapper() { JksSslStoreDetails storeDetails = JksSslStoreDetails.forLocation("classpath:test.jks"); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/JettyClientHttpConnectorFactoryTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/JettyClientHttpConnectorFactoryTests.java deleted file mode 100644 index ad99f85a778a..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/JettyClientHttpConnectorFactoryTests.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2012-2023 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.web.reactive.function.client; - -import org.springframework.http.client.reactive.JettyResourceFactory; - -/** - * Tests for {@link JettyClientHttpConnectorFactory}. - * - * @author Phillip Webb - */ -class JettyClientHttpConnectorFactoryTests extends AbstractClientHttpConnectorFactoryTests { - - @Override - protected ClientHttpConnectorFactory getFactory() { - JettyResourceFactory resourceFactory = new JettyResourceFactory(); - return new JettyClientHttpConnectorFactory(resourceFactory); - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/MultipartAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/MultipartAutoConfigurationTests.java index b1e1dd0926a0..f60ffd3a5ef7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/MultipartAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/MultipartAutoConfigurationTests.java @@ -31,7 +31,6 @@ import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.boot.testsupport.classpath.ForkedClassPath; import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories; -import org.springframework.boot.testsupport.web.servlet.Servlet5ClassPathOverrides; import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory; @@ -221,7 +220,6 @@ static class WebServerWithNothing { } - @Servlet5ClassPathOverrides @Configuration(proxyBeanMethods = false) static class WebServerWithNoMultipartJetty { @@ -282,7 +280,6 @@ WebController controller() { } - @Servlet5ClassPathOverrides @Configuration(proxyBeanMethods = false) static class WebServerWithEverythingJetty { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfigurationTests.java index 6555da75f8ff..a3211bd3417b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryAutoConfigurationTests.java @@ -38,7 +38,6 @@ import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories; -import org.springframework.boot.testsupport.web.servlet.Servlet5ClassPathOverrides; import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer; import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer; @@ -156,7 +155,6 @@ void initParametersAreConfiguredOnTheServletContext() { } @Test - @Servlet5ClassPathOverrides void jettyServerCustomizerBeanIsAddedToFactory() { WebApplicationContextRunner runner = new WebApplicationContextRunner( AnnotationConfigServletWebServerApplicationContext::new) @@ -171,7 +169,6 @@ void jettyServerCustomizerBeanIsAddedToFactory() { } @Test - @Servlet5ClassPathOverrides void jettyServerCustomizerRegisteredAsBeanAndViaFactoryIsOnlyCalledOnce() { WebApplicationContextRunner runner = new WebApplicationContextRunner( AnnotationConfigServletWebServerApplicationContext::new) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerServletContextListenerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerServletContextListenerTests.java index d2ef77630aa8..ec52ceab9cbb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerServletContextListenerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerServletContextListenerTests.java @@ -26,7 +26,6 @@ import org.springframework.boot.testsupport.classpath.ForkedClassPath; import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories; -import org.springframework.boot.testsupport.web.servlet.Servlet5ClassPathOverrides; import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory; @@ -90,7 +89,6 @@ ServletWebServerFactory webServerFactory() { } - @Servlet5ClassPathOverrides @Configuration(proxyBeanMethods = false) static class JettyConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/reactive/WebSocketReactiveAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/reactive/WebSocketReactiveAutoConfigurationTests.java index e9505d09632c..e092a9262f2e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/reactive/WebSocketReactiveAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/reactive/WebSocketReactiveAutoConfigurationTests.java @@ -24,14 +24,13 @@ import org.apache.catalina.Container; import org.apache.catalina.Context; import org.apache.catalina.startup.Tomcat; -import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.springframework.boot.testsupport.classpath.ForkedClassPath; import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories; -import org.springframework.boot.testsupport.web.servlet.Servlet5ClassPathOverrides; import org.springframework.boot.web.embedded.jetty.JettyReactiveWebServerFactory; import org.springframework.boot.web.embedded.jetty.JettyWebServer; import org.springframework.boot.web.embedded.tomcat.TomcatReactiveWebServerFactory; @@ -123,7 +122,6 @@ ReactiveWebServerFactory webServerFactory() { } - @Servlet5ClassPathOverrides @Configuration(proxyBeanMethods = false) static class JettyConfiguration extends CommonConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketServletAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketServletAutoConfigurationTests.java index 423c89df1d51..5bef88c48dd4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketServletAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketServletAutoConfigurationTests.java @@ -30,7 +30,7 @@ import jakarta.websocket.DeploymentException; import jakarta.websocket.server.ServerContainer; import jakarta.websocket.server.ServerEndpoint; -import org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter; +import org.eclipse.jetty.ee10.websocket.servlet.WebSocketUpgradeFilter; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -42,7 +42,6 @@ import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.testsupport.classpath.ForkedClassPath; import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories; -import org.springframework.boot.testsupport.web.servlet.Servlet5ClassPathOverrides; import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.server.WebServer; @@ -182,7 +181,6 @@ ServletWebServerFactory webServerFactory() { } - @Servlet5ClassPathOverrides @Configuration(proxyBeanMethods = false) static class JettyConfiguration extends CommonConfiguration { diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index eef9a4f5c9fa..3e1c8aa77487 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -675,7 +675,12 @@ bom { ] } } - library("Jetty", "11.0.15") { + library("Jetty", "12.0.1") { + group("org.eclipse.jetty.ee10") { + imports = [ + "jetty-ee10-bom" + ] + } group("org.eclipse.jetty") { imports = [ "jetty-bom" diff --git a/spring-boot-project/spring-boot-devtools/build.gradle b/spring-boot-project/spring-boot-devtools/build.gradle index 7e33304df021..b3047fd5d52d 100644 --- a/spring-boot-project/spring-boot-devtools/build.gradle +++ b/spring-boot-project/spring-boot-devtools/build.gradle @@ -65,9 +65,7 @@ dependencies { testImplementation("org.apache.tomcat.embed:tomcat-embed-jasper") testImplementation("org.assertj:assertj-core") testImplementation("org.awaitility:awaitility") - testImplementation("org.eclipse.jetty.websocket:websocket-jakarta-client") { - exclude group: "org.eclipse.jetty.toolchain", module: "jetty-jakarta-websocket-api" - } + testImplementation("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-client") testImplementation("org.hamcrest:hamcrest-library") testImplementation("org.hsqldb:hsqldb") testImplementation("org.junit.jupiter:junit-jupiter") diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/system-requirements.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/system-requirements.adoc index a180e0efb574..aa634dfb7d82 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/system-requirements.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/system-requirements.adoc @@ -27,8 +27,8 @@ Spring Boot supports the following embedded servlet containers: | Tomcat 10.1 | 6.0 -| Jetty 11.0 -| 5.0 +| Jetty 12.0 +| 6.0 | Undertow 2.3 | 6.0 diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-jetty/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-jetty/build.gradle index eb83be39310c..3050b1cd5c98 100644 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-jetty/build.gradle +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-jetty/build.gradle @@ -9,16 +9,8 @@ dependencies { api("jakarta.websocket:jakarta.websocket-api") api("jakarta.websocket:jakarta.websocket-client-api") api("org.apache.tomcat.embed:tomcat-embed-el") - api("org.eclipse.jetty:jetty-servlets") - api("org.eclipse.jetty:jetty-webapp") { - exclude(group: "org.eclipse.jetty.toolchain", module: "jetty-jakarta-servlet-api") - } - api("org.eclipse.jetty.websocket:websocket-jakarta-server") { - exclude(group: "org.eclipse.jetty.toolchain", module: "jetty-jakarta-servlet-api") - exclude(group: "org.eclipse.jetty.toolchain", module: "jetty-jakarta-websocket-api") - } - api("org.eclipse.jetty.websocket:websocket-jetty-server") { - exclude group: "org.eclipse.jetty", module: "jetty-jndi" - exclude(group: "org.eclipse.jetty.toolchain", module: "jetty-jakarta-servlet-api") - } + api("org.eclipse.jetty.ee10:jetty-ee10-servlets") + api("org.eclipse.jetty.ee10:jetty-ee10-webapp") + api("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-server") + api("org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jetty-server") } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/web/servlet/Servlet5ClassPathOverrides.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/web/servlet/Servlet5ClassPathOverrides.java deleted file mode 100644 index 2fc8544d79ad..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/web/servlet/Servlet5ClassPathOverrides.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2012-2022 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.testsupport.web.servlet; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import org.springframework.boot.testsupport.classpath.ClassPathExclusions; -import org.springframework.boot.testsupport.classpath.ClassPathOverrides; - -/** - * Annotation to downgrade to Servlet 5.0. - * - * @author Phillip Webb - * @since 3.0.0 - */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.TYPE, ElementType.METHOD }) -@Documented -@ClassPathExclusions("jakarta.servlet-api-6*.jar") -@ClassPathOverrides("jakarta.servlet:jakarta.servlet-api:5.0.0") -public @interface Servlet5ClassPathOverrides { - -} diff --git a/spring-boot-project/spring-boot/build.gradle b/spring-boot-project/spring-boot/build.gradle index 6b86fd02adcf..ebc27fffe715 100644 --- a/spring-boot-project/spring-boot/build.gradle +++ b/spring-boot-project/spring-boot/build.gradle @@ -56,18 +56,12 @@ dependencies { optional("org.assertj:assertj-core") optional("org.apache.groovy:groovy") optional("org.apache.groovy:groovy-xml") - optional("org.eclipse.jetty:jetty-servlets") - optional("org.eclipse.jetty:jetty-util") - optional("org.eclipse.jetty:jetty-webapp") { - exclude(group: "org.eclipse.jetty.toolchain", module: "jetty-jakarta-servlet-api") - } - optional("org.eclipse.jetty:jetty-alpn-conscrypt-server") { - exclude(group: "org.eclipse.jetty.toolchain", module: "jetty-jakarta-servlet-api") - } - optional("org.eclipse.jetty.http2:http2-server") { - exclude(group: "org.eclipse.jetty.toolchain", module: "jetty-jakarta-servlet-api") - } + optional("org.eclipse.jetty:jetty-alpn-conscrypt-server") optional("org.eclipse.jetty:jetty-client") + optional("org.eclipse.jetty:jetty-util") + optional("org.eclipse.jetty.ee10:jetty-ee10-servlets") + optional("org.eclipse.jetty.ee10:jetty-ee10-webapp") + optional("org.eclipse.jetty.http2:jetty-http2-server") optional("org.flywaydb:flyway-core") optional("org.hamcrest:hamcrest-library") optional("org.hibernate.orm:hibernate-core") @@ -120,8 +114,8 @@ dependencies { testImplementation("org.awaitility:awaitility") testImplementation("org.codehaus.janino:janino") testImplementation("org.eclipse.jetty:jetty-client") - testImplementation("org.eclipse.jetty.http2:http2-client") - testImplementation("org.eclipse.jetty.http2:http2-http-client-transport") + testImplementation("org.eclipse.jetty.http2:jetty-http2-client") + testImplementation("org.eclipse.jetty.http2:jetty-http2-client-transport") testImplementation("org.firebirdsql.jdbc:jaybird") { exclude group: "javax.resource", module: "connector-api" } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java index 0ecdc850a0b5..9f4aa4475487 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java @@ -39,7 +39,7 @@ import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; import org.apache.hc.core5.http.io.SocketConfig; -import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic; +import org.eclipse.jetty.client.transport.HttpClientTransportDynamic; import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.util.ssl.SslContextFactory; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/GracefulShutdown.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/GracefulShutdown.java index dc2580d25ea8..8ccd5d26e56c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/GracefulShutdown.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/GracefulShutdown.java @@ -99,6 +99,7 @@ private void awaitShutdown(GracefulShutdownCallback callback) { while (this.shuttingDown && this.activeRequests.get() > 0) { sleep(100); } + System.out.println(this.activeRequests.get()); this.shuttingDown = false; long activeRequests = this.activeRequests.get(); if (activeRequests == 0) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JasperInitializer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JasperInitializer.java index 74b6b5a7561a..d370f56d9fdb 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JasperInitializer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JasperInitializer.java @@ -24,8 +24,8 @@ import java.net.URLStreamHandlerFactory; import jakarta.servlet.ServletContainerInitializer; +import org.eclipse.jetty.ee10.webapp.WebAppContext; import org.eclipse.jetty.util.component.AbstractLifeCycle; -import org.eclipse.jetty.webapp.WebAppContext; import org.springframework.util.ClassUtils; @@ -63,7 +63,6 @@ private ServletContainerInitializer newInitializer() { } @Override - @SuppressWarnings("deprecation") protected void doStart() throws Exception { if (this.initializer == null) { return; @@ -84,11 +83,11 @@ protected void doStart() throws Exception { try { Thread.currentThread().setContextClassLoader(this.context.getClassLoader()); try { - setExtendedListenerTypes(true); + this.context.getContext().setExtendedListenerTypes(true); this.initializer.onStartup(null, this.context.getServletContext()); } finally { - setExtendedListenerTypes(false); + this.context.getContext().setExtendedListenerTypes(false); } } finally { @@ -96,15 +95,6 @@ protected void doStart() throws Exception { } } - private void setExtendedListenerTypes(boolean extended) { - try { - this.context.getServletContext().setExtendedListenerTypes(extended); - } - catch (NoSuchMethodError ex) { - // Not available on Jetty 8 - } - } - /** * {@link URLStreamHandlerFactory} to support {@literal war} protocol. */ diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyEmbeddedErrorHandler.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyEmbeddedErrorHandler.java index 5871fb668acf..924a53242139 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyEmbeddedErrorHandler.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyEmbeddedErrorHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -16,17 +16,8 @@ package org.springframework.boot.web.embedded.jetty; -import java.io.IOException; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.ee10.servlet.ErrorPageErrorHandler; import org.eclipse.jetty.http.HttpMethod; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.servlet.ErrorPageErrorHandler; /** * Variation of Jetty's {@link ErrorPageErrorHandler} that supports all {@link HttpMethod @@ -40,20 +31,9 @@ */ class JettyEmbeddedErrorHandler extends ErrorPageErrorHandler { - private static final Set HANDLED_HTTP_METHODS = new HashSet<>(Arrays.asList("GET", "POST", "HEAD")); - @Override public boolean errorPageForMethod(String method) { return true; } - @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) - throws IOException, ServletException { - if (!HANDLED_HTTP_METHODS.contains(baseRequest.getMethod())) { - baseRequest.setMethod("GET"); - } - super.handle(target, baseRequest, request, response); - } - } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyEmbeddedWebAppContext.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyEmbeddedWebAppContext.java index 000c0acd79cb..d1caaf1d5a32 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyEmbeddedWebAppContext.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyEmbeddedWebAppContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. @@ -16,8 +16,9 @@ package org.springframework.boot.web.embedded.jetty; -import org.eclipse.jetty.servlet.ServletHandler; -import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.ee10.servlet.ServletHandler; +import org.eclipse.jetty.ee10.webapp.ClassMatcher; +import org.eclipse.jetty.ee10.webapp.WebAppContext; /** * Jetty {@link WebAppContext} used by {@link JettyWebServer} to support deferred @@ -27,6 +28,11 @@ */ class JettyEmbeddedWebAppContext extends WebAppContext { + JettyEmbeddedWebAppContext() { + setServerClassMatcher(new ClassMatcher("org.springframework.boot.loader.")); + // setTempDirectory(WebInfConfiguration.getCanonicalNameForWebAppTmpDir(this)); + } + @Override protected ServletHandler newServletHandler() { return new JettyEmbeddedServletHandler(); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyHandlerWrappers.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyHandlerWrappers.java index 35a5286b81cb..bb61f81c6b79 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyHandlerWrappers.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyHandlerWrappers.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -16,15 +16,13 @@ package org.springframework.boot.web.embedded.jetty; -import java.io.IOException; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.http.HttpFields.Mutable; import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.handler.HandlerWrapper; +import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.util.Callback; import org.springframework.boot.web.server.Compression; @@ -38,7 +36,7 @@ final class JettyHandlerWrappers { private JettyHandlerWrappers() { } - static HandlerWrapper createGzipHandlerWrapper(Compression compression) { + static Handler.Wrapper createGzipHandlerWrapper(Compression compression) { GzipHandler handler = new GzipHandler(); handler.setMinGzipSize((int) compression.getMinResponseSize().toBytes()); handler.setIncludedMimeTypes(compression.getMimeTypes()); @@ -48,14 +46,14 @@ static HandlerWrapper createGzipHandlerWrapper(Compression compression) { return handler; } - static HandlerWrapper createServerHeaderHandlerWrapper(String header) { + static Handler.Wrapper createServerHeaderHandlerWrapper(String header) { return new ServerHeaderHandler(header); } /** - * {@link HandlerWrapper} to add a custom {@code server} header. + * {@link Handler.Wrapper} to add a custom {@code server} header. */ - private static class ServerHeaderHandler extends HandlerWrapper { + private static class ServerHeaderHandler extends Handler.Wrapper { private static final String SERVER_HEADER = "server"; @@ -66,12 +64,12 @@ private static class ServerHeaderHandler extends HandlerWrapper { } @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) - throws IOException, ServletException { - if (!response.getHeaderNames().contains(SERVER_HEADER)) { - response.setHeader(SERVER_HEADER, this.value); + public boolean handle(Request request, Response response, Callback callback) throws Exception { + Mutable headers = response.getHeaders(); + if (!headers.contains(SERVER_HEADER)) { + headers.add(SERVER_HEADER, this.value); } - super.handle(target, baseRequest, request, response); + return super.handle(request, response, callback); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java index 3102dd4aa3d4..613514927381 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java @@ -26,6 +26,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletHolder; import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.ConnectionFactory; @@ -35,10 +37,7 @@ import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.handler.HandlerWrapper; import org.eclipse.jetty.server.handler.StatisticsHandler; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.util.thread.ThreadPool; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory; @@ -185,7 +184,7 @@ protected Server createJettyServer(JettyHttpHandlerAdapter servlet) { server.setStopTimeout(0); ServletHolder servletHolder = new ServletHolder(servlet); servletHolder.setAsyncSupported(true); - ServletContextHandler contextHandler = new ServletContextHandler(server, "/", false, false); + ServletContextHandler contextHandler = new ServletContextHandler("/", false, false); contextHandler.addServlet(servletHolder, "/"); server.setHandler(addHandlerWrappers(contextHandler)); JettyReactiveWebServerFactory.logger.info("Server initialized with port: " + port); @@ -243,7 +242,7 @@ private Handler addHandlerWrappers(Handler handler) { return handler; } - private Handler applyWrapper(Handler handler, HandlerWrapper wrapper) { + private Handler applyWrapper(Handler handler, Handler.Wrapper wrapper) { wrapper.setHandler(handler); return wrapper; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java index 4d93f13367f5..43be8f876671 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java @@ -20,26 +20,43 @@ import java.io.IOException; import java.io.InputStream; import java.net.InetSocketAddress; -import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.nio.channels.ReadableByteChannel; +import java.nio.file.Path; import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.EventListener; +import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; +import java.util.ListIterator; import java.util.Set; +import java.util.Spliterator; +import java.util.UUID; +import java.util.function.Consumer; -import jakarta.servlet.ServletException; import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.servlet.http.HttpServletResponseWrapper; +import org.eclipse.jetty.ee10.servlet.ErrorHandler; +import org.eclipse.jetty.ee10.servlet.ErrorPageErrorHandler; +import org.eclipse.jetty.ee10.servlet.ListenerHolder; +import org.eclipse.jetty.ee10.servlet.ServletHandler; +import org.eclipse.jetty.ee10.servlet.ServletHolder; +import org.eclipse.jetty.ee10.servlet.ServletMapping; +import org.eclipse.jetty.ee10.servlet.SessionHandler; +import org.eclipse.jetty.ee10.servlet.Source; +import org.eclipse.jetty.ee10.webapp.AbstractConfiguration; +import org.eclipse.jetty.ee10.webapp.Configuration; +import org.eclipse.jetty.ee10.webapp.WebAppContext; +import org.eclipse.jetty.ee10.webapp.WebInfConfiguration; import org.eclipse.jetty.http.HttpCookie; +import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpFields.Mutable; import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.http.MimeTypes.Wrapper; import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.ConnectionFactory; @@ -48,28 +65,21 @@ import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.HttpCookieUtils; +import org.eclipse.jetty.server.HttpStream; import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.handler.ErrorHandler; -import org.eclipse.jetty.server.handler.HandlerWrapper; import org.eclipse.jetty.server.handler.StatisticsHandler; -import org.eclipse.jetty.server.session.DefaultSessionCache; -import org.eclipse.jetty.server.session.FileSessionDataStore; -import org.eclipse.jetty.server.session.SessionHandler; -import org.eclipse.jetty.servlet.ErrorPageErrorHandler; -import org.eclipse.jetty.servlet.ListenerHolder; -import org.eclipse.jetty.servlet.ServletHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jetty.servlet.ServletMapping; -import org.eclipse.jetty.servlet.Source; -import org.eclipse.jetty.util.resource.JarResource; +import org.eclipse.jetty.session.DefaultSessionCache; +import org.eclipse.jetty.session.FileSessionDataStore; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.resource.CombinedResource; import org.eclipse.jetty.util.resource.Resource; -import org.eclipse.jetty.util.resource.ResourceCollection; +import org.eclipse.jetty.util.resource.ResourceFactory; +import org.eclipse.jetty.util.resource.URLResourceFactory; import org.eclipse.jetty.util.thread.ThreadPool; -import org.eclipse.jetty.webapp.AbstractConfiguration; -import org.eclipse.jetty.webapp.Configuration; -import org.eclipse.jetty.webapp.WebAppContext; import org.springframework.boot.web.server.Cookie.SameSite; import org.springframework.boot.web.server.ErrorPage; @@ -161,9 +171,11 @@ public JettyServletWebServerFactory(String contextPath, int port) { @Override public WebServer getWebServer(ServletContextInitializer... initializers) { JettyEmbeddedWebAppContext context = new JettyEmbeddedWebAppContext(); + context.getContext().getServletContext().setExtendedListenerTypes(true); int port = Math.max(getPort(), 0); InetSocketAddress address = new InetSocketAddress(getAddress(), port); Server server = createServer(address); + context.setServer(server); configureWebAppContext(context, initializers); server.setHandler(addHandlerWrappers(context)); this.logger.info("Server initialized with port: " + port); @@ -191,12 +203,17 @@ private Server createServer(InetSocketAddress address) { Server server = new Server(getThreadPool()); server.setConnectors(new Connector[] { createConnector(address, server) }); server.setStopTimeout(0); + MimeTypes.Mutable mimeTypes = server.getMimeTypes(); + for (MimeMappings.Mapping mapping : getMimeMappings()) { + mimeTypes.addMimeMapping(mapping.getExtension(), mapping.getMimeType()); + } return server; } private AbstractConnector createConnector(InetSocketAddress address, Server server) { HttpConfiguration httpConfiguration = new HttpConfiguration(); httpConfiguration.setSendServerVersion(false); + httpConfiguration.setIdleTimeout(30000); List connectionFactories = new ArrayList<>(); connectionFactories.add(new HttpConnectionFactory(httpConfiguration)); if (getHttp2() != null && getHttp2().isEnabled()) { @@ -222,7 +239,7 @@ private Handler addHandlerWrappers(Handler handler) { return handler; } - private Handler applyWrapper(Handler handler, HandlerWrapper wrapper) { + private Handler applyWrapper(Handler handler, Handler.Wrapper wrapper) { wrapper.setHandler(handler); return wrapper; } @@ -239,7 +256,6 @@ private void customizeSsl(Server server, InetSocketAddress address) { protected final void configureWebAppContext(WebAppContext context, ServletContextInitializer... initializers) { Assert.notNull(context, "Context must not be null"); context.clearAliasChecks(); - context.setTempDirectory(getTempDirectory()); if (this.resourceLoader != null) { context.setClassLoader(this.resourceLoader.getClassLoader()); } @@ -260,6 +276,7 @@ protected final void configureWebAppContext(WebAppContext context, ServletContex context.setConfigurations(configurations); context.setThrowUnavailableOnStartupException(true); configureSession(context); + context.setTempDirectory(getTempDirectory(context)); postProcessWebAppContext(context); } @@ -289,40 +306,49 @@ private void addLocaleMappings(WebAppContext context) { .forEach((locale, charset) -> context.addLocaleEncoding(locale.toString(), charset.toString())); } - private File getTempDirectory() { + private File getTempDirectory(WebAppContext context) { String temp = System.getProperty("java.io.tmpdir"); - return (temp != null) ? new File(temp) : null; + return (temp != null) + ? new File(temp, WebInfConfiguration.getCanonicalNameForWebAppTmpDir(context) + UUID.randomUUID()) + : null; } private void configureDocumentRoot(WebAppContext handler) { File root = getValidDocumentRoot(); File docBase = (root != null) ? root : createTempDir("jetty-docbase"); try { + ResourceFactory resourceFactory = handler.getResourceFactory(); List resources = new ArrayList<>(); - Resource rootResource = (docBase.isDirectory() ? Resource.newResource(docBase.getCanonicalFile()) - : JarResource.newJarResource(Resource.newResource(docBase))); - resources.add((root != null) ? new LoaderHidingResource(rootResource) : rootResource); + Resource rootResource = (docBase.isDirectory() + ? resourceFactory.newResource(docBase.getCanonicalFile().toURI()) + : resourceFactory.newJarFileResource(docBase.toURI())); + resources.add((root != null) ? new LoaderHidingResource(rootResource, rootResource) : rootResource); + URLResourceFactory urlResourceFactory = new URLResourceFactory(); for (URL resourceJarUrl : getUrlsOfJarsWithMetaInfResources()) { - Resource resource = createResource(resourceJarUrl); - if (resource.exists() && resource.isDirectory()) { + Resource resource = createResource(resourceJarUrl, resourceFactory, urlResourceFactory); + if (resource != null) { resources.add(resource); } } - handler.setBaseResource(new ResourceCollection(resources.toArray(new Resource[0]))); + handler.setBaseResource(ResourceFactory.combine(resources)); } catch (Exception ex) { throw new IllegalStateException(ex); } } - private Resource createResource(URL url) throws Exception { + private Resource createResource(URL url, ResourceFactory resourceFactory, URLResourceFactory urlResourceFactory) + throws Exception { if ("file".equals(url.getProtocol())) { File file = new File(url.toURI()); if (file.isFile()) { - return Resource.newResource("jar:" + url + "!/META-INF/resources"); + return resourceFactory.newResource("jar:" + url + "!/META-INF/resources/"); + } + if (file.isDirectory()) { + return resourceFactory.newResource(url).resolve("META-INF/resources/"); } } - return Resource.newResource(url + "META-INF/resources"); + return urlResourceFactory.newResource(url + "META-INF/resources/"); } /** @@ -333,7 +359,7 @@ protected final void addDefaultServlet(WebAppContext context) { Assert.notNull(context, "Context must not be null"); ServletHolder holder = new ServletHolder(); holder.setName("default"); - holder.setClassName("org.eclipse.jetty.servlet.DefaultServlet"); + holder.setClassName("org.eclipse.jetty.ee10.servlet.DefaultServlet"); holder.setInitParameter("dirAllowed", "false"); holder.setInitOrder(1); context.getServletHandler().addServletWithMapping(holder, "/"); @@ -382,7 +408,7 @@ protected Configuration[] getWebAppContextConfigurations(WebAppContext webAppCon * @return a configuration object for adding error pages */ private Configuration getErrorPageConfiguration() { - return new AbstractConfiguration() { + return new AbstractConfiguration(new AbstractConfiguration.Builder()) { @Override public void configure(WebAppContext context) throws Exception { @@ -399,11 +425,12 @@ public void configure(WebAppContext context) throws Exception { * @return a configuration object for adding mime type mappings */ private Configuration getMimeTypeConfiguration() { - return new AbstractConfiguration() { + return new AbstractConfiguration(new AbstractConfiguration.Builder()) { @Override public void configure(WebAppContext context) throws Exception { - MimeTypes mimeTypes = context.getMimeTypes(); + MimeTypes.Wrapper mimeTypes = (Wrapper) context.getMimeTypes(); + mimeTypes.setWrapped(new MimeTypes(null)); for (MimeMappings.Mapping mapping : getMimeMappings()) { mimeTypes.addMimeMapping(mapping.getExtension(), mapping.getMimeType()); } @@ -558,28 +585,48 @@ private void addJettyErrorPages(ErrorHandler errorHandler, Collection private static final class LoaderHidingResource extends Resource { + private static final String LOADER_RESOURCE_PATH_PREFIX = "/org/springframework/boot/"; + + private final Resource base; + private final Resource delegate; - private LoaderHidingResource(Resource delegate) { + private LoaderHidingResource(Resource base, Resource delegate) { + this.base = base; this.delegate = delegate; } @Override - public Resource addPath(String path) throws IOException { - if (path.startsWith("/org/springframework/boot")) { - return null; + public void forEach(Consumer action) { + this.delegate.forEach(action); + } + + @Override + public Path getPath() { + return this.delegate.getPath(); + } + + @Override + public boolean isContainedIn(Resource r) { + return this.delegate.isContainedIn(r); + } + + @Override + public Iterator iterator() { + if (this.delegate instanceof CombinedResource) { + return list().iterator(); } - return this.delegate.addPath(path); + return List.of(this).iterator(); } @Override - public boolean isContainedIn(Resource resource) throws MalformedURLException { - return this.delegate.isContainedIn(resource); + public boolean equals(Object obj) { + return this.delegate.equals(obj); } @Override - public void close() { - this.delegate.close(); + public int hashCode() { + return this.delegate.hashCode(); } @Override @@ -587,13 +634,23 @@ public boolean exists() { return this.delegate.exists(); } + @Override + public Spliterator spliterator() { + return this.delegate.spliterator(); + } + @Override public boolean isDirectory() { return this.delegate.isDirectory(); } @Override - public long lastModified() { + public boolean isReadable() { + return this.delegate.isReadable(); + } + + @Override + public Instant lastModified() { return this.delegate.lastModified(); } @@ -608,38 +665,67 @@ public URI getURI() { } @Override - public File getFile() throws IOException { - return this.delegate.getFile(); + public String getName() { + return this.delegate.getName(); } @Override - public String getName() { - return this.delegate.getName(); + public String getFileName() { + return this.delegate.getFileName(); + } + + @Override + public InputStream newInputStream() throws IOException { + return this.delegate.newInputStream(); + } + + @Override + public ReadableByteChannel newReadableByteChannel() throws IOException { + return this.delegate.newReadableByteChannel(); } @Override - public InputStream getInputStream() throws IOException { - return this.delegate.getInputStream(); + public List list() { + return this.delegate.list().stream().filter(this::nonLoaderResource).toList(); + } + + private boolean nonLoaderResource(Resource resource) { + Path prefix = this.base.getPath().resolve(Path.of("org", "springframework", "boot")); + return !resource.getPath().startsWith(prefix); } @Override - public ReadableByteChannel getReadableByteChannel() throws IOException { - return this.delegate.getReadableByteChannel(); + public Resource resolve(String subUriPath) { + if (subUriPath.startsWith(LOADER_RESOURCE_PATH_PREFIX)) { + return null; + } + Resource resolved = this.delegate.resolve(subUriPath); + return (resolved != null) ? new LoaderHidingResource(this.base, resolved) : null; } @Override - public boolean delete() throws SecurityException { - return this.delegate.delete(); + public boolean isAlias() { + return this.delegate.isAlias(); } @Override - public boolean renameTo(Resource dest) throws SecurityException { - return this.delegate.renameTo(dest); + public URI getRealURI() { + return this.delegate.getRealURI(); } @Override - public String[] list() { - return this.delegate.list(); + public void copyTo(Path destination) throws IOException { + this.delegate.copyTo(destination); + } + + @Override + public Collection getAllResources() { + return this.delegate.getAllResources().stream().filter(this::nonLoaderResource).toList(); + } + + @Override + public String toString() { + return this.delegate.toString(); } } @@ -652,6 +738,7 @@ private static class WebListenersConfiguration extends AbstractConfiguration { private final Set classNames; WebListenersConfiguration(Set webListenerClassNames) { + super(new AbstractConfiguration.Builder()); this.classNames = webListenerClassNames; } @@ -681,10 +768,10 @@ private Class loadClass(WebAppContext context, String c } /** - * {@link HandlerWrapper} to apply {@link CookieSameSiteSupplier supplied} + * {@link Handler.Wrapper} to apply {@link CookieSameSiteSupplier supplied} * {@link SameSite} cookie values. */ - private static class SuppliedSameSiteCookieHandlerWrapper extends HandlerWrapper { + private static class SuppliedSameSiteCookieHandlerWrapper extends Handler.Wrapper { private final List suppliers; @@ -693,41 +780,53 @@ private static class SuppliedSameSiteCookieHandlerWrapper extends HandlerWrapper } @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) - throws IOException, ServletException { - HttpServletResponse wrappedResponse = new ResponseWrapper(response); - super.handle(target, baseRequest, request, wrappedResponse); + public boolean handle(Request request, Response response, Callback callback) throws Exception { + request.addHttpStreamWrapper((stream) -> new SameSiteCookieHttpStreamWrapper(stream, request)); + return super.handle(request, response, callback); } - class ResponseWrapper extends HttpServletResponseWrapper { + private final class SameSiteCookieHttpStreamWrapper extends HttpStream.Wrapper { + + private final Request request; - ResponseWrapper(HttpServletResponse response) { - super(response); + private SameSiteCookieHttpStreamWrapper(HttpStream wrapped, Request request) { + super(wrapped); + this.request = request; } @Override - @SuppressWarnings("removal") - public void addCookie(Cookie cookie) { - SameSite sameSite = getSameSite(cookie); - if (sameSite != null) { - String comment = HttpCookie.getCommentWithoutAttributes(cookie.getComment()); - String sameSiteComment = getSameSiteComment(sameSite); - cookie.setComment((comment != null) ? comment + sameSiteComment : sameSiteComment); + public void prepareResponse(Mutable headers) { + super.prepareResponse(headers); + ListIterator headerFields = headers.listIterator(); + while (headerFields.hasNext()) { + HttpCookieUtils.SetCookieHttpField updatedField = applySameSiteIfNecessary(headerFields.next()); + if (updatedField != null) { + headerFields.set(updatedField); + } } - super.addCookie(cookie); } - private String getSameSiteComment(SameSite sameSite) { - return switch (sameSite) { - case NONE -> HttpCookie.SAME_SITE_NONE_COMMENT; - case LAX -> HttpCookie.SAME_SITE_LAX_COMMENT; - case STRICT -> HttpCookie.SAME_SITE_STRICT_COMMENT; - }; + private HttpCookieUtils.SetCookieHttpField applySameSiteIfNecessary(HttpField headerField) { + HttpCookie cookie = HttpCookieUtils.getSetCookie(headerField); + if (cookie == null) { + return null; + } + SameSite sameSite = getSameSite(cookie); + if (sameSite == null) { + return null; + } + return new HttpCookieUtils.SetCookieHttpField( + HttpCookie.build(cookie) + .sameSite(org.eclipse.jetty.http.HttpCookie.SameSite.from(sameSite.name())) + .build(), + this.request.getConnectionMetaData().getHttpConfiguration().getResponseCookieCompliance()); } - private SameSite getSameSite(Cookie cookie) { + private SameSite getSameSite(HttpCookie cookie) { + Cookie servletCookie = new Cookie(cookie.getName(), cookie.getValue()); + cookie.getAttributes().forEach(servletCookie::setAttribute); for (CookieSameSiteSupplier supplier : SuppliedSameSiteCookieHandlerWrapper.this.suppliers) { - SameSite sameSite = supplier.getSameSite(cookie); + SameSite sameSite = supplier.getSameSite(servletCookie); if (sameSite != null) { return sameSite; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java index bbfa74b2f6b9..bbcde54a773c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java @@ -17,7 +17,6 @@ package org.springframework.boot.web.embedded.jetty; import java.io.IOException; -import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -29,8 +28,6 @@ import org.eclipse.jetty.server.NetworkConnector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ContextHandler; -import org.eclipse.jetty.server.handler.HandlerCollection; -import org.eclipse.jetty.server.handler.HandlerWrapper; import org.eclipse.jetty.server.handler.StatisticsHandler; import org.springframework.boot.web.server.GracefulShutdownCallback; @@ -106,7 +103,7 @@ private StatisticsHandler findStatisticsHandler(Handler handler) { if (handler instanceof StatisticsHandler statisticsHandler) { return statisticsHandler; } - if (handler instanceof HandlerWrapper handlerWrapper) { + if (handler instanceof Handler.Wrapper handlerWrapper) { return findStatisticsHandler(handlerWrapper.getHandler()); } return null; @@ -208,7 +205,8 @@ private String getProtocols(Connector connector) { } private String getContextPath() { - return Arrays.stream(this.server.getHandlers()) + return this.server.getHandlers() + .stream() .map(this::findContextHandler) .filter(Objects::nonNull) .map(ContextHandler::getContextPath) @@ -216,7 +214,7 @@ private String getContextPath() { } private ContextHandler findContextHandler(Handler handler) { - while (handler instanceof HandlerWrapper handlerWrapper) { + while (handler instanceof Handler.Wrapper handlerWrapper) { if (handler instanceof ContextHandler contextHandler) { return contextHandler; } @@ -225,17 +223,21 @@ private ContextHandler findContextHandler(Handler handler) { return null; } - private void handleDeferredInitialize(Handler... handlers) throws Exception { + private void handleDeferredInitialize(List handlers) throws Exception { for (Handler handler : handlers) { - if (handler instanceof JettyEmbeddedWebAppContext jettyEmbeddedWebAppContext) { - jettyEmbeddedWebAppContext.deferredInitialize(); - } - else if (handler instanceof HandlerWrapper handlerWrapper) { - handleDeferredInitialize(handlerWrapper.getHandler()); - } - else if (handler instanceof HandlerCollection handlerCollection) { - handleDeferredInitialize(handlerCollection.getHandlers()); - } + handleDeferredInitialize(handler); + } + } + + private void handleDeferredInitialize(Handler handler) throws Exception { + if (handler instanceof JettyEmbeddedWebAppContext jettyEmbeddedWebAppContext) { + jettyEmbeddedWebAppContext.deferredInitialize(); + } + else if (handler instanceof Handler.Wrapper handlerWrapper) { + handleDeferredInitialize(handlerWrapper.getHandler()); + } + else if (handler instanceof Handler.Collection handlerCollection) { + handleDeferredInitialize(handlerCollection.getHandlers()); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/ServletContextInitializerConfiguration.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/ServletContextInitializerConfiguration.java index 2b3ecbafa56f..98a86fb5ba4b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/ServletContextInitializerConfiguration.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/ServletContextInitializerConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -17,9 +17,9 @@ package org.springframework.boot.web.embedded.jetty; import jakarta.servlet.ServletException; -import org.eclipse.jetty.webapp.AbstractConfiguration; -import org.eclipse.jetty.webapp.Configuration; -import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.ee10.webapp.AbstractConfiguration; +import org.eclipse.jetty.ee10.webapp.Configuration; +import org.eclipse.jetty.ee10.webapp.WebAppContext; import org.springframework.boot.web.servlet.ServletContextInitializer; import org.springframework.util.Assert; @@ -41,6 +41,7 @@ public class ServletContextInitializerConfiguration extends AbstractConfiguratio * @since 1.2.1 */ public ServletContextInitializerConfiguration(ServletContextInitializer... initializers) { + super(new AbstractConfiguration.Builder()); Assert.notNull(initializers, "Initializers must not be null"); this.initializers = initializers; } @@ -59,22 +60,13 @@ public void configure(WebAppContext context) throws Exception { private void callInitializers(WebAppContext context) throws ServletException { try { - setExtendedListenerTypes(context, true); + context.getContext().setExtendedListenerTypes(true); for (ServletContextInitializer initializer : this.initializers) { initializer.onStartup(context.getServletContext()); } } finally { - setExtendedListenerTypes(context, false); - } - } - - private void setExtendedListenerTypes(WebAppContext context, boolean extended) { - try { - context.getServletContext().setExtendedListenerTypes(extended); - } - catch (NoSuchMethodError ex) { - // Not available on Jetty 8 + context.getContext().setExtendedListenerTypes(false); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java index cffefa6f0985..329b6a73f2fb 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java @@ -111,19 +111,7 @@ private ServerConnector createHttp11ServerConnector(HttpConfiguration config, private SslConnectionFactory createSslConnectionFactory(SslContextFactory.Server sslContextFactory, String protocol) { - try { - return new SslConnectionFactory(sslContextFactory, protocol); - } - catch (NoSuchMethodError ex) { - // Jetty 10 - try { - return SslConnectionFactory.class.getConstructor(SslContextFactory.Server.class, String.class) - .newInstance(sslContextFactory, protocol); - } - catch (Exception ex2) { - throw new RuntimeException(ex2); - } - } + return new SslConnectionFactory(sslContextFactory, protocol); } private boolean isJettyAlpnPresent() { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java index f8baaa8db5b6..336ed38ab011 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactoryTests.java @@ -30,11 +30,9 @@ import org.junit.jupiter.api.Test; import org.mockito.InOrder; -import org.springframework.boot.testsupport.web.servlet.Servlet5ClassPathOverrides; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactoryTests; import org.springframework.boot.web.server.Shutdown; -import org.springframework.http.client.reactive.JettyResourceFactory; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.web.reactive.function.client.WebClient; @@ -51,7 +49,6 @@ * @author Madhura Bhave * @author Moritz Halbritter */ -@Servlet5ClassPathOverrides class JettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactoryTests { @Override @@ -61,7 +58,8 @@ protected JettyReactiveWebServerFactory getFactory() { @Test @Override - @Disabled("Jetty 11 does not support User-Agent-based compression") + @Disabled("Jetty 12 does not support User-Agent-based compression") + // TODO Is this true with Jetty 12? protected void noCompressionForUserAgent() { } @@ -114,20 +112,6 @@ void useForwardedHeaders() { assertForwardHeaderIsUsed(factory); } - @Test - void useServerResources() throws Exception { - JettyResourceFactory resourceFactory = new JettyResourceFactory(); - resourceFactory.afterPropertiesSet(); - JettyReactiveWebServerFactory factory = getFactory(); - factory.setResourceFactory(resourceFactory); - JettyWebServer webServer = (JettyWebServer) factory.getWebServer(new EchoHandler()); - webServer.start(); - Connector connector = webServer.getServer().getConnectors()[0]; - assertThat(connector.getByteBufferPool()).isEqualTo(resourceFactory.getByteBufferPool()); - assertThat(connector.getExecutor()).isEqualTo(resourceFactory.getExecutor()); - assertThat(connector.getScheduler()).isEqualTo(resourceFactory.getScheduler()); - } - @Test void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() { JettyReactiveWebServerFactory factory = getFactory(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java index 560269579c62..dbb73701e06c 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java @@ -40,29 +40,26 @@ import org.apache.hc.core5.http.HttpResponse; import org.apache.jasper.servlet.JspServlet; import org.awaitility.Awaitility; +import org.eclipse.jetty.ee10.servlet.ErrorPageErrorHandler; +import org.eclipse.jetty.ee10.servlet.ServletHolder; +import org.eclipse.jetty.ee10.webapp.AbstractConfiguration; +import org.eclipse.jetty.ee10.webapp.ClassMatcher; +import org.eclipse.jetty.ee10.webapp.Configuration; +import org.eclipse.jetty.ee10.webapp.WebAppContext; import org.eclipse.jetty.server.ConnectionLimit; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; -import org.eclipse.jetty.server.handler.HandlerCollection; -import org.eclipse.jetty.server.handler.HandlerWrapper; -import org.eclipse.jetty.servlet.ErrorPageErrorHandler; -import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.ThreadPool; -import org.eclipse.jetty.webapp.AbstractConfiguration; -import org.eclipse.jetty.webapp.ClassMatcher; -import org.eclipse.jetty.webapp.Configuration; -import org.eclipse.jetty.webapp.WebAppContext; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.InOrder; import org.springframework.boot.testsupport.system.CapturedOutput; -import org.springframework.boot.testsupport.web.servlet.Servlet5ClassPathOverrides; import org.springframework.boot.web.server.Compression; import org.springframework.boot.web.server.GracefulShutdownResult; import org.springframework.boot.web.server.PortInUseException; @@ -89,12 +86,20 @@ * @author Henri Kerola * @author Moritz Halbritter */ -@Servlet5ClassPathOverrides class JettyServletWebServerFactoryTests extends AbstractServletWebServerFactoryTests { @Override protected JettyServletWebServerFactory getFactory() { - return new JettyServletWebServerFactory(0); + JettyServletWebServerFactory factory = new JettyServletWebServerFactory(0); + factory.addServerCustomizers((server) -> { + for (Connector connector : server.getConnectors()) { + if (connector instanceof ServerConnector serverConnector) { + // TODO Set the shutdown idle timeout in main code? + serverConnector.setShutdownIdleTimeout(10000); + } + } + }); + return factory; } @Override @@ -144,10 +149,17 @@ protected void handleExceptionCausedByBlockedPortOnSecondaryConnector(RuntimeExc @Test @Override - @Disabled("Jetty 11 does not support User-Agent-based compression") + @Disabled("Jetty 12 does not support User-Agent-based compression") protected void noCompressionForUserAgent() { } + @Test + @Override + @Disabled("Jetty 12 does not support SSL session tracking") + protected void sslSessionTracking() { + + } + @Test void contextPathIsLoggedOnStartupWhenCompressionIsEnabled(CapturedOutput output) { AbstractServletWebServerFactory factory = getFactory(); @@ -385,11 +397,9 @@ void wrappedHandlers() throws Exception { JettyServletWebServerFactory factory = getFactory(); factory.setServerCustomizers(Collections.singletonList((server) -> { Handler handler = server.getHandler(); - HandlerWrapper wrapper = new HandlerWrapper(); + Handler.Wrapper wrapper = new Handler.Wrapper(); wrapper.setHandler(handler); - HandlerCollection collection = new HandlerCollection(); - collection.addHandler(wrapper); - server.setHandler(collection); + server.setHandler(wrapper); })); this.webServer = factory.getWebServer(exampleServletRegistration()); this.webServer.start(); @@ -507,7 +517,7 @@ public void contextDestroyed(ServletContextEvent event) { @Test void errorHandlerCanBeOverridden() { JettyServletWebServerFactory factory = getFactory(); - factory.addConfigurations(new AbstractConfiguration() { + factory.addConfigurations(new AbstractConfiguration(new AbstractConfiguration.Builder()) { @Override public void configure(WebAppContext context) throws Exception { @@ -544,7 +554,7 @@ private WebAppContext findWebAppContext(Handler handler) { if (handler instanceof WebAppContext webAppContext) { return webAppContext; } - if (handler instanceof HandlerWrapper wrapper) { + if (handler instanceof Handler.Wrapper wrapper) { return findWebAppContext(wrapper.getHandler()); } throw new IllegalStateException("No WebAppContext found"); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java index 509483854ee0..af2e45417fb5 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java @@ -41,10 +41,10 @@ import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.awaitility.Awaitility; -import org.eclipse.jetty.client.api.ContentResponse; -import org.eclipse.jetty.client.util.StringRequestContent; +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.client.StringRequestContent; import org.eclipse.jetty.http2.client.HTTP2Client; -import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2; +import org.eclipse.jetty.http2.client.transport.HttpClientTransportOverHTTP2; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/ServletComponentScanIntegrationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/ServletComponentScanIntegrationTests.java index bbc5e0a06fd2..2281650ef41f 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/ServletComponentScanIntegrationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/ServletComponentScanIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -38,7 +38,6 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.testsupport.classpath.ForkedClassPath; import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories; -import org.springframework.boot.testsupport.web.servlet.Servlet5ClassPathOverrides; import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer; import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; @@ -159,7 +158,6 @@ protected ServletWebServerFactory webServerFactory(ObjectProvider setCookieHeaders = clientResponse.getHeaders().get("Set-Cookie"); assertThat(setCookieHeaders).satisfiesExactlyInAnyOrder( (header) -> assertThat(header).contains("JSESSIONID").doesNotContain("SameSite"), @@ -957,7 +958,7 @@ void cookieSameSiteSuppliers() throws Exception { } @Test - void sslSessionTracking() { + protected void sslSessionTracking() { AbstractServletWebServerFactory factory = getFactory(); Ssl ssl = new Ssl(); ssl.setEnabled(true); @@ -1017,14 +1018,12 @@ void compressionWithoutContentSizeHeader() throws Exception { void mimeMappingsAreCorrectlyConfigured() { AbstractServletWebServerFactory factory = getFactory(); this.webServer = factory.getWebServer(); - Map configuredMimeMappings = getActualMimeMappings(); + Collection configuredMimeMappings = getActualMimeMappings().entrySet() + .stream() + .map((entry) -> new MimeMappings.Mapping(entry.getKey(), entry.getValue())) + .toList(); Collection expectedMimeMappings = MimeMappings.DEFAULT.getAll(); - configuredMimeMappings - .forEach((key, value) -> assertThat(expectedMimeMappings).contains(new MimeMappings.Mapping(key, value))); - for (MimeMappings.Mapping mapping : expectedMimeMappings) { - assertThat(configuredMimeMappings).containsEntry(mapping.getExtension(), mapping.getMimeType()); - } - assertThat(configuredMimeMappings).hasSameSizeAs(expectedMimeMappings); + assertThat(configuredMimeMappings).containsExactlyInAnyOrderElementsOf(expectedMimeMappings); } @Test diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/spring-boot-server-tests-app/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/spring-boot-server-tests-app/build.gradle index b72b482864ed..b8ff1ca48d1a 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/spring-boot-server-tests-app/build.gradle +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/spring-boot-server-tests-app/build.gradle @@ -41,14 +41,6 @@ configurations { } } -dependencyManagement { - jetty { - dependencies { - dependency "jakarta.servlet:jakarta.servlet-api:5.0.0" - } - } -} - tasks.register("resourcesJar", Jar) { jar -> def nested = project.resources.text.fromString("nested") from(nested) { @@ -66,7 +58,7 @@ tasks.register("resourcesJar", Jar) { jar -> } dependencies { - compileOnly("org.eclipse.jetty:jetty-server") + compileOnly("org.eclipse.jetty.ee10:jetty-ee10-servlet") compileOnly("org.springframework:spring-web") implementation("org.springframework.boot:spring-boot-starter") diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty-jsp/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty-jsp/build.gradle index 140f0a3c9a0c..d740445850ed 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty-jsp/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty-jsp/build.gradle @@ -11,10 +11,6 @@ configurations { } } -configurations.all { - resolutionStrategy.force("jakarta.servlet:jakarta.servlet-api:5.0.0") -} - dependencies { compileOnly(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-jetty")) @@ -22,10 +18,7 @@ dependencies { exclude module: "spring-boot-starter-tomcat" } - providedRuntime("org.eclipse.jetty:apache-jsp") { - exclude group: "org.eclipse.jetty.toolchain", module: "jetty-jakarta-servlet-api" - exclude group: "org.eclipse.jetty.toolchain", module: "jetty-schemas" - } + providedRuntime("org.eclipse.jetty.ee10:jetty-ee10-apache-jsp") runtimeOnly("org.glassfish.web:jakarta.servlet.jsp.jstl") diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty-jsp/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty-jsp/src/main/resources/application.properties index f18efd166420..b3b89e953ed8 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty-jsp/src/main/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty-jsp/src/main/resources/application.properties @@ -1,3 +1,4 @@ +application.message: Hello Spring Boot +server.servlet.jsp.class-name=org.eclipse.jetty.ee10.jsp.JettyJspServlet spring.mvc.view.prefix: /WEB-INF/jsp/ spring.mvc.view.suffix: .jsp -application.message: Hello Spring Boot diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty-ssl/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty-ssl/build.gradle index 8333e2118933..72c93ab6c314 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty-ssl/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty-ssl/build.gradle @@ -5,10 +5,6 @@ plugins { description = "Spring Boot Jetty SSL smoke test" -configurations.all { - resolutionStrategy.force("jakarta.servlet:jakarta.servlet-api:5.0.0") -} - dependencies { implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-jetty")) implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web")) { diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/build.gradle index 94173aa1ba18..b204e4a35e31 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/build.gradle @@ -5,10 +5,6 @@ plugins { description = "Spring Boot Jetty smoke test" -configurations.all { - resolutionStrategy.force("jakarta.servlet:jakarta.servlet-api:5.0.0") -} - dependencies { implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web")) { exclude module: "spring-boot-starter-tomcat" diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/main/resources/application.properties index 877098676857..0bb34b330311 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/main/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/main/resources/application.properties @@ -2,4 +2,4 @@ server.compression.enabled: true server.compression.min-response-size: 1 server.max-http-request-header-size=1000 server.jetty.threads.acceptors=2 -server.jetty.max-http-response-header-size=1000 +server.jetty.max-http-response-header-size=4096 diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/test/java/smoketest/jetty/SampleJettyApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/test/java/smoketest/jetty/SampleJettyApplicationTests.java index abde5d9033a5..d72e2c2d7fc8 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/test/java/smoketest/jetty/SampleJettyApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/test/java/smoketest/jetty/SampleJettyApplicationTests.java @@ -42,7 +42,7 @@ * @author Florian Storz * @author Michael Weidmann */ -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "logging.level.org.eclipse:trace") @ExtendWith(OutputCaptureExtension.class) class SampleJettyApplicationTests { @@ -65,9 +65,8 @@ void testCompression() { ResponseEntity entity = this.restTemplate.getForEntity("/", String.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(entity.getBody()).isEqualTo("Hello World"); - // Jetty HttpClient decodes gzip reponses automatically - // Check that we received a gzip-encoded response - assertThat(entity.getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING)).isEqualTo("gzip"); + // Jetty HttpClient decodes gzip reponses automatically and removes the + // Content-Encoding header. We have to assume that the response was gzipped. } @Test diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/build.gradle index e09d6e6806b5..58e1f903b5c6 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/build.gradle @@ -5,10 +5,6 @@ plugins { description = "Spring Boot WebSocket Jetty smoke test" -configurations.all { - resolutionStrategy.force("jakarta.servlet:jakarta.servlet-api:5.0.0") -} - dependencies { implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-jetty")) implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-websocket")) { From 15c24d35fd5a51836e534b0604f87ca66686e74a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 Sep 2023 10:17:47 +0100 Subject: [PATCH 0384/1215] Start building against Spring GraphQL 1.2.3 snapshots See gh-37232 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 3e1c8aa77487..63cab2f44789 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1535,7 +1535,7 @@ bom { ] } } - library("Spring GraphQL", "1.2.2") { + library("Spring GraphQL", "1.2.3-SNAPSHOT") { considerSnapshots() group("org.springframework.graphql") { modules = [ From 7d9b0cee39c9576bb6c35b4c4d407d176a75c385 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 Sep 2023 10:17:52 +0100 Subject: [PATCH 0385/1215] Start building against Spring Integration 6.2.0 snapshots See gh-37233 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 63cab2f44789..e37ee9b9ef5a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1552,7 +1552,7 @@ bom { ] } } - library("Spring Integration", "6.2.0-M2") { + library("Spring Integration", "6.2.0-SNAPSHOT") { considerSnapshots() group("org.springframework.integration") { imports = [ From c49e1dda337e052452226954f7bbb575ebc71fec Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 Sep 2023 10:17:57 +0100 Subject: [PATCH 0386/1215] Start building against Spring Kafka 3.1.0 snapshots See gh-37234 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e37ee9b9ef5a..23811dc48260 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1560,7 +1560,7 @@ bom { ] } } - library("Spring Kafka", "3.0.10") { + library("Spring Kafka", "3.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.kafka") { modules = [ From a94f171cae3312286e547fa8b5ff14b7e652ee8b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 Sep 2023 10:18:02 +0100 Subject: [PATCH 0387/1215] Start building against Spring LDAP 3.2.0 snapshots See gh-37235 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 23811dc48260..31e5ef709e9c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1569,7 +1569,7 @@ bom { ] } } - library("Spring LDAP", "3.2.0-M2") { + library("Spring LDAP", "3.2.0-SNAPSHOT") { considerSnapshots() group("org.springframework.ldap") { modules = [ From 9b52ee5dac95cd688305dea65364c97be7dec116 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 Sep 2023 10:18:07 +0100 Subject: [PATCH 0388/1215] Start building against Spring Security 6.2.0 snapshots See gh-37236 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 31e5ef709e9c..cd8c23d634dc 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1604,7 +1604,7 @@ bom { ] } } - library("Spring Security", "6.2.0-M2") { + library("Spring Security", "6.2.0-SNAPSHOT") { considerSnapshots() group("org.springframework.security") { imports = [ From 1d7c0108d06401c0a9e28d7b47ea3834b4a6d273 Mon Sep 17 00:00:00 2001 From: dkswnkk Date: Wed, 6 Sep 2023 23:58:04 +0900 Subject: [PATCH 0389/1215] Capitalize order constant in TomcatWebServerFactoryCustomizer See gh-37211 --- .../TomcatVirtualThreadsWebServerFactoryCustomizer.java | 2 +- .../web/embedded/TomcatWebServerFactoryCustomizer.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatVirtualThreadsWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatVirtualThreadsWebServerFactoryCustomizer.java index 9af929ffaa82..54ba36c7e67f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatVirtualThreadsWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatVirtualThreadsWebServerFactoryCustomizer.java @@ -41,7 +41,7 @@ public void customize(ConfigurableTomcatWebServerFactory factory) { @Override public int getOrder() { - return TomcatWebServerFactoryCustomizer.order + 1; + return TomcatWebServerFactoryCustomizer.ORDER + 1; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java index 1e8250deda87..1c357d2d0ac0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java @@ -67,7 +67,7 @@ public class TomcatWebServerFactoryCustomizer implements WebServerFactoryCustomizer, Ordered { - static final int order = 0; + static final int ORDER = 0; private final Environment environment; @@ -80,7 +80,7 @@ public TomcatWebServerFactoryCustomizer(Environment environment, ServerPropertie @Override public int getOrder() { - return order; + return ORDER; } @Override From 32b65e85aee452e69c6e96820da86885d3761708 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 8 Sep 2023 14:58:24 +0200 Subject: [PATCH 0390/1215] Add config property for GraphQL Schema Mapping Inspection This commit adds a new `spring.graphql.schema.inspection.enabled` property, which is `true` by default. This property enables the logging at the INFO level of the GraphQL Schema inspection report. During startup, Spring for GraphQL will inspect the schema and report fields and registrations that are unmapped in the application. Closes gh-36252 --- .../graphql/GraphQlAutoConfiguration.java | 3 +++ .../graphql/GraphQlProperties.java | 24 +++++++++++++++++++ .../GraphQlAutoConfigurationTests.java | 9 +++++++ 3 files changed, 36 insertions(+) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfiguration.java index a42ef90a10d1..886e597a5220 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfiguration.java @@ -104,6 +104,9 @@ public GraphQlSource graphQlSource(ResourcePatternResolver resourcePatternResolv .exceptionResolvers(exceptionResolvers.orderedStream().toList()) .subscriptionExceptionResolvers(subscriptionExceptionResolvers.orderedStream().toList()) .instrumentation(instrumentations.orderedStream().toList()); + if (properties.getSchema().getInspection().isEnabled()) { + builder.inspectSchemaMappings(logger::info); + } if (!properties.getSchema().getIntrospection().isEnabled()) { builder.configureRuntimeWiring(this::enableIntrospection); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlProperties.java index 1ea766a8be68..68ddfb07a726 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlProperties.java @@ -79,6 +79,8 @@ public static class Schema { */ private String[] fileExtensions = new String[] { ".graphqls", ".gqls" }; + private final Inspection inspection = new Inspection(); + private final Introspection introspection = new Introspection(); private final Printer printer = new Printer(); @@ -105,6 +107,10 @@ private String[] appendSlashIfNecessary(String[] locations) { .toArray(String[]::new); } + public Inspection getInspection() { + return this.inspection; + } + public Introspection getIntrospection() { return this.introspection; } @@ -113,6 +119,24 @@ public Printer getPrinter() { return this.printer; } + public static class Inspection { + + /** + * Whether schema should be compared to the application to detect missing + * mappings. + */ + private boolean enabled = true; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + } + public static class Introspection { /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java index 100cd9ddda09..3b7182e18ab2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java @@ -30,6 +30,7 @@ import graphql.schema.visibility.NoIntrospectionGraphqlFieldVisibility; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; @@ -37,6 +38,8 @@ import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration.GraphQlResourcesRuntimeHints; import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ByteArrayResource; @@ -56,6 +59,7 @@ /** * Tests for {@link GraphQlAutoConfiguration}. */ +@ExtendWith(OutputCaptureExtension.class) class GraphQlAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() @@ -158,6 +162,11 @@ void shouldApplyGraphQlSourceBuilderCustomizer() { }); } + @Test + void schemaInspectionShouldBeEnabledByDefault(CapturedOutput output) { + this.contextRunner.run((context) -> assertThat(output).contains("GraphQL schema inspection")); + } + @Test void fieldIntrospectionShouldBeEnabledByDefault() { this.contextRunner.run((context) -> { From d8c3902a7e6d4162c49a84554cc91d8084de698b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 8 Sep 2023 14:56:18 +0100 Subject: [PATCH 0391/1215] Polish --- .../boot/context/config/ConfigDataLocationRuntimeHints.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationRuntimeHints.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationRuntimeHints.java index c914f935c260..7998837e0b6b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationRuntimeHints.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationRuntimeHints.java @@ -49,9 +49,9 @@ public void registerHints(RuntimeHints hints, ClassLoader classLoader) { logger.debug("Registering application configuration hints for " + fileNames + "(" + extensions + ") at " + locations); } - FilePatternResourceHintsRegistrar.forClassPathLocations(locations.toArray(new String[0])) - .withFilePrefixes(fileNames.toArray(new String[0])) - .withFileExtensions(extensions.toArray(new String[0])) + FilePatternResourceHintsRegistrar.forClassPathLocations(locations) + .withFilePrefixes(fileNames) + .withFileExtensions(extensions) .registerHints(hints.resources(), classLoader); } From 16940518c1354a88640a9e4141f7e3f08ad3ad99 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 8 Sep 2023 17:52:42 +0200 Subject: [PATCH 0392/1215] Polish GraphQL QueryBE and QueryDSL auto-configurations Closes gh-34974 --- ...raphQlQueryByExampleAutoConfiguration.java | 13 ++-- .../GraphQlQuerydslAutoConfiguration.java | 13 ++-- ...raphQlQuerydslSourceBuilderCustomizer.java | 65 ------------------- ...activeQueryByExampleAutoConfiguration.java | 10 +-- ...phQlReactiveQuerydslAutoConfiguration.java | 10 +-- ...lQueryByExampleAutoConfigurationTests.java | 2 +- ...GraphQlQuerydslAutoConfigurationTests.java | 2 +- 7 files changed, 28 insertions(+), 87 deletions(-) delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQuerydslSourceBuilderCustomizer.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQueryByExampleAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQueryByExampleAutoConfiguration.java index 52df8e11cb43..42d79c0f31c9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQueryByExampleAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQueryByExampleAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -16,6 +16,7 @@ package org.springframework.boot.autoconfigure.graphql.data; +import java.util.Collections; import java.util.List; import graphql.GraphQL; @@ -29,9 +30,9 @@ import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.data.repository.query.QueryByExampleExecutor; -import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; import org.springframework.graphql.data.query.QueryByExampleDataFetcher; import org.springframework.graphql.execution.GraphQlSource; +import org.springframework.graphql.execution.RuntimeWiringConfigurer; /** * {@link EnableAutoConfiguration Auto-configuration} that creates a @@ -49,10 +50,10 @@ public class GraphQlQueryByExampleAutoConfiguration { @Bean - public GraphQlSourceBuilderCustomizer queryByExampleRegistrar(ObjectProvider> executors, - ObjectProvider> reactiveExecutors) { - return new GraphQlQuerydslSourceBuilderCustomizer<>(QueryByExampleDataFetcher::autoRegistrationConfigurer, - executors, reactiveExecutors); + public GraphQlSourceBuilderCustomizer queryByExampleRegistrar(ObjectProvider> executors) { + RuntimeWiringConfigurer configurer = QueryByExampleDataFetcher + .autoRegistrationConfigurer(executors.orderedStream().toList(), Collections.emptyList()); + return (builder) -> builder.configureRuntimeWiring(configurer); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQuerydslAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQuerydslAutoConfiguration.java index c32b21b7811f..ca168f833e69 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQuerydslAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQuerydslAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -16,6 +16,7 @@ package org.springframework.boot.autoconfigure.graphql.data; +import java.util.Collections; import java.util.List; import graphql.GraphQL; @@ -29,9 +30,9 @@ import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.data.querydsl.QuerydslPredicateExecutor; -import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; import org.springframework.graphql.data.query.QuerydslDataFetcher; import org.springframework.graphql.execution.GraphQlSource; +import org.springframework.graphql.execution.RuntimeWiringConfigurer; /** * {@link EnableAutoConfiguration Auto-configuration} that creates a @@ -50,10 +51,10 @@ public class GraphQlQuerydslAutoConfiguration { @Bean - public GraphQlSourceBuilderCustomizer querydslRegistrar(ObjectProvider> executors, - ObjectProvider> reactiveExecutors) { - return new GraphQlQuerydslSourceBuilderCustomizer<>(QuerydslDataFetcher::autoRegistrationConfigurer, executors, - reactiveExecutors); + public GraphQlSourceBuilderCustomizer querydslRegistrar(ObjectProvider> executors) { + RuntimeWiringConfigurer configurer = QuerydslDataFetcher + .autoRegistrationConfigurer(executors.orderedStream().toList(), Collections.emptyList()); + return (builder) -> builder.configureRuntimeWiring(configurer); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQuerydslSourceBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQuerydslSourceBuilderCustomizer.java deleted file mode 100644 index ae0db51b20ab..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQuerydslSourceBuilderCustomizer.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2012-2022 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.graphql.data; - -import java.util.Collections; -import java.util.List; -import java.util.function.BiFunction; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer; -import org.springframework.graphql.execution.GraphQlSource; -import org.springframework.graphql.execution.RuntimeWiringConfigurer; - -/** - * {@link GraphQlSourceBuilderCustomizer} to apply auto-configured QueryDSL - * {@link RuntimeWiringConfigurer RuntimeWiringConfigurers}. - * - * @param the executor type - * @param the reactive executor type - * @author Phillip Webb - * @author Rossen Stoyanchev - * @author Brian Clozel - */ -class GraphQlQuerydslSourceBuilderCustomizer implements GraphQlSourceBuilderCustomizer { - - private final BiFunction, List, RuntimeWiringConfigurer> wiringConfigurerFactory; - - private final List executors; - - private final List reactiveExecutors; - - GraphQlQuerydslSourceBuilderCustomizer( - BiFunction, List, RuntimeWiringConfigurer> wiringConfigurerFactory, ObjectProvider executors, - ObjectProvider reactiveExecutors) { - this.wiringConfigurerFactory = wiringConfigurerFactory; - this.executors = asList(executors); - this.reactiveExecutors = asList(reactiveExecutors); - } - - private static List asList(ObjectProvider provider) { - return (provider != null) ? provider.orderedStream().toList() : Collections.emptyList(); - } - - @Override - public void customize(GraphQlSource.SchemaResourceBuilder builder) { - if (!this.executors.isEmpty() || !this.reactiveExecutors.isEmpty()) { - builder.configureRuntimeWiring(this.wiringConfigurerFactory.apply(this.executors, this.reactiveExecutors)); - } - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlReactiveQueryByExampleAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlReactiveQueryByExampleAutoConfiguration.java index f28b17801ef1..6e784108e9da 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlReactiveQueryByExampleAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlReactiveQueryByExampleAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -16,6 +16,7 @@ package org.springframework.boot.autoconfigure.graphql.data; +import java.util.Collections; import java.util.List; import graphql.GraphQL; @@ -28,10 +29,10 @@ import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration; import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer; import org.springframework.context.annotation.Bean; -import org.springframework.data.repository.query.QueryByExampleExecutor; import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor; import org.springframework.graphql.data.query.QueryByExampleDataFetcher; import org.springframework.graphql.execution.GraphQlSource; +import org.springframework.graphql.execution.RuntimeWiringConfigurer; /** * {@link EnableAutoConfiguration Auto-configuration} that creates a @@ -51,8 +52,9 @@ public class GraphQlReactiveQueryByExampleAutoConfiguration { @Bean public GraphQlSourceBuilderCustomizer reactiveQueryByExampleRegistrar( ObjectProvider> reactiveExecutors) { - return new GraphQlQuerydslSourceBuilderCustomizer<>(QueryByExampleDataFetcher::autoRegistrationConfigurer, - (ObjectProvider>) null, reactiveExecutors); + RuntimeWiringConfigurer configurer = QueryByExampleDataFetcher + .autoRegistrationConfigurer(Collections.emptyList(), reactiveExecutors.orderedStream().toList()); + return (builder) -> builder.configureRuntimeWiring(configurer); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlReactiveQuerydslAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlReactiveQuerydslAutoConfiguration.java index f12be0563ac7..9d9cc4c61097 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlReactiveQuerydslAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlReactiveQuerydslAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -16,6 +16,7 @@ package org.springframework.boot.autoconfigure.graphql.data; +import java.util.Collections; import java.util.List; import graphql.GraphQL; @@ -28,10 +29,10 @@ import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration; import org.springframework.boot.autoconfigure.graphql.GraphQlSourceBuilderCustomizer; import org.springframework.context.annotation.Bean; -import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; import org.springframework.graphql.data.query.QuerydslDataFetcher; import org.springframework.graphql.execution.GraphQlSource; +import org.springframework.graphql.execution.RuntimeWiringConfigurer; /** * {@link EnableAutoConfiguration Auto-configuration} that creates a @@ -52,8 +53,9 @@ public class GraphQlReactiveQuerydslAutoConfiguration { @Bean public GraphQlSourceBuilderCustomizer reactiveQuerydslRegistrar( ObjectProvider> reactiveExecutors) { - return new GraphQlQuerydslSourceBuilderCustomizer<>(QuerydslDataFetcher::autoRegistrationConfigurer, - (ObjectProvider>) null, reactiveExecutors); + RuntimeWiringConfigurer configurer = QuerydslDataFetcher.autoRegistrationConfigurer(Collections.emptyList(), + reactiveExecutors.orderedStream().toList()); + return (builder) -> builder.configureRuntimeWiring(configurer); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQueryByExampleAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQueryByExampleAutoConfigurationTests.java index 0998bfe10a92..964bf0ebaff6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQueryByExampleAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQueryByExampleAutoConfigurationTests.java @@ -49,7 +49,7 @@ class GraphQlQueryByExampleAutoConfigurationTests { .withConfiguration( AutoConfigurations.of(GraphQlAutoConfiguration.class, GraphQlQueryByExampleAutoConfiguration.class)) .withUserConfiguration(MockRepositoryConfig.class) - .withPropertyValues("spring.main.web-application-type=reactive"); + .withPropertyValues("spring.main.web-application-type=servlet"); @Test void shouldRegisterDataFetcherForQueryByExampleRepositories() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQuerydslAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQuerydslAutoConfigurationTests.java index 3bbb3df8a03b..608733146946 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQuerydslAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQuerydslAutoConfigurationTests.java @@ -50,7 +50,7 @@ class GraphQlQuerydslAutoConfigurationTests { .withConfiguration( AutoConfigurations.of(GraphQlAutoConfiguration.class, GraphQlQuerydslAutoConfiguration.class)) .withUserConfiguration(MockRepositoryConfig.class) - .withPropertyValues("spring.main.web-application-type=reactive"); + .withPropertyValues("spring.main.web-application-type=servlet"); @Test void shouldRegisterDataFetcherForQueryDslRepositories() { From 31fc124c772c5fd92e426081b2f388c25b1d3e38 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 08:57:55 +0200 Subject: [PATCH 0393/1215] Start building against Spring Retry 2.0.3 snapshots See gh-37281 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5d7e325693f5..21a219c6813a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1598,7 +1598,7 @@ bom { ] } } - library("Spring Retry", "2.0.2") { + library("Spring Retry", "2.0.3-SNAPSHOT") { considerSnapshots() group("org.springframework.retry") { modules = [ From 11ebe32dcf486d8d648b155afc3be6b7cc24b22d Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Mon, 11 Sep 2023 09:49:28 +0200 Subject: [PATCH 0394/1215] Remove dependency to opentelemetry-sdk Closes gh-37284 --- .../OpenTelemetryAutoConfiguration.java | 13 ++++- .../OpenTelemetryProperties.java | 12 ---- .../OpenTelemetryPropertiesTests.java | 55 +++++++++++++++++++ 3 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryPropertiesTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java index 92bb18e374ad..8a21539df875 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java @@ -16,6 +16,8 @@ package org.springframework.boot.actuate.autoconfigure.opentelemetry; +import java.util.Map.Entry; + import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.context.propagation.ContextPropagators; @@ -24,6 +26,7 @@ import io.opentelemetry.sdk.logs.SdkLoggerProvider; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.ResourceBuilder; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; @@ -71,7 +74,15 @@ Resource openTelemetryResource(Environment environment, OpenTelemetryProperties String applicationName = environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME); return Resource.getDefault() .merge(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, applicationName))) - .merge(properties.toResource()); + .merge(toResource(properties)); + } + + private static Resource toResource(OpenTelemetryProperties properties) { + ResourceBuilder builder = Resource.builder(); + for (Entry entry : properties.getResourceAttributes().entrySet()) { + builder.put(entry.getKey(), entry.getValue()); + } + return builder.build(); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryProperties.java index 4b2b68da0cdb..4c973ecf578b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryProperties.java @@ -18,10 +18,6 @@ import java.util.HashMap; import java.util.Map; -import java.util.Map.Entry; - -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.resources.ResourceBuilder; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -47,12 +43,4 @@ public void setResourceAttributes(Map resourceAttributes) { this.resourceAttributes = resourceAttributes; } - Resource toResource() { - ResourceBuilder builder = Resource.builder(); - for (Entry entry : this.resourceAttributes.entrySet()) { - builder.put(entry.getKey(), entry.getValue()); - } - return builder.build(); - } - } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryPropertiesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryPropertiesTests.java new file mode 100644 index 000000000000..fd754322f303 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryPropertiesTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.opentelemetry; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * Tests for {@link OpenTelemetryProperties}. + * + * @author Moritz Halbritter + */ +class OpenTelemetryPropertiesTests { + + private final ApplicationContextRunner runner = new ApplicationContextRunner().withPropertyValues( + "management.opentelemetry.resource-attributes.a=alpha", + "management.opentelemetry.resource-attributes.b=beta"); + + @Test + @ClassPathExclusions("opentelemetry-sdk-*") + void shouldNotDependOnOpenTelemetrySdk() { + this.runner.withUserConfiguration(TestConfiguration.class).run((context) -> { + OpenTelemetryProperties properties = context.getBean(OpenTelemetryProperties.class); + assertThat(properties.getResourceAttributes()).containsOnly(entry("a", "alpha"), entry("b", "beta")); + }); + } + + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(OpenTelemetryProperties.class) + private static class TestConfiguration { + + } + +} From fa423166520820047aeaf89d7ed068d1ef9ed12a Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Mon, 11 Sep 2023 10:38:51 +0200 Subject: [PATCH 0395/1215] Use spring.application.name for OTel service.name when not set Closes gh-37285 --- .../OtlpMetricsExportAutoConfiguration.java | 16 ++------ .../otlp/OtlpPropertiesConfigAdapter.java | 24 ++++++++++-- .../OtlpPropertiesConfigAdapterTests.java | 37 +++++++++++++++++-- 3 files changed, 59 insertions(+), 18 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java index 5ee64e539ee4..27e974bab84d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java @@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; /** * {@link EnableAutoConfiguration Auto-configuration} for exporting metrics to OTLP. @@ -49,20 +50,11 @@ @EnableConfigurationProperties({ OtlpProperties.class, OpenTelemetryProperties.class }) public class OtlpMetricsExportAutoConfiguration { - private final OtlpProperties properties; - - private final OpenTelemetryProperties openTelemetryProperties; - - public OtlpMetricsExportAutoConfiguration(OtlpProperties properties, - OpenTelemetryProperties openTelemetryProperties) { - this.properties = properties; - this.openTelemetryProperties = openTelemetryProperties; - } - @Bean @ConditionalOnMissingBean - public OtlpConfig otlpConfig() { - return new OtlpPropertiesConfigAdapter(this.properties, this.openTelemetryProperties); + OtlpConfig otlpConfig(OtlpProperties properties, OpenTelemetryProperties openTelemetryProperties, + Environment environment) { + return new OtlpPropertiesConfigAdapter(properties, openTelemetryProperties, environment); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java index ebf406f30f93..f8dd3c5bb247 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java @@ -16,6 +16,8 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.otlp; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -24,6 +26,7 @@ import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.StepRegistryPropertiesConfigAdapter; import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryProperties; +import org.springframework.core.env.Environment; import org.springframework.util.CollectionUtils; /** @@ -35,11 +38,20 @@ */ class OtlpPropertiesConfigAdapter extends StepRegistryPropertiesConfigAdapter implements OtlpConfig { + /** + * Default value for application name if {@code spring.application.name} is not set. + */ + private static final String DEFAULT_APPLICATION_NAME = "application"; + private final OpenTelemetryProperties openTelemetryProperties; - OtlpPropertiesConfigAdapter(OtlpProperties properties, OpenTelemetryProperties openTelemetryProperties) { + private final Environment environment; + + OtlpPropertiesConfigAdapter(OtlpProperties properties, OpenTelemetryProperties openTelemetryProperties, + Environment environment) { super(properties); this.openTelemetryProperties = openTelemetryProperties; + this.environment = environment; } @Override @@ -60,10 +72,16 @@ public AggregationTemporality aggregationTemporality() { @Override @SuppressWarnings("removal") public Map resourceAttributes() { + Map result; if (!CollectionUtils.isEmpty(this.openTelemetryProperties.getResourceAttributes())) { - return this.openTelemetryProperties.getResourceAttributes(); + result = new HashMap<>(this.openTelemetryProperties.getResourceAttributes()); + } + else { + result = new HashMap<>(get(OtlpProperties::getResourceAttributes, OtlpConfig.super::resourceAttributes)); } - return get(OtlpProperties::getResourceAttributes, OtlpConfig.super::resourceAttributes); + result.computeIfAbsent("service.name", + (ignore) -> this.environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME)); + return Collections.unmodifiableMap(result); } @Override diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java index 27bd22ccb7a3..8d736bca0e44 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryProperties; +import org.springframework.mock.env.MockEnvironment; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; @@ -41,10 +42,13 @@ class OtlpPropertiesConfigAdapterTests { private OpenTelemetryProperties openTelemetryProperties; + private MockEnvironment environment; + @BeforeEach void setUp() { this.properties = new OtlpProperties(); this.openTelemetryProperties = new OpenTelemetryProperties(); + this.environment = new MockEnvironment(); } @Test @@ -93,7 +97,8 @@ void whenPropertiesBaseTimeUnitIsSetAdapterBaseTimeUnitReturnsIt() { void openTelemetryPropertiesShouldOverrideOtlpPropertiesIfNotEmpty() { this.properties.setResourceAttributes(Map.of("a", "alpha")); this.openTelemetryProperties.setResourceAttributes(Map.of("b", "beta")); - assertThat(createAdapter().resourceAttributes()).containsExactly(entry("b", "beta")); + assertThat(createAdapter().resourceAttributes()).contains(entry("b", "beta")); + assertThat(createAdapter().resourceAttributes()).doesNotContain(entry("a", "alpha")); } @Test @@ -101,11 +106,37 @@ void openTelemetryPropertiesShouldOverrideOtlpPropertiesIfNotEmpty() { void openTelemetryPropertiesShouldNotOverrideOtlpPropertiesIfEmpty() { this.properties.setResourceAttributes(Map.of("a", "alpha")); this.openTelemetryProperties.setResourceAttributes(Collections.emptyMap()); - assertThat(createAdapter().resourceAttributes()).containsExactly(entry("a", "alpha")); + assertThat(createAdapter().resourceAttributes()).contains(entry("a", "alpha")); + } + + @Test + @SuppressWarnings("removal") + void serviceNameOverridesApplicationName() { + this.environment.setProperty("spring.application.name", "alpha"); + this.properties.setResourceAttributes(Map.of("service.name", "beta")); + assertThat(createAdapter().resourceAttributes()).containsEntry("service.name", "beta"); + } + + @Test + void serviceNameOverridesApplicationNameWhenUsingOtelProperties() { + this.environment.setProperty("spring.application.name", "alpha"); + this.openTelemetryProperties.setResourceAttributes(Map.of("service.name", "beta")); + assertThat(createAdapter().resourceAttributes()).containsEntry("service.name", "beta"); + } + + @Test + void shouldUseApplicationNameIfServiceNameIsNotSet() { + this.environment.setProperty("spring.application.name", "alpha"); + assertThat(createAdapter().resourceAttributes()).containsEntry("service.name", "alpha"); + } + + @Test + void shouldUseDefaultApplicationNameIfApplicationNameIsNotSet() { + assertThat(createAdapter().resourceAttributes()).containsEntry("service.name", "application"); } private OtlpPropertiesConfigAdapter createAdapter() { - return new OtlpPropertiesConfigAdapter(this.properties, this.openTelemetryProperties); + return new OtlpPropertiesConfigAdapter(this.properties, this.openTelemetryProperties, this.environment); } } From 10f9f29b2b1b253f548d511d6db51c53b21c2087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edd=C3=BA=20Mel=C3=A9ndez?= Date: Fri, 8 Sep 2023 11:04:17 -0600 Subject: [PATCH 0396/1215] Add pulsar starters into dependency management Spring for Apache Pulsar was added, introducing two new starters. Those starters should be listed in dependency management. See gh-37272 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 21a219c6813a..c49b666ccaec 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1420,6 +1420,8 @@ bom { "spring-boot-starter-oauth2-authorization-server", "spring-boot-starter-oauth2-client", "spring-boot-starter-oauth2-resource-server", + "spring-boot-starter-pulsar", + "spring-boot-starter-pulsar-reactive", "spring-boot-starter-quartz", "spring-boot-starter-reactor-netty", "spring-boot-starter-rsocket", From eb6b151c41bf86e243d8cc8e96b60d484d912284 Mon Sep 17 00:00:00 2001 From: Zisis Pavloudis <25851168+zpavloudis@users.noreply.github.com> Date: Mon, 28 Aug 2023 20:22:32 +0300 Subject: [PATCH 0397/1215] Support unwrapping in ValidatorAdapter See gh-37119 --- .../autoconfigure/validation/ValidatorAdapter.java | 14 ++++++++++++++ .../validation/ValidatorAdapterTests.java | 12 ++++++++++++ 2 files changed, 26 insertions(+) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidatorAdapter.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidatorAdapter.java index 700a68a91610..2f2e5e4283ea 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidatorAdapter.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidatorAdapter.java @@ -39,6 +39,7 @@ * * @author Stephane Nicoll * @author Phillip Webb + * @author Zisis Pavloudis * @since 2.0.0 */ public class ValidatorAdapter implements SmartValidator, ApplicationContextAware, InitializingBean, DisposableBean { @@ -153,4 +154,17 @@ private static Validator wrap(Validator validator, boolean existingBean) { return validator; } + @Override + @SuppressWarnings("unchecked") + public T unwrap(Class type) { + if (type.isAssignableFrom(this.target.getClass())) { + if (this.target instanceof SpringValidatorAdapter adapter) { + return adapter.unwrap(type); + } + return (T) this.target; + } + + throw new IllegalArgumentException("Cannot unwrap " + this.target + " to " + type.getName()); + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidatorAdapterTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidatorAdapterTests.java index bf57084b98ef..2230b94d612f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidatorAdapterTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidatorAdapterTests.java @@ -18,7 +18,10 @@ import java.util.HashMap; +import jakarta.validation.Validator; import jakarta.validation.constraints.Min; +import org.hibernate.validator.HibernateValidator; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.FilteredClassLoader; @@ -91,6 +94,15 @@ void wrapperWhenValidationProviderNotPresentShouldNotThrowException() { .run((context) -> ValidatorAdapter.get(context, null)); } + @Test + void unwrapValidatorInstanceOfJakartaTypeAndExceptionThrownWhenTypeNotSupported() { + this.contextRunner.withUserConfiguration(LocalValidatorFactoryBeanConfig.class).run((context) -> { + ValidatorAdapter wrapper = context.getBean(ValidatorAdapter.class); + assertThat(wrapper.unwrap(Validator.class)).isInstanceOf(Validator.class); + Assertions.assertThrows(IllegalArgumentException.class, () -> wrapper.unwrap(HibernateValidator.class)); + }); + } + @Configuration(proxyBeanMethods = false) static class LocalValidatorFactoryBeanConfig { From 4085425f915344258bfb54a95233a2d383f0b44e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 11 Sep 2023 11:03:33 +0100 Subject: [PATCH 0398/1215] Polish "Support unwrapping in ValidatorAdapter" See gh-37119 --- .../validation/ValidatorAdapter.java | 8 +-- .../validation/ValidatorAdapterTests.java | 72 ++++++++++++++++++- 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidatorAdapter.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidatorAdapter.java index 2f2e5e4283ea..e6c792838d86 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidatorAdapter.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/validation/ValidatorAdapter.java @@ -157,14 +157,10 @@ private static Validator wrap(Validator validator, boolean existingBean) { @Override @SuppressWarnings("unchecked") public T unwrap(Class type) { - if (type.isAssignableFrom(this.target.getClass())) { - if (this.target instanceof SpringValidatorAdapter adapter) { - return adapter.unwrap(type); - } + if (type.isInstance(this.target)) { return (T) this.target; } - - throw new IllegalArgumentException("Cannot unwrap " + this.target + " to " + type.getName()); + return this.target.unwrap(type); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidatorAdapterTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidatorAdapterTests.java index 2230b94d612f..641c45dc220f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidatorAdapterTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidatorAdapterTests.java @@ -21,7 +21,6 @@ import jakarta.validation.Validator; import jakarta.validation.constraints.Min; import org.hibernate.validator.HibernateValidator; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.FilteredClassLoader; @@ -30,10 +29,13 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; +import org.springframework.validation.Errors; import org.springframework.validation.MapBindingResult; +import org.springframework.validation.SmartValidator; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatRuntimeException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; @@ -95,11 +97,26 @@ void wrapperWhenValidationProviderNotPresentShouldNotThrowException() { } @Test - void unwrapValidatorInstanceOfJakartaTypeAndExceptionThrownWhenTypeNotSupported() { + void unwrapToJakartaValidatorShouldReturnJakartaValidator() { this.contextRunner.withUserConfiguration(LocalValidatorFactoryBeanConfig.class).run((context) -> { ValidatorAdapter wrapper = context.getBean(ValidatorAdapter.class); assertThat(wrapper.unwrap(Validator.class)).isInstanceOf(Validator.class); - Assertions.assertThrows(IllegalArgumentException.class, () -> wrapper.unwrap(HibernateValidator.class)); + }); + } + + @Test + void whenJakartaValidatorIsWrappedMultipleTimesUnwrapToJakartaValidatorShouldReturnJakartaValidator() { + this.contextRunner.withUserConfiguration(DoubleWrappedConfig.class).run((context) -> { + ValidatorAdapter wrapper = context.getBean(ValidatorAdapter.class); + assertThat(wrapper.unwrap(Validator.class)).isInstanceOf(Validator.class); + }); + } + + @Test + void unwrapToUnsupportedTypeShouldThrow() { + this.contextRunner.withUserConfiguration(LocalValidatorFactoryBeanConfig.class).run((context) -> { + ValidatorAdapter wrapper = context.getBean(ValidatorAdapter.class); + assertThatRuntimeException().isThrownBy(() -> wrapper.unwrap(HibernateValidator.class)); }); } @@ -118,6 +135,55 @@ ValidatorAdapter wrapper(LocalValidatorFactoryBean validator) { } + @Configuration(proxyBeanMethods = false) + static class DoubleWrappedConfig { + + @Bean + LocalValidatorFactoryBean validator() { + return new LocalValidatorFactoryBean(); + } + + @Bean + ValidatorAdapter wrapper(LocalValidatorFactoryBean validator) { + return new ValidatorAdapter(new Wrapper(validator), true); + } + + static class Wrapper implements SmartValidator { + + private final SmartValidator delegate; + + Wrapper(SmartValidator delegate) { + this.delegate = delegate; + } + + @Override + public boolean supports(Class clazz) { + return this.delegate.supports(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + this.delegate.validate(target, errors); + } + + @Override + public void validate(Object target, Errors errors, Object... validationHints) { + this.delegate.validate(target, errors, validationHints); + } + + @Override + @SuppressWarnings("unchecked") + public T unwrap(Class type) { + if (type.isInstance(this.delegate)) { + return (T) this.delegate; + } + return this.delegate.unwrap(type); + } + + } + + } + @Configuration(proxyBeanMethods = false) static class NonManagedBeanConfig { From c951c4c2121fc9258f6164213cd58ba475cb2c38 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 11 Sep 2023 12:55:27 +0200 Subject: [PATCH 0399/1215] Polish GraphQL auto-configuration changes This commit fixes build issues, as the recent changes surfaced an existing problem: QueryDsl auto-configurations were not guarded by classpath conditions for QueryDsl Core. See gh-34974 --- spring-boot-project/spring-boot-autoconfigure/build.gradle | 1 + .../graphql/data/GraphQlQuerydslAutoConfiguration.java | 3 ++- .../graphql/data/GraphQlReactiveQuerydslAutoConfiguration.java | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index ed61c0213dd6..85a72c0600cd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -29,6 +29,7 @@ dependencies { optional("com.nimbusds:oauth2-oidc-sdk") optional("com.oracle.database.jdbc:ojdbc8") optional("com.oracle.database.jdbc:ucp") + optional("com.querydsl:querydsl-core") optional("com.samskivert:jmustache") optional("io.lettuce:lettuce-core") optional("io.projectreactor.netty:reactor-netty-http") diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQuerydslAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQuerydslAutoConfiguration.java index ca168f833e69..97e2debd0a4b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQuerydslAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQuerydslAutoConfiguration.java @@ -19,6 +19,7 @@ import java.util.Collections; import java.util.List; +import com.querydsl.core.Query; import graphql.GraphQL; import org.springframework.beans.factory.ObjectProvider; @@ -46,7 +47,7 @@ * @see QuerydslDataFetcher#autoRegistrationConfigurer(List, List) */ @AutoConfiguration(after = GraphQlAutoConfiguration.class) -@ConditionalOnClass({ GraphQL.class, QuerydslDataFetcher.class, QuerydslPredicateExecutor.class }) +@ConditionalOnClass({ GraphQL.class, Query.class, QuerydslDataFetcher.class, QuerydslPredicateExecutor.class }) @ConditionalOnBean(GraphQlSource.class) public class GraphQlQuerydslAutoConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlReactiveQuerydslAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlReactiveQuerydslAutoConfiguration.java index 9d9cc4c61097..14b81dcc7108 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlReactiveQuerydslAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlReactiveQuerydslAutoConfiguration.java @@ -19,6 +19,7 @@ import java.util.Collections; import java.util.List; +import com.querydsl.core.Query; import graphql.GraphQL; import org.springframework.beans.factory.ObjectProvider; @@ -46,7 +47,7 @@ * @see QuerydslDataFetcher#autoRegistrationConfigurer(List, List) */ @AutoConfiguration(after = GraphQlAutoConfiguration.class) -@ConditionalOnClass({ GraphQL.class, QuerydslDataFetcher.class, ReactiveQuerydslPredicateExecutor.class }) +@ConditionalOnClass({ GraphQL.class, Query.class, QuerydslDataFetcher.class, ReactiveQuerydslPredicateExecutor.class }) @ConditionalOnBean(GraphQlSource.class) public class GraphQlReactiveQuerydslAutoConfiguration { From 0d902c13231f9f39a6e08c9764c518ae20c551bb Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 11 Sep 2023 15:04:43 +0200 Subject: [PATCH 0400/1215] Document Welcome Page support ordering This commit documents the relative ordering of `HandlerMapping` support in Spring MVC and WebFlux applications. As of Spring Framework 6.1.0, the Welcome Page support acts as a fallback in case no index route has been defined by the application as a `RouterFunction` or within an annotated `@Controller`. Closes gh-34846 --- .../src/docs/asciidoc/web/reactive.adoc | 15 +++++++++++++++ .../src/docs/asciidoc/web/servlet.adoc | 14 ++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc index 7b1c23005c4e..fbae93af9cfd 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc @@ -120,6 +120,21 @@ It first looks for an `index.html` file in the configured static content locatio If one is not found, it then looks for an `index` template. If either is found, it is automatically used as the welcome page of the application. +This only acts as a fallback for actual index routes defined by the application. +The ordering is defined by the order of `HandlerMapping` beans which is by default the following: + +[cols="1,1"] +|=== +|`RouterFunctionMapping` +|Endpoints declared with `RouterFunction` beans + +|`RequestMappingHandlerMapping` +|Endpoints declared in `@Controller` beans + +|`RouterFunctionMapping` for the Welcome Page +|The welcome page support +|=== + [[web.reactive.webflux.template-engines]] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc index c965ce1821bf..86ed5fbc3270 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc @@ -196,6 +196,20 @@ It first looks for an `index.html` file in the configured static content locatio If one is not found, it then looks for an `index` template. If either is found, it is automatically used as the welcome page of the application. +This only acts as a fallback for actual index routes defined by the application. +The ordering is defined by the order of `HandlerMapping` beans which is by default the following: + +[cols="1,1"] +|=== +|`RouterFunctionMapping` +|Endpoints declared with `RouterFunction` beans + +|`RequestMappingHandlerMapping` +|Endpoints declared in `@Controller` beans + +|`WelcomePageHandlerMapping` +|The welcome page support +|=== [[web.servlet.spring-mvc.favicon]] From 3a12a86ea26f30ca0d00aa71c8d6c345c13b8706 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:38:24 +0200 Subject: [PATCH 0401/1215] Upgrade to Byte Buddy 1.14.7 Closes gh-37311 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c49b666ccaec..f7a809db8dee 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -123,7 +123,7 @@ bom { ] } } - library("Byte Buddy", "1.14.6") { + library("Byte Buddy", "1.14.7") { group("net.bytebuddy") { modules = [ "byte-buddy", From 1ffb580d6fdd442697aeb08141210832cc99f243 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:38:28 +0200 Subject: [PATCH 0402/1215] Upgrade to Commons DBCP2 2.10.0 Closes gh-37312 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- .../metadata/CommonsDbcp2DataSourcePoolMetadataTests.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f7a809db8dee..5bd10861e164 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -179,7 +179,7 @@ bom { ] } } - library("Commons DBCP2", "2.9.0") { + library("Commons DBCP2", "2.10.0") { group("org.apache.commons") { modules = [ "commons-dbcp2" { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/metadata/CommonsDbcp2DataSourcePoolMetadataTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/metadata/CommonsDbcp2DataSourcePoolMetadataTests.java index 75d4ac1a55e9..72c9420a98ef 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/metadata/CommonsDbcp2DataSourcePoolMetadataTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/metadata/CommonsDbcp2DataSourcePoolMetadataTests.java @@ -16,6 +16,8 @@ package org.springframework.boot.jdbc.metadata; +import java.time.Duration; + import org.apache.commons.dbcp2.BasicDataSource; import org.junit.jupiter.api.Test; @@ -83,7 +85,7 @@ private CommonsDbcp2DataSourcePoolMetadata createDataSourceMetadata(int minSize, BasicDataSource dataSource = createDataSource(); dataSource.setMinIdle(minSize); dataSource.setMaxTotal(maxSize); - dataSource.setMinEvictableIdleTimeMillis(5000); + dataSource.setMinEvictableIdle(Duration.ofSeconds(5)); return new CommonsDbcp2DataSourcePoolMetadata(dataSource); } From 18177451a964268ade0456a8c8985b16109c5992 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:38:33 +0200 Subject: [PATCH 0403/1215] Upgrade to Couchbase Client 3.4.10 Closes gh-37313 --- .../boot/autoconfigure/couchbase/CouchbaseProperties.java | 2 +- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseProperties.java index 222d71794dce..d1e93181c570 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseProperties.java @@ -118,7 +118,7 @@ public static class Io { * Length of time an HTTP connection may remain idle before it is closed and * removed from the pool. */ - private Duration idleHttpConnectionTimeout = Duration.ofMillis(4500); + private Duration idleHttpConnectionTimeout = Duration.ofSeconds(1); public int getMinEndpoints() { return this.minEndpoints; diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5bd10861e164..905a9364ca56 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -209,7 +209,7 @@ bom { ] } } - library("Couchbase Client", "3.4.8") { + library("Couchbase Client", "3.4.10") { prohibit { versionRange "[3.4.9]" because "it contains unshaded io.opentelemetry classes that break our Otel integration" From 8283db300556b9e992fc977ad8c3542669eedac8 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:38:37 +0200 Subject: [PATCH 0404/1215] Upgrade to Elasticsearch Client 8.9.2 Closes gh-37314 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 905a9364ca56..bc454d41e236 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -268,7 +268,7 @@ bom { ] } } - library("Elasticsearch Client", "8.9.0") { + library("Elasticsearch Client", "8.9.2") { group("org.elasticsearch.client") { modules = [ "elasticsearch-rest-client" { From 0b07c24bbd3a1caa55707e5a19215deb3eb94f00 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:38:42 +0200 Subject: [PATCH 0405/1215] Upgrade to Flyway 9.22.0 Closes gh-37315 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index bc454d41e236..c1cbe53e340a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -285,7 +285,7 @@ bom { ] } } - library("Flyway", "9.21.2") { + library("Flyway", "9.22.0") { group("org.flywaydb") { modules = [ "flyway-core", From 172b4c846a8cc06a4e5c2974aad6ce6b10aa16f3 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:38:46 +0200 Subject: [PATCH 0406/1215] Upgrade to H2 2.2.222 Closes gh-37316 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c1cbe53e340a..33f5f5d99803 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -352,7 +352,7 @@ bom { ] } } - library("H2", "2.2.220") { + library("H2", "2.2.222") { group("com.h2database") { modules = [ "h2" From bb249bea4c28e62fbd3ca3ba941f3ef84b5f42aa Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:38:51 +0200 Subject: [PATCH 0407/1215] Upgrade to Hibernate 6.2.8.Final Closes gh-37317 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 33f5f5d99803..2a9bce65491c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -376,7 +376,7 @@ bom { ] } } - library("Hibernate", "6.2.7.Final") { + library("Hibernate", "6.2.8.Final") { group("org.hibernate.orm") { modules = [ "hibernate-agroal", From 20cb8712dbb340351dd9249b5788dcfe7994ad59 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:38:55 +0200 Subject: [PATCH 0408/1215] Upgrade to Infinispan 14.0.17.Final Closes gh-37318 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 2a9bce65491c..2dadb7e0ff50 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -464,7 +464,7 @@ bom { ] } } - library("Infinispan", "14.0.14.Final") { + library("Infinispan", "14.0.17.Final") { group("org.infinispan") { imports = [ "infinispan-bom" From 795e796eb6c4e459399ef9a6ccc5abd4df6a7922 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:38:59 +0200 Subject: [PATCH 0409/1215] Upgrade to Jedis 4.4.4 Closes gh-37319 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 2dadb7e0ff50..e8d78c368c61 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -656,7 +656,7 @@ bom { ] } } - library("Jedis", "4.4.3") { + library("Jedis", "4.4.4") { group("redis.clients") { modules = [ "jedis" From 037aba5f88bde214a6ee9f755f0476e342ca6ea9 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:39:04 +0200 Subject: [PATCH 0410/1215] Upgrade to MariaDB 3.2.0 Closes gh-37320 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e8d78c368c61..788531bcc4ed 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -862,7 +862,7 @@ bom { ] } } - library("MariaDB", "3.1.4") { + library("MariaDB", "3.2.0") { group("org.mariadb.jdbc") { modules = [ "mariadb-java-client" From 5dc6fcf3e541837cfbaeb8dd603147510f95b1f7 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:39:09 +0200 Subject: [PATCH 0411/1215] Upgrade to Maven Enforcer Plugin 3.4.1 Closes gh-37321 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 788531bcc4ed..d5e920c0a603 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -911,7 +911,7 @@ bom { ] } } - library("Maven Enforcer Plugin", "3.4.0") { + library("Maven Enforcer Plugin", "3.4.1") { group("org.apache.maven.plugins") { plugins = [ "maven-enforcer-plugin" From b89556eb80a50ecb2792635ece33aa464bdedf66 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:39:13 +0200 Subject: [PATCH 0412/1215] Upgrade to MSSQL JDBC 12.4.1.jre11 Closes gh-37322 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d5e920c0a603..486134680ebc 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1036,7 +1036,7 @@ bom { ] } } - library("MSSQL JDBC", "12.4.0.jre11") { + library("MSSQL JDBC", "12.4.1.jre11") { prohibit { endsWith([".jre8", "-preview"]) because "we use the non-preview .jre11 version" From b8eeb8d7f847d37035de5497511c6cc7c5537ca8 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:39:18 +0200 Subject: [PATCH 0413/1215] Upgrade to Native Build Tools Plugin 0.9.26 Closes gh-37323 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e9a6a13fef71..d75c9665131a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ org.gradle.parallel=true org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 kotlinVersion=1.9.10 -nativeBuildToolsVersion=0.9.24 +nativeBuildToolsVersion=0.9.26 springFrameworkVersion=6.1.0-SNAPSHOT tomcatVersion=10.1.12 From 078b399a93a72719f5de802af793e01e410ccdda Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:39:22 +0200 Subject: [PATCH 0414/1215] Upgrade to Neo4j Java Driver 5.12.0 Closes gh-37324 --- .../neo4j/Neo4jAutoConfigurationIntegrationTests.java | 4 ++-- .../boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java | 2 +- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationIntegrationTests.java index 966fc33637bd..e6cbbe3605eb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationIntegrationTests.java @@ -118,7 +118,7 @@ static class TestConfiguration { @Bean AuthTokenManager authTokenManager() { - return AuthTokenManagers.expirationBased(() -> AuthTokens.basic("neo4j", neo4jServer.getAdminPassword()) + return AuthTokenManagers.bearer(() -> AuthTokens.basic("neo4j", neo4jServer.getAdminPassword()) .expiringAt(System.currentTimeMillis() + 5_000)); } @@ -155,7 +155,7 @@ static class TestConfiguration { @Bean AuthTokenManager authTokenManager() { - return AuthTokenManagers.expirationBased(() -> AuthTokens.basic("wrongagain", "stillwrong") + return AuthTokenManagers.bearer(() -> AuthTokens.basic("wrongagain", "stillwrong") .expiringAt(System.currentTimeMillis() + 5_000)); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java index 68ca8ed3750f..1a05936d3c65 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/neo4j/Neo4jAutoConfigurationTests.java @@ -192,7 +192,7 @@ void authenticationWithAuthTokenManagerAndUsernameShouldProvideAuthTokenManger() authentication.setPassword("Urlaub"); authentication.setRealm("Test Realm"); assertThat(new PropertiesNeo4jConnectionDetails(properties, - AuthTokenManagers.expirationBased( + AuthTokenManagers.bearer( () -> AuthTokens.basic("username", "password").expiringAt(System.currentTimeMillis() + 5000))) .getAuthTokenManager()).isNotNull(); } diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 486134680ebc..326e5f761627 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1070,7 +1070,7 @@ bom { ] } } - library("Neo4j Java Driver", "5.11.0") { + library("Neo4j Java Driver", "5.12.0") { group("org.neo4j.driver") { modules = [ "neo4j-java-driver" From ef04925a60c6f51e86172d2315d63b84815c6eba Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:39:27 +0200 Subject: [PATCH 0415/1215] Upgrade to OpenTelemetry 1.30.0 Closes gh-37325 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 326e5f761627..346845a81ac9 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1091,7 +1091,7 @@ bom { ] } } - library("OpenTelemetry", "1.29.0") { + library("OpenTelemetry", "1.30.0") { group("io.opentelemetry") { imports = [ "opentelemetry-bom" From 5d05347e2c22fb4338c7232b496fa64ab98a7443 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:39:32 +0200 Subject: [PATCH 0416/1215] Upgrade to Pooled JMS 3.1.2 Closes gh-37326 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 346845a81ac9..c3285fd8f9e1 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1115,7 +1115,7 @@ bom { ] } } - library("Pooled JMS", "3.1.1") { + library("Pooled JMS", "3.1.2") { group("org.messaginghub") { modules = [ "pooled-jms" From 07922563f00e9ffad6d08d27e4bfa0dfba3a527d Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:39:36 +0200 Subject: [PATCH 0417/1215] Upgrade to REST Assured 5.3.2 Closes gh-37327 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c3285fd8f9e1..15a97d40aed6 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1335,7 +1335,7 @@ bom { ] } } - library("REST Assured", "5.3.1") { + library("REST Assured", "5.3.2") { group("io.rest-assured") { imports = [ "rest-assured-bom" From b7f77e055a7adcfff985784e6291c718263a1089 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:39:41 +0200 Subject: [PATCH 0418/1215] Upgrade to Selenium 4.12.1 Closes gh-37328 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 15a97d40aed6..7fdfdbe2adaa 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1448,7 +1448,7 @@ bom { ] } } - library("Selenium", "4.11.0") { + library("Selenium", "4.12.1") { group("org.seleniumhq.selenium") { imports = [ "selenium-bom" From 94ba36b8d08f02efa6fe2b7aa2bef01964377c04 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:39:46 +0200 Subject: [PATCH 0419/1215] Upgrade to Selenium HtmlUnit 4.12.0 Closes gh-37329 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 7fdfdbe2adaa..ca8c0062c6e8 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1455,7 +1455,7 @@ bom { ] } } - library("Selenium HtmlUnit", "4.11.0") { + library("Selenium HtmlUnit", "4.12.0") { group("org.seleniumhq.selenium") { modules = [ "htmlunit-driver" From 35733199ab8e76974d164a46fa9253ac4885d5b5 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:39:50 +0200 Subject: [PATCH 0420/1215] Upgrade to SLF4J 2.0.9 Closes gh-37330 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index ca8c0062c6e8..25308bee5fa8 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1469,7 +1469,7 @@ bom { ] } } - library("SLF4J", "2.0.7") { + library("SLF4J", "2.0.9") { group("org.slf4j") { modules = [ "jcl-over-slf4j", From 791e6dcc29324c60ed90377dbdc999f0a29e1e73 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:39:55 +0200 Subject: [PATCH 0421/1215] Upgrade to SnakeYAML 2.2 Closes gh-37331 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 25308bee5fa8..0959d6ec9efc 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1486,7 +1486,7 @@ bom { ] } } - library("SnakeYAML", "2.1") { + library("SnakeYAML", "2.2") { group("org.yaml") { modules = [ "snakeyaml" From 18c4401f07d67274420b66ac54fdeba4c42ab60a Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:39:59 +0200 Subject: [PATCH 0422/1215] Upgrade to SQLite JDBC 3.43.0.0 Closes gh-37332 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 0959d6ec9efc..f1e3ced83e6e 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1636,7 +1636,7 @@ bom { ] } } - library("SQLite JDBC", "3.42.0.0") { + library("SQLite JDBC", "3.43.0.0") { group("org.xerial") { modules = [ "sqlite-jdbc" From 22bc339cb72d825349cc0dad92523a79905a595a Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:40:04 +0200 Subject: [PATCH 0423/1215] Upgrade to Tomcat 10.1.13 Closes gh-37333 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d75c9665131a..59a5f9a7ef57 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,6 @@ org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 kotlinVersion=1.9.10 nativeBuildToolsVersion=0.9.26 springFrameworkVersion=6.1.0-SNAPSHOT -tomcatVersion=10.1.12 +tomcatVersion=10.1.13 kotlin.stdlib.default.dependency=false From 14a59a33dcef8918a5777c6bddb9c1857c0f8f41 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 11 Sep 2023 12:53:45 +0100 Subject: [PATCH 0424/1215] Test that GraphQL QueryDSL auto-config backs off without Query DSL See gh-34974 --- .../spring-boot-autoconfigure/build.gradle | 1 - .../data/GraphQlQuerydslAutoConfigurationTests.java | 9 +++++++++ .../GraphQlReactiveQuerydslAutoConfigurationTests.java | 9 +++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index 85a72c0600cd..5eaddf26aab9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -217,7 +217,6 @@ dependencies { testImplementation("com.ibm.db2:jcc") testImplementation("com.jayway.jsonpath:json-path") testImplementation("com.mysql:mysql-connector-j") - testImplementation("com.querydsl:querydsl-core") testImplementation("com.squareup.okhttp3:mockwebserver") testImplementation("com.sun.xml.messaging.saaj:saaj-impl") testImplementation("io.projectreactor:reactor-test") diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQuerydslAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQuerydslAutoConfigurationTests.java index 608733146946..ff2624099b2d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQuerydslAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlQuerydslAutoConfigurationTests.java @@ -23,6 +23,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.graphql.Book; import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -33,6 +34,7 @@ import org.springframework.graphql.test.tester.ExecutionGraphQlServiceTester; import org.springframework.graphql.test.tester.GraphQlTester; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -65,6 +67,13 @@ void shouldRegisterDataFetcherForQueryDslRepositories() { }); } + @Test + void shouldBackOffWithoutQueryDsl() { + this.contextRunner.withClassLoader(new FilteredClassLoader("com.querydsl.core")) + .run((context) -> assertThat(context).doesNotHaveBean("querydslRegistrar") + .doesNotHaveBean(GraphQlQuerydslAutoConfiguration.class)); + } + @Configuration(proxyBeanMethods = false) static class MockRepositoryConfig { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlReactiveQuerydslAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlReactiveQuerydslAutoConfigurationTests.java index 9901d096cb75..8c807fe98984 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlReactiveQuerydslAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/data/GraphQlReactiveQuerydslAutoConfigurationTests.java @@ -22,6 +22,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.graphql.Book; import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -32,6 +33,7 @@ import org.springframework.graphql.test.tester.ExecutionGraphQlServiceTester; import org.springframework.graphql.test.tester.GraphQlTester; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -64,6 +66,13 @@ void shouldRegisterDataFetcherForQueryDslRepositories() { }); } + @Test + void shouldBackOffWithoutQueryDsl() { + this.contextRunner.withClassLoader(new FilteredClassLoader("com.querydsl.core")) + .run((context) -> assertThat(context).doesNotHaveBean("querydslRegistrar") + .doesNotHaveBean(GraphQlReactiveQuerydslAutoConfiguration.class)); + } + @Configuration(proxyBeanMethods = false) static class MockRepositoryConfig { From 24eadd70ed35d3caa95bb9003b9d45007071c74d Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 14:25:11 +0200 Subject: [PATCH 0425/1215] Adapt to Spring Framework API change This commit adapts to API changes in Spring Framework, see spring-projects/spring-framework#31117 Previously, the "autowired" executable to use for a bean was always resolved, even if a custom code fragment didn't really need it. This is key for binding of immutable configuration properties as we use an instance supplier for it. This changes means that the workaround added in maintenance releases can be removed. See gh-37337 --- ...ustomizerBeanRegistrationAotProcessor.java | 5 +-- ...BeanFactoryInitializationAotProcessor.java | 36 ------------------- ...ropertiesBeanRegistrationAotProcessor.java | 10 ++++-- ...leEntriesBeanRegistrationAotProcessor.java | 15 +++++--- 4 files changed, 18 insertions(+), 48 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/ResourceProviderCustomizerBeanRegistrationAotProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/ResourceProviderCustomizerBeanRegistrationAotProcessor.java index 919cfd3f4e68..6bdcccecd377 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/ResourceProviderCustomizerBeanRegistrationAotProcessor.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway/ResourceProviderCustomizerBeanRegistrationAotProcessor.java @@ -16,8 +16,6 @@ package org.springframework.boot.autoconfigure.flyway; -import java.lang.reflect.Executable; - import javax.lang.model.element.Modifier; import org.springframework.aot.generate.GeneratedMethod; @@ -58,8 +56,7 @@ protected AotContribution(BeanRegistrationCodeFragments delegate, RegisteredBean @Override public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, Executable constructorOrFactoryMethod, - boolean allowDirectSupplierShortcut) { + BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) { GeneratedMethod generatedMethod = beanRegistrationCode.getMethods().add("getInstance", (method) -> { method.addJavadoc("Get the bean instance for '$L'.", this.registeredBean.getBeanName()); method.addModifiers(Modifier.PRIVATE, Modifier.STATIC); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanFactoryInitializationAotProcessor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanFactoryInitializationAotProcessor.java index 41efc8cd9174..3786fc3acad9 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanFactoryInitializationAotProcessor.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanFactoryInitializationAotProcessor.java @@ -16,18 +16,14 @@ package org.springframework.boot.context.properties; -import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.List; import org.springframework.aot.generate.GenerationContext; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; -import org.springframework.boot.context.properties.bind.BindConstructorProvider; import org.springframework.boot.context.properties.bind.BindMethod; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.BindableRuntimeHintsRegistrar; @@ -47,7 +43,6 @@ class ConfigurationPropertiesBeanFactoryInitializationAotProcessor implements Be @Override public ConfigurationPropertiesReflectionHintsContribution processAheadOfTime( ConfigurableListableBeanFactory beanFactory) { - beanFactory.addBeanPostProcessor(new BindConstructorAwareBeanPostProcessor(beanFactory)); String[] beanNames = beanFactory.getBeanNamesForAnnotation(ConfigurationProperties.class); List> bindables = new ArrayList<>(); for (String beanName : beanNames) { @@ -63,37 +58,6 @@ public ConfigurationPropertiesReflectionHintsContribution processAheadOfTime( return (!bindables.isEmpty()) ? new ConfigurationPropertiesReflectionHintsContribution(bindables) : null; } - /** - * {@link SmartInstantiationAwareBeanPostProcessor} implementation to work around - * framework's constructor resolver for immutable configuration properties. - *

    - * Constructor binding supports multiple constructors as long as one is identified as - * the candidate for binding. Unfortunately, framework is not aware of such feature - * and attempts to resolve the autowired constructor to use. - */ - static class BindConstructorAwareBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor { - - private final ConfigurableListableBeanFactory beanFactory; - - BindConstructorAwareBeanPostProcessor(ConfigurableListableBeanFactory beanFactory) { - this.beanFactory = beanFactory; - } - - @Override - public Constructor[] determineCandidateConstructors(Class beanClass, String beanName) - throws BeansException { - BindMethod bindMethod = BindMethodAttribute.get(this.beanFactory, beanName); - if (bindMethod != null && bindMethod == BindMethod.VALUE_OBJECT) { - Constructor bindConstructor = BindConstructorProvider.DEFAULT.getBindConstructor(beanClass, false); - if (bindConstructor != null) { - return new Constructor[] { bindConstructor }; - } - } - return null; - } - - } - static final class ConfigurationPropertiesReflectionHintsContribution implements BeanFactoryInitializationAotContribution { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrationAotProcessor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrationAotProcessor.java index 623626154b5c..5e96c1ae963f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrationAotProcessor.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanRegistrationAotProcessor.java @@ -16,7 +16,6 @@ package org.springframework.boot.context.properties; -import java.lang.reflect.Executable; import java.util.function.Predicate; import javax.lang.model.element.Modifier; @@ -34,6 +33,7 @@ import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.boot.context.properties.bind.BindMethod; +import org.springframework.javapoet.ClassName; import org.springframework.javapoet.CodeBlock; /** @@ -80,10 +80,14 @@ public CodeBlock generateSetBeanDefinitionPropertiesCode(GenerationContext gener beanDefinition, attributeFilter.or(BindMethodAttribute.NAME::equals)); } + @Override + public ClassName getTarget(RegisteredBean registeredBean) { + return ClassName.get(this.registeredBean.getBeanClass()); + } + @Override public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, Executable constructorOrFactoryMethod, - boolean allowDirectSupplierShortcut) { + BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) { GeneratedMethod generatedMethod = beanRegistrationCode.getMethods().add("getInstance", (method) -> { Class beanClass = this.registeredBean.getBeanClass(); method.addJavadoc("Get the bean instance for '$L'.", this.registeredBean.getBeanName()) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinModuleEntriesBeanRegistrationAotProcessor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinModuleEntriesBeanRegistrationAotProcessor.java index 980928d762ee..93749dd160a2 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinModuleEntriesBeanRegistrationAotProcessor.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonMixinModuleEntriesBeanRegistrationAotProcessor.java @@ -16,7 +16,6 @@ package org.springframework.boot.jackson; -import java.lang.reflect.Executable; import java.util.LinkedHashSet; import java.util.Set; @@ -33,6 +32,7 @@ import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments; import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsDecorator; import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.javapoet.ClassName; import org.springframework.javapoet.CodeBlock; /** @@ -54,6 +54,8 @@ public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registe static class AotContribution extends BeanRegistrationCodeFragmentsDecorator { + private static final Class BEAN_TYPE = JsonMixinModuleEntries.class; + private final RegisteredBean registeredBean; private final ClassLoader classLoader; @@ -64,18 +66,21 @@ static class AotContribution extends BeanRegistrationCodeFragmentsDecorator { this.classLoader = registeredBean.getBeanFactory().getBeanClassLoader(); } + @Override + public ClassName getTarget(RegisteredBean registeredBean) { + return ClassName.get(BEAN_TYPE); + } + @Override public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, - BeanRegistrationCode beanRegistrationCode, Executable constructorOrFactoryMethod, - boolean allowDirectSupplierShortcut) { + BeanRegistrationCode beanRegistrationCode, boolean allowDirectSupplierShortcut) { JsonMixinModuleEntries entries = this.registeredBean.getBeanFactory() .getBean(this.registeredBean.getBeanName(), JsonMixinModuleEntries.class); contributeHints(generationContext.getRuntimeHints(), entries); GeneratedMethod generatedMethod = beanRegistrationCode.getMethods().add("getInstance", (method) -> { - Class beanType = JsonMixinModuleEntries.class; method.addJavadoc("Get the bean instance for '$L'.", this.registeredBean.getBeanName()); method.addModifiers(Modifier.PRIVATE, Modifier.STATIC); - method.returns(beanType); + method.returns(BEAN_TYPE); CodeBlock.Builder code = CodeBlock.builder(); code.add("return $T.create(", JsonMixinModuleEntries.class).beginControlFlow("(mixins) ->"); entries.doWithEntry(this.classLoader, (type, mixin) -> addEntryCode(code, type, mixin)); From a1c2ca7f3b924e7d5af342f24a89430968552df5 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Mon, 11 Sep 2023 16:29:30 +0200 Subject: [PATCH 0426/1215] Upgrade to Jetty Reactive HTTPClient 4.0.0 Closes gh-37339 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f1e3ced83e6e..8063b185a1e4 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -670,7 +670,7 @@ bom { ] } } - library("Jetty Reactive HTTPClient", "3.0.8") { + library("Jetty Reactive HTTPClient", "4.0.0") { group("org.eclipse.jetty") { modules = [ "jetty-reactive-httpclient" From c8d036eaa8b61d8eac85ffefd7afa297455c67dd Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 12 Sep 2023 10:59:25 +0200 Subject: [PATCH 0427/1215] Remove ServerHttpObservationFilter from WebFlux This commit removes the auto-configuration of the `ServerHttpObservationFilter` bean for WebFlux applications as it's been deprecated by Spring Framework. The Observability instrumentation is now handled at the `WebHttpHandlerBuilder` in Framework directly and doesn't need any auto-configuration from Spring Boot. Closes gh-37344 --- .../WebFluxObservationAutoConfiguration.java | 57 +++-------- ...FluxObservationAutoConfigurationTests.java | 96 +++---------------- .../src/docs/asciidoc/web/reactive.adoc | 3 - 3 files changed, 23 insertions(+), 133 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java index 91b1d6fa95aa..0ccb94d55b5c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java @@ -21,9 +21,6 @@ import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties; import org.springframework.boot.actuate.autoconfigure.metrics.OnlyOnceLoggingDenyMeterFilter; import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; @@ -33,16 +30,10 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; -import org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention; -import org.springframework.http.server.reactive.observation.ServerRequestObservationConvention; -import org.springframework.web.filter.reactive.ServerHttpObservationFilter; /** * {@link EnableAutoConfiguration Auto-configuration} for instrumentation of Spring @@ -53,48 +44,22 @@ * @author Dmytro Nosan * @since 3.0.0 */ -@AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, - SimpleMetricsExportAutoConfiguration.class, ObservationAutoConfiguration.class }) -@ConditionalOnClass(Observation.class) -@ConditionalOnBean(ObservationRegistry.class) +@AutoConfiguration(after = { SimpleMetricsExportAutoConfiguration.class, ObservationAutoConfiguration.class }) +@ConditionalOnClass({ Observation.class, MeterRegistry.class }) +@ConditionalOnBean({ ObservationRegistry.class, MeterRegistry.class }) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) @EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class }) -@SuppressWarnings("removal") public class WebFluxObservationAutoConfiguration { - private final ObservationProperties observationProperties; - - public WebFluxObservationAutoConfiguration(ObservationProperties observationProperties) { - this.observationProperties = observationProperties; - } - @Bean - @ConditionalOnMissingBean(ServerHttpObservationFilter.class) - @Order(Ordered.HIGHEST_PRECEDENCE + 1) - public ServerHttpObservationFilter webfluxObservationFilter(ObservationRegistry registry, - ObjectProvider customConvention) { - String name = this.observationProperties.getHttp().getServer().getRequests().getName(); - ServerRequestObservationConvention convention = customConvention - .getIfAvailable(() -> new DefaultServerRequestObservationConvention(name)); - return new ServerHttpObservationFilter(registry, convention); - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(MeterRegistry.class) - @ConditionalOnBean(MeterRegistry.class) - static class MeterFilterConfiguration { - - @Bean - @Order(0) - MeterFilter metricsHttpServerUriTagFilter(MetricsProperties metricsProperties, - ObservationProperties observationProperties) { - String name = observationProperties.getHttp().getServer().getRequests().getName(); - MeterFilter filter = new OnlyOnceLoggingDenyMeterFilter( - () -> "Reached the maximum number of URI tags for '%s'.".formatted(name)); - return MeterFilter.maximumAllowableTags(name, "uri", metricsProperties.getWeb().getServer().getMaxUriTags(), - filter); - } - + @Order(0) + MeterFilter metricsHttpServerUriTagFilter(MetricsProperties metricsProperties, + ObservationProperties observationProperties) { + String name = observationProperties.getHttp().getServer().getRequests().getName(); + MeterFilter filter = new OnlyOnceLoggingDenyMeterFilter( + () -> "Reached the maximum number of URI tags for '%s'.".formatted(name)); + return MeterFilter.maximumAllowableTags(name, "uri", metricsProperties.getWeb().getServer().getMaxUriTags(), + filter); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java index 48c065660fa0..b5e05e99ea0a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java @@ -16,12 +16,12 @@ package org.springframework.boot.actuate.autoconfigure.observation.web.reactive; -import java.util.List; +import java.time.Duration; +import java.time.temporal.ChronoUnit; import io.micrometer.core.instrument.MeterRegistry; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import reactor.core.publisher.Mono; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; @@ -33,16 +33,6 @@ import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention; -import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.filter.reactive.ServerHttpObservationFilter; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.WebFilter; -import org.springframework.web.server.WebFilterChain; import static org.assertj.core.api.Assertions.assertThat; @@ -54,7 +44,6 @@ * @author Madhura Bhave */ @ExtendWith(OutputCaptureExtension.class) -@SuppressWarnings("removal") class WebFluxObservationAutoConfigurationTests { private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() @@ -62,31 +51,6 @@ class WebFluxObservationAutoConfigurationTests { .withConfiguration( AutoConfigurations.of(ObservationAutoConfiguration.class, WebFluxObservationAutoConfiguration.class)); - @Test - void shouldProvideWebFluxObservationFilter() { - this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ServerHttpObservationFilter.class)); - } - - @Test - void shouldProvideWebFluxObservationFilterOrdered() { - this.contextRunner.withBean(FirstWebFilter.class).withBean(ThirdWebFilter.class).run((context) -> { - List webFilters = context.getBeanProvider(WebFilter.class).orderedStream().toList(); - assertThat(webFilters.get(0)).isInstanceOf(FirstWebFilter.class); - assertThat(webFilters.get(1)).isInstanceOf(ServerHttpObservationFilter.class); - assertThat(webFilters.get(2)).isInstanceOf(ThirdWebFilter.class); - }); - } - - @Test - void shouldUseCustomConventionWhenAvailable() { - this.contextRunner.withUserConfiguration(CustomConventionConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(ServerHttpObservationFilter.class); - assertThat(context).getBean(ServerHttpObservationFilter.class) - .extracting("observationConvention") - .isInstanceOf(CustomConvention.class); - }); - } - @Test void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) { this.contextRunner.withUserConfiguration(TestController.class) @@ -108,7 +72,7 @@ void afterMaxUrisReachedFurtherUrisAreDeniedWhenUsingCustomObservationName(Captu .withPropertyValues("management.metrics.web.server.max-uri-tags=2", "management.observations.http.server.requests.name=my.http.server.requests") .run((context) -> { - MeterRegistry registry = getInitializedMeterRegistry(context); + MeterRegistry registry = getInitializedMeterRegistry(context, "my.http.server.requests"); assertThat(registry.get("my.http.server.requests").meters()).hasSizeLessThanOrEqualTo(2); assertThat(output).contains("Reached the maximum number of URI tags for 'my.http.server.requests'"); }); @@ -127,53 +91,17 @@ void shouldNotDenyNorLogIfMaxUrisIsNotReached(CapturedOutput output) { }); } - private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicationContext context) - throws Exception { - return getInitializedMeterRegistry(context, "/test0", "/test1", "/test2"); + private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicationContext context) { + return getInitializedMeterRegistry(context, "http.server.requests"); } - private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicationContext context, String... urls) - throws Exception { - assertThat(context).hasSingleBean(ServerHttpObservationFilter.class); - WebTestClient client = WebTestClient.bindToApplicationContext(context).build(); - for (String url : urls) { - client.get().uri(url).exchange().expectStatus().isOk(); - } - return context.getBean(MeterRegistry.class); - } - - @Configuration(proxyBeanMethods = false) - static class CustomConventionConfiguration { - - @Bean - CustomConvention customConvention() { - return new CustomConvention(); - } - - } - - static class CustomConvention extends DefaultServerRequestObservationConvention { - - } - - @Order(Ordered.HIGHEST_PRECEDENCE) - static class FirstWebFilter implements WebFilter { - - @Override - public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { - return chain.filter(exchange); - } - - } - - @Order(Ordered.HIGHEST_PRECEDENCE + 2) - static class ThirdWebFilter implements WebFilter { - - @Override - public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { - return chain.filter(exchange); - } - + private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicationContext context, + String metricName) { + MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); + meterRegistry.timer(metricName, "uri", "/test0").record(Duration.of(500, ChronoUnit.SECONDS)); + meterRegistry.timer(metricName, "uri", "/test1").record(Duration.of(500, ChronoUnit.SECONDS)); + meterRegistry.timer(metricName, "uri", "/test2").record(Duration.of(500, ChronoUnit.SECONDS)); + return meterRegistry; } } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc index fbae93af9cfd..21c41f0c5d42 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc @@ -245,9 +245,6 @@ When it does so, the orders shown in the following table will be used: |=== | Web Filter | Order -| `ServerHttpObservationFilter` (Micrometer Observability) -| `Ordered.HIGHEST_PRECEDENCE + 1` - | `WebFilterChainProxy` (Spring Security) | `-100` From f2bf49ec35930c253c12c51a5d78a4492abcaa40 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 12 Sep 2023 11:43:03 +0200 Subject: [PATCH 0428/1215] Upgrade to Micrometer 1.12.0-M3 Closes gh-37226 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 8063b185a1e4..2d2484c131f6 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -995,7 +995,7 @@ bom { ] } } - library("Micrometer", "1.12.0-SNAPSHOT") { + library("Micrometer", "1.12.0-M3") { considerSnapshots() group("io.micrometer") { modules = [ From 0587fb5331146217ea8b2c4b274df99c15c4d9b4 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 12 Sep 2023 11:43:08 +0200 Subject: [PATCH 0429/1215] Upgrade to Micrometer Tracing 1.2.0-M3 Closes gh-37346 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 2d2484c131f6..8b2e91932b22 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1008,7 +1008,7 @@ bom { ] } } - library("Micrometer Tracing", "1.2.0-M2") { + library("Micrometer Tracing", "1.2.0-M3") { considerSnapshots() calendarName = "Tracing" group("io.micrometer") { From 2d1fcab61197880bcf11e29ae01870654c5a6081 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 12 Sep 2023 11:43:14 +0200 Subject: [PATCH 0430/1215] Upgrade to OpenTelemetry 1.30.1 Closes gh-37347 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 8b2e91932b22..c58e6213f2f2 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1091,7 +1091,7 @@ bom { ] } } - library("OpenTelemetry", "1.30.0") { + library("OpenTelemetry", "1.30.1") { group("io.opentelemetry") { imports = [ "opentelemetry-bom" From 626d858d81dc0e7b6c67cc9a69cffaa18c81ce71 Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Tue, 12 Sep 2023 11:49:50 +0200 Subject: [PATCH 0431/1215] Update Batch tests Related to: https://github.com/spring-projects/spring-batch/issues/4245 Closes gh-37348 --- .../batch/BatchAutoConfigurationTests.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java index 7253b6e6679d..9d508642d239 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java @@ -39,7 +39,6 @@ import org.springframework.batch.core.configuration.JobRegistry; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration; -import org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor; import org.springframework.batch.core.explore.JobExplorer; import org.springframework.batch.core.job.AbstractJob; import org.springframework.batch.core.launch.JobLauncher; @@ -96,6 +95,7 @@ * @author Stephane Nicoll * @author Vedran Pavic * @author Kazuki Shimizu + * @author Mahmoud Ben Hassine */ @ExtendWith(OutputCaptureExtension.class) class BatchAutoConfigurationTests { @@ -515,13 +515,6 @@ static class NamedJobConfigurationWithRegisteredAndLocalJob { @Autowired private JobRepository jobRepository; - @Bean - static JobRegistryBeanPostProcessor registryProcessor(JobRegistry jobRegistry) { - JobRegistryBeanPostProcessor processor = new JobRegistryBeanPostProcessor(); - processor.setJobRegistry(jobRegistry); - return processor; - } - @Bean Job discreteJob() { AbstractJob job = new AbstractJob("discreteRegisteredJob") { @@ -685,7 +678,17 @@ protected void doExecute(JobExecution execution) { @Bean Job job2() { - return mock(Job.class); + return new Job() { + @Override + public String getName() { + return "discreteLocalJob2"; + } + + @Override + public void execute(JobExecution execution) { + execution.setStatus(BatchStatus.COMPLETED); + } + }; } } From 8874cadebf6781adde806fad3b3a402d2e00ea51 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 12 Sep 2023 08:19:47 +0100 Subject: [PATCH 0432/1215] Revert workaround for gh-18440 This reverts the main code changes from commit b240c810a810c873e84d058614b5d0174591cff9. The tests are kept to verify that the workaround is no longer required. Closes gh-18591 --- .../ConfigurationPropertiesBean.java | 29 +------------------ 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBean.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBean.java index 186184b7c8b1..3fa38d84391b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBean.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBean.java @@ -23,7 +23,6 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -42,8 +41,6 @@ import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.util.ReflectionUtils; import org.springframework.validation.annotation.Validated; /** @@ -237,36 +234,12 @@ private static Method findFactoryMethod(ConfigurableListableBeanFactory beanFact if (beanFactory.containsBeanDefinition(beanName)) { BeanDefinition beanDefinition = beanFactory.getMergedBeanDefinition(beanName); if (beanDefinition instanceof RootBeanDefinition rootBeanDefinition) { - Method resolvedFactoryMethod = rootBeanDefinition.getResolvedFactoryMethod(); - if (resolvedFactoryMethod != null) { - return resolvedFactoryMethod; - } + return rootBeanDefinition.getResolvedFactoryMethod(); } - return findFactoryMethodUsingReflection(beanFactory, beanDefinition); } return null; } - private static Method findFactoryMethodUsingReflection(ConfigurableListableBeanFactory beanFactory, - BeanDefinition beanDefinition) { - String factoryMethodName = beanDefinition.getFactoryMethodName(); - String factoryBeanName = beanDefinition.getFactoryBeanName(); - if (factoryMethodName == null || factoryBeanName == null) { - return null; - } - Class factoryType = beanFactory.getType(factoryBeanName); - if (factoryType.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)) { - factoryType = factoryType.getSuperclass(); - } - AtomicReference factoryMethod = new AtomicReference<>(); - ReflectionUtils.doWithMethods(factoryType, (method) -> { - if (method.getName().equals(factoryMethodName)) { - factoryMethod.set(method); - } - }); - return factoryMethod.get(); - } - static ConfigurationPropertiesBean forValueObject(Class beanType, String beanName) { Bindable bindTarget = createBindTarget(null, beanType, null); Assert.state(bindTarget != null && deduceBindMethod(bindTarget) == VALUE_OBJECT_BIND_METHOD, From 4ebee17cb185eaa50583361ffb8c6801f207a399 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 12 Sep 2023 12:36:05 +0100 Subject: [PATCH 0433/1215] Suppress warnings until more to new semconv module OTel has deprecated their semconv module and introduced a new module with different Maven coordinates. micrometer-metrics/tracing#343 will move Micrometer Tracing to the new module. Until then, we need to suppress the deprecation warnings that result from using the old one. Closes gh-37347 --- .../opentelemetry/OpenTelemetryAutoConfiguration.java | 5 +++-- .../OpenTelemetryAutoConfigurationTests.java | 11 ++++++----- .../tracing/OpenTelemetryAutoConfigurationTests.java | 6 ++++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java index 8a21539df875..be533a86892d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java @@ -28,7 +28,6 @@ import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.resources.ResourceBuilder; import io.opentelemetry.sdk.trace.SdkTracerProvider; -import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -70,10 +69,12 @@ OpenTelemetrySdk openTelemetry(ObjectProvider tracerProvider, @Bean @ConditionalOnMissingBean + @SuppressWarnings("deprecation") Resource openTelemetryResource(Environment environment, OpenTelemetryProperties properties) { String applicationName = environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME); return Resource.getDefault() - .merge(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, applicationName))) + .merge(Resource.create(Attributes + .of(io.opentelemetry.semconv.resource.attributes.ResourceAttributes.SERVICE_NAME, applicationName))) .merge(toResource(properties)); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java index e6fbbcf5dd3c..6a7570563180 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java @@ -24,7 +24,6 @@ import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; -import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -82,20 +81,22 @@ void backsOffOnUserSuppliedBeans() { } @Test + @SuppressWarnings("deprecation") void shouldApplySpringApplicationNameToResource() { this.runner.withPropertyValues("spring.application.name=my-application").run((context) -> { Resource resource = context.getBean(Resource.class); - assertThat(resource.getAttributes().asMap()) - .contains(entry(ResourceAttributes.SERVICE_NAME, "my-application")); + assertThat(resource.getAttributes().asMap()).contains(entry( + io.opentelemetry.semconv.resource.attributes.ResourceAttributes.SERVICE_NAME, "my-application")); }); } @Test + @SuppressWarnings("deprecation") void shouldFallbackToDefaultApplicationNameIfSpringApplicationNameIsNotSet() { this.runner.run((context) -> { Resource resource = context.getBean(Resource.class); - assertThat(resource.getAttributes().asMap()) - .contains(entry(ResourceAttributes.SERVICE_NAME, "application")); + assertThat(resource.getAttributes().asMap()).contains( + entry(io.opentelemetry.semconv.resource.attributes.ResourceAttributes.SERVICE_NAME, "application")); }); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java index 4af8830cffc5..2d02df024c92 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java @@ -50,7 +50,6 @@ import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.export.SpanExporter; import io.opentelemetry.sdk.trace.samplers.Sampler; -import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -170,6 +169,7 @@ void shouldBackOffOnCustomBeans() { } @Test + @SuppressWarnings("deprecation") void shouldSetupDefaultResourceAttributes() { this.contextRunner .withConfiguration( @@ -182,7 +182,9 @@ void shouldSetupDefaultResourceAttributes() { exporter.await(Duration.ofSeconds(10)); SpanData spanData = exporter.getExportedSpans().get(0); Map, Object> expectedAttributes = Resource.getDefault() - .merge(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, "application"))) + .merge(Resource.create( + Attributes.of(io.opentelemetry.semconv.resource.attributes.ResourceAttributes.SERVICE_NAME, + "application"))) .getAttributes() .asMap(); assertThat(spanData.getResource().getAttributes().asMap()).isEqualTo(expectedAttributes); From 1bda578327c28f09b4b26b8a7fe40a96835f2f59 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 12 Sep 2023 12:09:31 +0200 Subject: [PATCH 0434/1215] Start building against Spring Data Bom 2023.1.0 snapshots See gh-37351 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e1dc4380389a..a4a5ff2b8463 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1517,7 +1517,7 @@ bom { ] } } - library("Spring Data Bom", "2023.1.0-M2") { + library("Spring Data Bom", "2023.1.0-SNAPSHOT") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From a9f26e0f95bf6f08e8f01d67efe8f0c446fdadbd Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Tue, 12 Sep 2023 17:00:59 -0500 Subject: [PATCH 0435/1215] Conditionally enable image building bind cache tests Accessing bind mount directories as is done in the tests for building images with bind mount caches requires Docker configuration when using Docker Desktop. It works without configuration on Linux with Docker Engine. See gh-28387 --- .../gradle/tasks/bundling/BootBuildImageIntegrationTests.java | 3 +++ .../java/org/springframework/boot/maven/BuildImageTests.java | 3 +++ 2 files changed, 6 insertions(+) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java index 8e3f23eb2c6d..06bc731d634b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java @@ -37,6 +37,7 @@ import org.gradle.testkit.runner.BuildResult; import org.gradle.testkit.runner.TaskOutcome; import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; import org.springframework.boot.buildpack.platform.docker.DockerApi; @@ -299,6 +300,8 @@ void buildsImageWithVolumeCaches() throws IOException { } @TestTemplate + @EnabledOnOs(value = OS.LINUX, disabledReason = "Works with Docker Engine on Linux but is not reliable with " + + "Docker Desktop on other OSs") void buildsImageWithBindCaches() throws IOException { writeMainClass(); writeLongNameResource(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java index 8ea336c6a1de..a729404432a5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java @@ -26,6 +26,7 @@ import java.util.stream.IntStream; import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.extension.ExtendWith; @@ -400,6 +401,8 @@ void whenBuildImageIsInvokedWithVolumeCaches(MavenBuild mavenBuild) { } @TestTemplate + @EnabledOnOs(value = OS.LINUX, disabledReason = "Works with Docker Engine on Linux but is not reliable with " + + "Docker Desktop on other OSs") void whenBuildImageIsInvokedWithBindCaches(MavenBuild mavenBuild) { String testBuildId = randomString(); mavenBuild.project("build-image-bind-caches") From c2b78830fff77ceff1566e353b1d6f02a61f6b40 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Mon, 11 Sep 2023 23:51:23 +0900 Subject: [PATCH 0436/1215] Polish See gh-37340 --- .../spring-boot-actuator-autoconfigure/build.gradle | 2 +- .../signalfx/SignalFxPropertiesConfigAdapterTests.java | 4 ++-- .../observation/ObservationHandlerGroupingTests.java | 2 +- .../autoconfigure/amqp/RabbitAutoConfigurationTests.java | 3 +-- .../jdbc/JdbcClientAutoConfigurationTests.java | 6 ++++-- .../jdbc/JdbcTemplateAutoConfigurationTests.java | 4 ++-- .../liquibase/LiquibaseAutoConfigurationTests.java | 2 +- .../orm/jpa/HibernateJpaAutoConfigurationTests.java | 2 +- .../web/reactive/WebFluxAutoConfigurationTests.java | 3 +-- .../spring-boot-docs/src/docs/asciidoc/data/sql.adoc | 2 +- .../java/smoketest/jetty/SampleJettyApplicationTests.java | 2 +- 11 files changed, 16 insertions(+), 16 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index 6992d4185092..a55874e629f6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -73,8 +73,8 @@ dependencies { optional("io.opentelemetry:opentelemetry-exporter-otlp") optional("io.projectreactor.netty:reactor-netty-http") optional("io.r2dbc:r2dbc-pool") - optional("io.r2dbc:r2dbc-spi") optional("io.r2dbc:r2dbc-proxy") + optional("io.r2dbc:r2dbc-spi") optional("jakarta.jms:jakarta.jms-api") optional("jakarta.persistence:jakarta.persistence-api") optional("jakarta.servlet:jakarta.servlet-api") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/signalfx/SignalFxPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/signalfx/SignalFxPropertiesConfigAdapterTests.java index 4fa086069d04..d664a342148a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/signalfx/SignalFxPropertiesConfigAdapterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/signalfx/SignalFxPropertiesConfigAdapterTests.java @@ -68,7 +68,7 @@ void whenPropertiesSourceIsSetAdapterSourceReturnsIt() { } @Test - void whenPropertiesPublishHistogramTypeIsCumulativePublishCumulativeHistogramReturnsIt() { + void whenPropertiesPublishHistogramTypeIsCumulativeAdapterPublishCumulativeHistogramReturnsIt() { SignalFxProperties properties = createProperties(); properties.setPublishedHistogramType(HistogramType.CUMULATIVE); assertThat(createConfigAdapter(properties).publishCumulativeHistogram()).isTrue(); @@ -76,7 +76,7 @@ void whenPropertiesPublishHistogramTypeIsCumulativePublishCumulativeHistogramRet } @Test - void whenPropertiesPublishHistogramTypeIsDeltaPublishDeltaHistogramReturnsIt() { + void whenPropertiesPublishHistogramTypeIsDeltaAdapterPublishDeltaHistogramReturnsIt() { SignalFxProperties properties = createProperties(); properties.setPublishedHistogramType(HistogramType.DELTA); assertThat(createConfigAdapter(properties).publishDeltaHistogram()).isTrue(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationHandlerGroupingTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationHandlerGroupingTests.java index 62ac14d092b1..b42d0a155c81 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationHandlerGroupingTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationHandlerGroupingTests.java @@ -38,7 +38,7 @@ class ObservationHandlerGroupingTests { @Test - void shouldGroupCategoriesIntoFirstMatchingHandlerAndRespectsCategoryOrder() { + void shouldGroupCategoriesIntoFirstMatchingHandlerAndRespectCategoryOrder() { ObservationHandlerGrouping grouping = new ObservationHandlerGrouping( List.of(ObservationHandlerA.class, ObservationHandlerB.class)); ObservationConfig config = new ObservationConfig(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java index 1d54051d7305..e9a9b69d94d3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java @@ -541,8 +541,7 @@ void shouldConfigureVirtualThreads() { this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> { SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory = context .getBean("rabbitListenerContainerFactory", SimpleRabbitListenerContainerFactory.class); - Object executor = ReflectionTestUtils.getField(rabbitListenerContainerFactory, "taskExecutor"); - assertThat(executor).as("rabbitListenerContainerFactory.taskExecutor") + assertThat(rabbitListenerContainerFactory).extracting("taskExecutor") .isInstanceOf(VirtualThreadTaskExecutor.class); }); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfigurationTests.java index 0430b4f8f747..03222cfbeb86 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfigurationTests.java @@ -43,7 +43,9 @@ class JdbcClientAutoConfigurationTests { @Test void jdbcClientWhenNoAvailableJdbcTemplateIsNotCreated() { - new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(DataSourceAutoConfiguration.class, JdbcClientAutoConfiguration.class)) .run((context) -> assertThat(context).doesNotHaveBean(JdbcClient.class)); } @@ -79,7 +81,7 @@ void jdbcClientIsOrderedAfterFlywayMigration() { @Test void jdbcClientIsOrderedAfterLiquibaseMigration() { this.contextRunner.withUserConfiguration(JdbcClientDataSourceMigrationValidator.class) - .withPropertyValues("spring.liquibase.changeLog:classpath:db/changelog/db.changelog-city.yaml") + .withPropertyValues("spring.liquibase.change-log:classpath:db/changelog/db.changelog-city.yaml") .withConfiguration(AutoConfigurations.of(LiquibaseAutoConfiguration.class)) .run((context) -> { assertThat(context).hasNotFailed().hasSingleBean(JdbcClient.class); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcTemplateAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcTemplateAutoConfigurationTests.java index e9424bd2bd36..7154f618dd49 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcTemplateAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcTemplateAutoConfigurationTests.java @@ -184,7 +184,7 @@ void testDependencyToFlywayWithJdbcTemplateMixed() { @Test void testDependencyToLiquibase() { this.contextRunner.withUserConfiguration(DataSourceMigrationValidator.class) - .withPropertyValues("spring.liquibase.changeLog:classpath:db/changelog/db.changelog-city.yaml") + .withPropertyValues("spring.liquibase.change-log:classpath:db/changelog/db.changelog-city.yaml") .withConfiguration(AutoConfigurations.of(LiquibaseAutoConfiguration.class)) .run((context) -> { assertThat(context).hasNotFailed(); @@ -195,7 +195,7 @@ void testDependencyToLiquibase() { @Test void testDependencyToLiquibaseWithJdbcTemplateMixed() { this.contextRunner.withUserConfiguration(NamedParameterDataSourceMigrationValidator.class) - .withPropertyValues("spring.liquibase.changeLog:classpath:db/changelog/db.changelog-city.yaml") + .withPropertyValues("spring.liquibase.change-log:classpath:db/changelog/db.changelog-city.yaml") .withConfiguration(AutoConfigurations.of(LiquibaseAutoConfiguration.class)) .run((context) -> { assertThat(context).hasNotFailed(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java index 3c66a6009707..7d1f68526f4b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java @@ -396,7 +396,7 @@ void testOverrideParameters() { void rollbackFile(@TempDir Path temp) throws IOException { File file = Files.createTempFile(temp, "rollback-file", "sql").toFile(); this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.liquibase.rollbackFile:" + file.getAbsolutePath()) + .withPropertyValues("spring.liquibase.rollback-file:" + file.getAbsolutePath()) .run((context) -> { SpringLiquibase liquibase = context.getBean(SpringLiquibase.class); File actualFile = (File) ReflectionTestUtils.getField(liquibase, "rollbackFile"); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java index d1ddd7e00972..100f98b045db 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java @@ -153,7 +153,7 @@ void testFlywayPlusValidation() { @Test void testLiquibasePlusValidation() { contextRunner() - .withPropertyValues("spring.liquibase.changeLog:classpath:db/changelog/db.changelog-city.yaml", + .withPropertyValues("spring.liquibase.change-log:classpath:db/changelog/db.changelog-city.yaml", "spring.jpa.hibernate.ddl-auto:validate") .withConfiguration(AutoConfigurations.of(LiquibaseAutoConfiguration.class)) .run((context) -> assertThat(context).hasNotFailed()); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java index b1753c4142d5..cf40c58a52c9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java @@ -1032,8 +1032,7 @@ static class CustomExceptionHandler extends ResponseEntityExceptionHandler { } @Configuration(proxyBeanMethods = false) - @Import({ OrderedControllerAdviceBeansConfiguration.LowestOrderedControllerAdvice.class, - OrderedControllerAdviceBeansConfiguration.HighestOrderedControllerAdvice.class }) + @Import({ LowestOrderedControllerAdvice.class, HighestOrderedControllerAdvice.class }) static class OrderedControllerAdviceBeansConfiguration { @ControllerAdvice diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/sql.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/sql.adoc index 18af81398a2d..bc59650c58cd 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/sql.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/sql.adoc @@ -183,7 +183,7 @@ You can inject it directly in your own beans as well, as shown in the following include::code:MyBean[] -If you rely on auto-configuration to create the underlying `JdbcTemplate`, any customization using `spring.jdbc.template.*` properties are taken into account in the client as well. +If you rely on auto-configuration to create the underlying `JdbcTemplate`, any customization using `spring.jdbc.template.*` properties is taken into account in the client as well. diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/test/java/smoketest/jetty/SampleJettyApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/test/java/smoketest/jetty/SampleJettyApplicationTests.java index d72e2c2d7fc8..f1fb7c1cb8b2 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/test/java/smoketest/jetty/SampleJettyApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-jetty/src/test/java/smoketest/jetty/SampleJettyApplicationTests.java @@ -65,7 +65,7 @@ void testCompression() { ResponseEntity entity = this.restTemplate.getForEntity("/", String.class); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(entity.getBody()).isEqualTo("Hello World"); - // Jetty HttpClient decodes gzip reponses automatically and removes the + // Jetty HttpClient decodes gzip responses automatically and removes the // Content-Encoding header. We have to assume that the response was gzipped. } From 05b87c5fe8cf0caa4474d7c73d091adfc9e7b737 Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Fri, 8 Sep 2023 10:14:18 +0800 Subject: [PATCH 0437/1215] Introduce configuration property for strict servlet compliance The property is named spring.servlet.multipart.strict-servlet-compliance See gh-37242 --- .../web/servlet/MultipartAutoConfiguration.java | 4 +++- .../web/servlet/MultipartProperties.java | 17 ++++++++++++++++- .../MultipartAutoConfigurationTests.java | 12 ++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/MultipartAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/MultipartAutoConfiguration.java index f91fa1ec8ac2..9bac3910ac20 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/MultipartAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/MultipartAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -46,6 +46,7 @@ * @author Greg Turnquist * @author Josh Long * @author Toshiaki Maki + * @author Yanming Zhou * @since 2.0.0 */ @AutoConfiguration @@ -72,6 +73,7 @@ public MultipartConfigElement multipartConfigElement() { public StandardServletMultipartResolver multipartResolver() { StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver(); multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily()); + multipartResolver.setStrictServletCompliance(this.multipartProperties.isStrictServletCompliance()); return multipartResolver; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/MultipartProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/MultipartProperties.java index 1388ae73bbb1..4db7ec2e1c25 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/MultipartProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/MultipartProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -43,6 +43,7 @@ * @author Josh Long * @author Toshiaki Maki * @author Stephane Nicoll + * @author Yanming Zhou * @since 2.0.0 */ @ConfigurationProperties(prefix = "spring.servlet.multipart", ignoreUnknownFields = false) @@ -79,6 +80,12 @@ public class MultipartProperties { */ private boolean resolveLazily = false; + /** + * Whether to resolve the multipart request strictly comply with the Servlet specification, + * only kicking in for "multipart/form-data" requests. + */ + private boolean strictServletCompliance = false; + public boolean getEnabled() { return this.enabled; } @@ -127,6 +134,14 @@ public void setResolveLazily(boolean resolveLazily) { this.resolveLazily = resolveLazily; } + public boolean isStrictServletCompliance() { + return this.strictServletCompliance; + } + + public void setStrictServletCompliance(boolean strictServletCompliance) { + this.strictServletCompliance = strictServletCompliance; + } + /** * Create a new {@link MultipartConfigElement} using the properties. * @return a new {@link MultipartConfigElement} configured using there properties diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/MultipartAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/MultipartAutoConfigurationTests.java index f60ffd3a5ef7..208a647ac0f5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/MultipartAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/MultipartAutoConfigurationTests.java @@ -64,6 +64,7 @@ * @author Josh Long * @author Ivan Sopov * @author Toshiaki Maki + * @author Yanming Zhou */ @DirtiesUrlFactories class MultipartAutoConfigurationTests { @@ -174,6 +175,17 @@ void configureResolveLazily() { assertThat(multipartResolver).hasFieldOrPropertyWithValue("resolveLazily", true); } + @Test + void configureStrictServletCompliance() { + this.context = new AnnotationConfigServletWebServerApplicationContext(); + TestPropertyValues.of("spring.servlet.multipart.strict-servlet-compliance=true").applyTo(this.context); + this.context.register(WebServerWithNothing.class, BaseConfiguration.class); + this.context.refresh(); + StandardServletMultipartResolver multipartResolver = this.context + .getBean(StandardServletMultipartResolver.class); + assertThat(multipartResolver).hasFieldOrPropertyWithValue("strictServletCompliance", true); + } + @Test void configureMultipartProperties() { this.context = new AnnotationConfigServletWebServerApplicationContext(); From 92500720a7eb88a52be72e4af00c0a8fd1f8d004 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 13 Sep 2023 10:41:35 +0200 Subject: [PATCH 0438/1215] Polish "Introduce configuration property for strict servlet compliance" See gh-37242 --- .../boot/autoconfigure/web/servlet/MultipartProperties.java | 4 ++-- .../web/servlet/MultipartAutoConfigurationTests.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/MultipartProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/MultipartProperties.java index 4db7ec2e1c25..6e3a6ccfa2e2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/MultipartProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/MultipartProperties.java @@ -81,8 +81,8 @@ public class MultipartProperties { private boolean resolveLazily = false; /** - * Whether to resolve the multipart request strictly comply with the Servlet specification, - * only kicking in for "multipart/form-data" requests. + * Whether to resolve the multipart request strictly comply with the Servlet + * specification, only to be used for "multipart/form-data" requests. */ private boolean strictServletCompliance = false; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/MultipartAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/MultipartAutoConfigurationTests.java index 208a647ac0f5..edbc441d871f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/MultipartAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/MultipartAutoConfigurationTests.java @@ -182,7 +182,7 @@ void configureStrictServletCompliance() { this.context.register(WebServerWithNothing.class, BaseConfiguration.class); this.context.refresh(); StandardServletMultipartResolver multipartResolver = this.context - .getBean(StandardServletMultipartResolver.class); + .getBean(StandardServletMultipartResolver.class); assertThat(multipartResolver).hasFieldOrPropertyWithValue("strictServletCompliance", true); } From 42434565904b94d73d75c6dcdde7ed799933ce11 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 13 Sep 2023 17:14:46 +0200 Subject: [PATCH 0439/1215] Fix deprecation in Spring Framework --- .../boot/context/properties/bind/Bindable.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Bindable.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Bindable.java index d40937abb32d..9e76a84e82c1 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Bindable.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Bindable.java @@ -156,13 +156,7 @@ public boolean equals(Object obj) { @Override public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ObjectUtils.nullSafeHashCode(this.type); - result = prime * result + ObjectUtils.nullSafeHashCode(this.annotations); - result = prime * result + ObjectUtils.nullSafeHashCode(this.bindRestrictions); - result = prime * result + ObjectUtils.nullSafeHashCode(this.bindMethod); - return result; + return ObjectUtils.nullSafeHash(this.type, this.annotations, this.bindRestrictions, this.bindMethod); } @Override From 77a85fd18068ae4ddf08a5ce6283319c439478ae Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 13 Sep 2023 18:04:55 +0200 Subject: [PATCH 0440/1215] Fix deprecation in Spring Framework --- .../filter/AnnotationCustomizableTypeExcludeFilter.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/AnnotationCustomizableTypeExcludeFilter.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/AnnotationCustomizableTypeExcludeFilter.java index af92b8a1541c..807c4a7d9cf8 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/AnnotationCustomizableTypeExcludeFilter.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/AnnotationCustomizableTypeExcludeFilter.java @@ -18,6 +18,8 @@ import java.io.IOException; import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Objects; import java.util.Set; import org.springframework.beans.factory.BeanClassLoaderAware; @@ -135,11 +137,11 @@ public int hashCode() { int result = 0; result = prime * result + Boolean.hashCode(hasAnnotation()); for (FilterType filterType : FilterType.values()) { - result = prime * result + ObjectUtils.nullSafeHashCode(getFilters(filterType)); + result = prime * result + Arrays.hashCode(getFilters(filterType)); } result = prime * result + Boolean.hashCode(isUseDefaultFilters()); - result = prime * result + ObjectUtils.nullSafeHashCode(getDefaultIncludes()); - result = prime * result + ObjectUtils.nullSafeHashCode(getComponentIncludes()); + result = prime * result + Objects.hashCode(getDefaultIncludes()); + result = prime * result + Objects.hashCode(getComponentIncludes()); return result; } From 360cdc07a8b5c160c61960647e22d5f766cde456 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 13 Sep 2023 21:34:36 +0200 Subject: [PATCH 0441/1215] Upgrade to Elasticsearch Client 8.10.0 Closes gh-37374 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index a4a5ff2b8463..4eb131906a8f 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -268,7 +268,7 @@ bom { ] } } - library("Elasticsearch Client", "8.9.2") { + library("Elasticsearch Client", "8.10.0") { group("org.elasticsearch.client") { modules = [ "elasticsearch-rest-client" { From 4bb2e918edbec578d326bc90ba3124863c410334 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 13 Sep 2023 21:34:36 +0200 Subject: [PATCH 0442/1215] Upgrade to Reactor Bom 2023.0.0-M3 Closes gh-37227 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 4eb131906a8f..949a08de9797 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1326,7 +1326,7 @@ bom { ] } } - library("Reactor Bom", "2023.0.0-SNAPSHOT") { + library("Reactor Bom", "2023.0.0-M3") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From 6997277f75285e98daa2d9a3f912091be675a427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edd=C3=BA=20Mel=C3=A9ndez?= Date: Thu, 14 Sep 2023 09:42:23 +0200 Subject: [PATCH 0443/1215] Add service connection from OpenTelemetry Collector See gh-35082 --- .../export/otlp/OtlpConnectionDetails.java | 31 +++++ .../OtlpMetricsExportAutoConfiguration.java | 28 +++- .../otlp/OtlpPropertiesConfigAdapter.java | 7 +- .../tracing/otlp/OtlpAutoConfiguration.java | 34 ++++- .../otlp/OtlpTracingConnectionDetails.java | 31 +++++ ...lpMetricsExportAutoConfigurationTests.java | 28 ++++ .../OtlpPropertiesConfigAdapterTests.java | 7 +- .../otlp/OtlpAutoConfigurationTests.java | 30 +++++ ...DockerComposeConnectionDetailsFactory.java | 65 +++++++++ ...DockerComposeConnectionDetailsFactory.java | 65 +++++++++ .../service/connection/otlp/package-info.java | 20 +++ .../main/resources/META-INF/spring.factories | 2 + ...nectionDetailsFactoryIntegrationTests.java | 52 ++++++++ .../service/connection/otlp/otlp-compose.yaml | 5 + .../src/docs/asciidoc/features/testing.adoc | 3 + .../spring-boot-testcontainers/build.gradle | 4 + ...OpenTelemetryConnectionDetailsFactory.java | 61 +++++++++ ...emetryTracingConnectionDetailsFactory.java | 63 +++++++++ .../service/connection/otlp/package-info.java | 20 +++ .../main/resources/META-INF/spring.factories | 2 + ...nectionDetailsFactoryIntegrationTests.java | 124 ++++++++++++++++++ ...nectionDetailsFactoryIntegrationTests.java | 64 +++++++++ .../src/test/resources/collector-config.yml | 20 +++ .../testcontainers/DockerImageNames.java | 10 ++ 24 files changed, 768 insertions(+), 8 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpConnectionDetails.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConnectionDetails.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/package-info.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/otlp/otlp-compose.yaml create mode 100644 spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/package-info.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OtlpContainerConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OtlpTracingContainerConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/test/resources/collector-config.yml diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpConnectionDetails.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpConnectionDetails.java new file mode 100644 index 000000000000..7e294e1bac95 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpConnectionDetails.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.metrics.export.otlp; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to a OpenTelemetry Collector service. + * + * @author Eddú Meléndez + * @since 3.1.0 + */ +public interface OtlpConnectionDetails extends ConnectionDetails { + + String getUrl(); + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java index 27e974bab84d..8e87dc48994a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java @@ -50,11 +50,17 @@ @EnableConfigurationProperties({ OtlpProperties.class, OpenTelemetryProperties.class }) public class OtlpMetricsExportAutoConfiguration { + @Bean + @ConditionalOnMissingBean(OtlpConnectionDetails.class) + public OtlpConnectionDetails otlpConnectionDetails(OtlpProperties properties) { + return new PropertiesOtlpConnectionDetails(properties); + } + @Bean @ConditionalOnMissingBean OtlpConfig otlpConfig(OtlpProperties properties, OpenTelemetryProperties openTelemetryProperties, - Environment environment) { - return new OtlpPropertiesConfigAdapter(properties, openTelemetryProperties, environment); + OtlpConnectionDetails connectionDetails, Environment environment) { + return new OtlpPropertiesConfigAdapter(properties, openTelemetryProperties, connectionDetails, environment); } @Bean @@ -63,4 +69,22 @@ public OtlpMeterRegistry otlpMeterRegistry(OtlpConfig otlpConfig, Clock clock) { return new OtlpMeterRegistry(otlpConfig, clock); } + /** + * Adapts {@link OtlpProperties} to {@link OtlpConnectionDetails}. + */ + static class PropertiesOtlpConnectionDetails implements OtlpConnectionDetails { + + private final OtlpProperties properties; + + PropertiesOtlpConnectionDetails(OtlpProperties properties) { + this.properties = properties; + } + + @Override + public String getUrl() { + return this.properties.getUrl(); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java index f8dd3c5bb247..e6ea8f14ba0c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java @@ -45,11 +45,14 @@ class OtlpPropertiesConfigAdapter extends StepRegistryPropertiesConfigAdapter this.connectionDetails.getUrl(), OtlpConfig.super::url); } @Override diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfiguration.java index e86b48ae555e..adc37cc21b54 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfiguration.java @@ -28,6 +28,7 @@ import org.springframework.boot.actuate.autoconfigure.tracing.ConditionalOnEnabledTracing; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -47,6 +48,7 @@ * * @author Jonatan Ivanov * @author Moritz Halbritter + * @author Eddú Meléndez * @since 3.1.0 */ @AutoConfiguration @@ -54,14 +56,22 @@ @EnableConfigurationProperties(OtlpProperties.class) public class OtlpAutoConfiguration { + @Bean + @ConditionalOnMissingBean(OtlpTracingConnectionDetails.class) + @ConditionalOnProperty(prefix = "management.otlp.tracing", name = "endpoint") + OtlpTracingConnectionDetails otlpTracingConnectionDetails(OtlpProperties properties) { + return new PropertiesOtlpTracingConnectionDetails(properties); + } + @Bean @ConditionalOnMissingBean(value = OtlpHttpSpanExporter.class, type = "io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter") - @ConditionalOnProperty(prefix = "management.otlp.tracing", name = "endpoint") + @ConditionalOnBean(OtlpTracingConnectionDetails.class) @ConditionalOnEnabledTracing - OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpProperties properties) { + OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpProperties properties, + OtlpTracingConnectionDetails connectionDetails) { OtlpHttpSpanExporterBuilder builder = OtlpHttpSpanExporter.builder() - .setEndpoint(properties.getEndpoint()) + .setEndpoint(connectionDetails.getEndpoint()) .setTimeout(properties.getTimeout()) .setCompression(properties.getCompression().name().toLowerCase()); for (Entry header : properties.getHeaders().entrySet()) { @@ -70,4 +80,22 @@ OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpProperties properties) { return builder.build(); } + /** + * Adapts {@link OtlpProperties} to {@link OtlpTracingConnectionDetails}. + */ + static class PropertiesOtlpTracingConnectionDetails implements OtlpTracingConnectionDetails { + + private final OtlpProperties properties; + + PropertiesOtlpTracingConnectionDetails(OtlpProperties properties) { + this.properties = properties; + } + + @Override + public String getEndpoint() { + return this.properties.getEndpoint(); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConnectionDetails.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConnectionDetails.java new file mode 100644 index 000000000000..9bbcf58aba72 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConnectionDetails.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.tracing.otlp; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to a OpenTelemetry service. + * + * @author Eddú Meléndez + * @since 3.1.0 + */ +public interface OtlpTracingConnectionDetails extends ConnectionDetails { + + String getEndpoint(); + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfigurationTests.java index 09752dd0c7ff..9a7648387fc6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfigurationTests.java @@ -83,6 +83,24 @@ void allowsRegistryToBeCustomized() { .hasBean("customRegistry")); } + @Test + void definesPropertiesBasedConnectionDetailsByDefault() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class) + .run((context) -> assertThat(context) + .hasSingleBean(OtlpMetricsExportAutoConfiguration.PropertiesOtlpConnectionDetails.class)); + } + + @Test + void testConnectionFactoryWithOverridesWhenUsingCustomConnectionDetails() { + this.contextRunner.withUserConfiguration(BaseConfiguration.class, ConnectionDetailsConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(OtlpConnectionDetails.class) + .doesNotHaveBean(OtlpMetricsExportAutoConfiguration.PropertiesOtlpConnectionDetails.class); + OtlpConfig config = context.getBean(OtlpConfig.class); + assertThat(config.url()).isEqualTo("http://localhost:12345/v1/metrics"); + }); + } + @Configuration(proxyBeanMethods = false) static class BaseConfiguration { @@ -115,4 +133,14 @@ OtlpMeterRegistry customRegistry(OtlpConfig config, Clock clock) { } + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsConfiguration { + + @Bean + OtlpConnectionDetails otlpConnectionDetails() { + return () -> "http://localhost:12345/v1/metrics"; + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java index 8d736bca0e44..0fd2e2e7cb24 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java @@ -24,6 +24,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsExportAutoConfiguration.PropertiesOtlpConnectionDetails; import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryProperties; import org.springframework.mock.env.MockEnvironment; @@ -44,11 +45,14 @@ class OtlpPropertiesConfigAdapterTests { private MockEnvironment environment; + private OtlpConnectionDetails connectionDetails; + @BeforeEach void setUp() { this.properties = new OtlpProperties(); this.openTelemetryProperties = new OpenTelemetryProperties(); this.environment = new MockEnvironment(); + this.connectionDetails = new PropertiesOtlpConnectionDetails(this.properties); } @Test @@ -136,7 +140,8 @@ void shouldUseDefaultApplicationNameIfApplicationNameIsNotSet() { } private OtlpPropertiesConfigAdapter createAdapter() { - return new OtlpPropertiesConfigAdapter(this.properties, this.openTelemetryProperties, this.environment); + return new OtlpPropertiesConfigAdapter(this.properties, this.openTelemetryProperties, this.connectionDetails, + this.environment); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationTests.java index 0f5fbca25c80..301410b7631f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationTests.java @@ -19,6 +19,7 @@ import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; import io.opentelemetry.sdk.trace.export.SpanExporter; +import okhttp3.HttpUrl; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -34,6 +35,7 @@ * * @author Jonatan Ivanov * @author Moritz Halbritter + * @author Eddú Meléndez */ class OtlpAutoConfigurationTests { @@ -106,6 +108,24 @@ void shouldNotSupplyOtlpHttpSpanExporterIfTracingIsDisabled() { .run((context) -> assertThat(context).doesNotHaveBean(OtlpHttpSpanExporter.class)); } + @Test + void definesPropertiesBasedConnectionDetailsByDefault() { + this.contextRunner.withPropertyValues("management.otlp.tracing.endpoint=http://localhost:4318/v1/traces") + .run((context) -> assertThat(context) + .hasSingleBean(OtlpAutoConfiguration.PropertiesOtlpTracingConnectionDetails.class)); + } + + @Test + void testConnectionFactoryWithOverridesWhenUsingCustomConnectionDetails() { + this.contextRunner.withUserConfiguration(ConnectionDetailsConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(OtlpTracingConnectionDetails.class) + .doesNotHaveBean(OtlpAutoConfiguration.PropertiesOtlpTracingConnectionDetails.class); + OtlpHttpSpanExporter otlpHttpSpanExporter = context.getBean(OtlpHttpSpanExporter.class); + assertThat(otlpHttpSpanExporter).extracting("delegate.httpSender.url") + .isEqualTo(HttpUrl.get("http://localhost:12345/v1/traces")); + }); + } + @Configuration(proxyBeanMethods = false) private static class CustomHttpExporterConfiguration { @@ -126,4 +146,14 @@ OtlpGrpcSpanExporter customOtlpGrpcSpanExporter() { } + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsConfiguration { + + @Bean + OtlpTracingConnectionDetails otlpTracingConnectionDetails() { + return () -> "http://localhost:12345/v1/traces"; + } + + } + } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryDockerComposeConnectionDetailsFactory.java new file mode 100644 index 000000000000..e8a1aae3eba9 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.otlp; + +import org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link OtlpConnectionDetails} + * for a {@code otlp} service. + * + * @author Eddú Meléndez + */ +class OpenTelemetryDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + private static final int OTLP_PORT = 4318; + + OpenTelemetryDockerComposeConnectionDetailsFactory() { + super("otel/opentelemetry-collector-contrib", + "org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsExportAutoConfiguration"); + } + + @Override + protected OtlpConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new OpenTelemetryContainerConnectionDetails(source.getRunningService()); + } + + private static final class OpenTelemetryContainerConnectionDetails extends DockerComposeConnectionDetails + implements OtlpConnectionDetails { + + private final String host; + + private final int port; + + private OpenTelemetryContainerConnectionDetails(RunningService source) { + super(source); + this.host = source.host(); + this.port = source.ports().get(OTLP_PORT); + } + + @Override + public String getUrl() { + return "http://" + this.host + ":" + this.port + "/v1/metrics"; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactory.java new file mode 100644 index 000000000000..e137a17b0f1d --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.otlp; + +import org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpTracingConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create + * {@link OtlpTracingConnectionDetails} for a {@code otlp} service. + * + * @author Eddú Meléndez + */ +class OpenTelemetryTracingDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + private static final int OTLP_PORT = 4318; + + OpenTelemetryTracingDockerComposeConnectionDetailsFactory() { + super("otel/opentelemetry-collector-contrib", + "org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpAutoConfiguration"); + } + + @Override + protected OtlpTracingConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new OpenTelemetryTracingDockerComposeConnectionDetails(source.getRunningService()); + } + + private static final class OpenTelemetryTracingDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements OtlpTracingConnectionDetails { + + private final String host; + + private final int port; + + private OpenTelemetryTracingDockerComposeConnectionDetails(RunningService source) { + super(source); + this.host = source.host(); + this.port = source.ports().get(OTLP_PORT); + } + + @Override + public String getEndpoint() { + return "http://" + this.host + ":" + this.port + "/v1/traces"; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/package-info.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/package-info.java new file mode 100644 index 000000000000..cbac91d2c639 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Support for docker compose OpenTelemetry service connections. + */ +package org.springframework.boot.docker.compose.service.connection.otlp; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories index cf5ad6c25a22..fad3f7bb0681 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories @@ -17,6 +17,8 @@ org.springframework.boot.docker.compose.service.connection.mysql.MySqlJdbcDocker org.springframework.boot.docker.compose.service.connection.mysql.MySqlR2dbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.oracle.OracleJdbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.oracle.OracleR2dbcDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.otlp.OpenTelemetryDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.otlp.OpenTelemetryTracingDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.postgres.PostgresJdbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.postgres.PostgresR2dbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.pulsar.PulsarDockerComposeConnectionDetailsFactory,\ diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..4997b96248b0 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.otlp; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpConnectionDetails; +import org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpTracingConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link OpenTelemetryDockerComposeConnectionDetailsFactory}. + * + * @author Eddú Meléndez + */ +public class OpenTelemetryDockerComposeConnectionDetailsFactoryIntegrationTests + extends AbstractDockerComposeIntegrationTests { + + OpenTelemetryDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("otlp-compose.yaml", DockerImageNames.opentelemetry()); + } + + @Test + void runCreatesConnectionDetails() { + OtlpConnectionDetails connectionDetails = run(OtlpConnectionDetails.class); + assertThat(connectionDetails.getUrl()).startsWith("http://").endsWith("/v1/metrics"); + } + + @Test + void runCreatesTracingConnectionDetails() { + OtlpTracingConnectionDetails connectionDetails = run(OtlpTracingConnectionDetails.class); + assertThat(connectionDetails.getEndpoint()).startsWith("http://").endsWith("/v1/traces"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/otlp/otlp-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/otlp/otlp-compose.yaml new file mode 100644 index 000000000000..258e73e333ee --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/otlp/otlp-compose.yaml @@ -0,0 +1,5 @@ +services: + otlp: + image: '{imageName}' + ports: + - '4318' diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc index 9af81d8d44d7..dadaf2d19e0e 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc @@ -992,6 +992,9 @@ The following service connection factories are provided in the `spring-boot-test | `Neo4jConnectionDetails` | Containers of type `Neo4jContainer` +| `OtlpConnectionDetails` +| Containers named "otel/opentelemetry-collector-contrib" + | `PulsarConnectionDetails` | Containers of type `PulsarContainer` diff --git a/spring-boot-project/spring-boot-testcontainers/build.gradle b/spring-boot-project/spring-boot-testcontainers/build.gradle index d09e6d485f29..a0f64a191d88 100644 --- a/spring-boot-project/spring-boot-testcontainers/build.gradle +++ b/spring-boot-project/spring-boot-testcontainers/build.gradle @@ -37,6 +37,10 @@ dependencies { testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) testImplementation(project(":spring-boot-project:spring-boot-test")) testImplementation("ch.qos.logback:logback-classic") + testImplementation("io.micrometer:micrometer-registry-otlp") + testImplementation("io.rest-assured:rest-assured") { + exclude group: "commons-logging", module: "commons-logging" + } testImplementation("org.apache.activemq:activemq-client-jakarta") testImplementation("org.assertj:assertj-core") testImplementation("org.awaitility:awaitility") diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryConnectionDetailsFactory.java new file mode 100644 index 000000000000..f9708636bd5e --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryConnectionDetailsFactory.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2023 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.testcontainers.service.connection.otlp; + +import org.testcontainers.containers.Container; +import org.testcontainers.containers.GenericContainer; + +import org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpConnectionDetails; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; + +/** + * {@link ContainerConnectionDetailsFactory} to create {@link OtlpConnectionDetails} from + * a {@link ServiceConnection @ServiceConnection}-annotated {@link GenericContainer} using + * the {@code "otel/opentelemetry-collector-contrib"} image. + * + * @author Eddú Meléndez + */ +class OpenTelemetryConnectionDetailsFactory + extends ContainerConnectionDetailsFactory, OtlpConnectionDetails> { + + OpenTelemetryConnectionDetailsFactory() { + super("otel/opentelemetry-collector-contrib", + "org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsExportAutoConfiguration"); + } + + @Override + protected OtlpConnectionDetails getContainerConnectionDetails(ContainerConnectionSource> source) { + return new OpenTelemetryContainerConnectionDetails(source); + } + + private static final class OpenTelemetryContainerConnectionDetails extends ContainerConnectionDetails> + implements OtlpConnectionDetails { + + private OpenTelemetryContainerConnectionDetails(ContainerConnectionSource> source) { + super(source); + } + + @Override + public String getUrl() { + return "http://" + getContainer().getHost() + ":" + getContainer().getMappedPort(4318) + "/v1/metrics"; + } + + } + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingConnectionDetailsFactory.java new file mode 100644 index 000000000000..cc82b3c93207 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingConnectionDetailsFactory.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2023 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.testcontainers.service.connection.otlp; + +import org.testcontainers.containers.Container; +import org.testcontainers.containers.GenericContainer; + +import org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpTracingConnectionDetails; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; + +/** + * {@link ContainerConnectionDetailsFactory} to create + * {@link OtlpTracingConnectionDetails} from a + * {@link ServiceConnection @ServiceConnection}-annotated {@link GenericContainer} using + * the {@code "otel/opentelemetry-collector-contrib"} image. + * + * @author Eddú Meléndez + */ +class OpenTelemetryTracingConnectionDetailsFactory + extends ContainerConnectionDetailsFactory, OtlpTracingConnectionDetails> { + + OpenTelemetryTracingConnectionDetailsFactory() { + super("otel/opentelemetry-collector-contrib", + "org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpAutoConfiguration"); + } + + @Override + protected OtlpTracingConnectionDetails getContainerConnectionDetails( + ContainerConnectionSource> source) { + return new OpenTelemetryTracingConnectionDetails(source); + } + + private static final class OpenTelemetryTracingConnectionDetails extends ContainerConnectionDetails> + implements OtlpTracingConnectionDetails { + + private OpenTelemetryTracingConnectionDetails(ContainerConnectionSource> source) { + super(source); + } + + @Override + public String getEndpoint() { + return "http://" + getContainer().getHost() + ":" + getContainer().getMappedPort(4318) + "/v1/traces"; + } + + } + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/package-info.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/package-info.java new file mode 100644 index 000000000000..59b4a9dce0a2 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Support for testcontainers OpenTelemetry service connections. + */ +package org.springframework.boot.testcontainers.service.connection.otlp; diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories index e005e992812a..2ee8ddf68f98 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories @@ -19,6 +19,8 @@ org.springframework.boot.testcontainers.service.connection.kafka.KafkaContainerC org.springframework.boot.testcontainers.service.connection.liquibase.LiquibaseContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.mongo.MongoContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.neo4j.Neo4jContainerConnectionDetailsFactory,\ +org.springframework.boot.testcontainers.service.connection.otlp.OpenTelemetryConnectionDetailsFactory,\ +org.springframework.boot.testcontainers.service.connection.otlp.OpenTelemetryTracingConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.pulsar.PulsarContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.r2dbc.MariaDbR2dbcContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.r2dbc.MySqlR2dbcContainerConnectionDetailsFactory,\ diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OtlpContainerConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OtlpContainerConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..d7947dac68d6 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OtlpContainerConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2012-2023 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.testcontainers.service.connection.otlp; + +import java.time.Duration; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import io.restassured.RestAssured; +import io.restassured.response.Response; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.MountableFile; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsExportAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.matchesPattern; + +/** + * Tests for {@link OpenTelemetryConnectionDetailsFactory}. + * + * @author Eddú Meléndez + * @author Jonatan Ivanov + */ +@SpringJUnitConfig +@TestPropertySource(properties = { "management.otlp.metrics.export.resource-attributes.service.name=test", + "management.otlp.metrics.export.step=1s" }) +@Testcontainers(disabledWithoutDocker = true) +class OtlpContainerConnectionDetailsFactoryIntegrationTests { + + private static final String OPENMETRICS_001 = "application/openmetrics-text; version=0.0.1; charset=utf-8"; + + private static final String CONFIG_FILE_NAME = "collector-config.yml"; + + @Container + @ServiceConnection + static final GenericContainer container = new GenericContainer<>(DockerImageNames.opentelemetry()) + .withCommand("--config=/etc/" + CONFIG_FILE_NAME) + .withCopyToContainer(MountableFile.forClasspathResource(CONFIG_FILE_NAME), "/etc/" + CONFIG_FILE_NAME) + .withExposedPorts(4318, 9090); + + @Autowired + private MeterRegistry meterRegistry; + + @Test + void connectionCanBeMadeToOpenTelemetryCollectorContainer() { + Counter.builder("test.counter").register(this.meterRegistry).increment(42); + Gauge.builder("test.gauge", () -> 12).register(this.meterRegistry); + Timer.builder("test.timer").register(this.meterRegistry).record(Duration.ofMillis(123)); + DistributionSummary.builder("test.distributionsummary").register(this.meterRegistry).record(24); + + Awaitility.await() + .atMost(Duration.ofSeconds(5)) + .pollDelay(Duration.ofMillis(100)) + .pollInterval(Duration.ofMillis(100)) + .untilAsserted(() -> whenPrometheusScraped().then() + .statusCode(200) + .contentType(OPENMETRICS_001) + .body(endsWith("# EOF\n"))); + + whenPrometheusScraped().then() + .body(containsString( + "{job=\"test\",service_name=\"test\",telemetry_sdk_language=\"java\",telemetry_sdk_name=\"io.micrometer\""), + + matchesPattern("(?s)^.*test_counter\\{.+} 42\\.0\\n.*$"), + matchesPattern("(?s)^.*test_gauge\\{.+} 12\\.0\\n.*$"), + + matchesPattern("(?s)^.*test_timer_count\\{.+} 1\\n.*$"), + + matchesPattern("(?s)^.*test_timer_sum\\{.+} 123\\.0\\n.*$"), + matchesPattern("(?s)^.*test_timer_bucket\\{.+,le=\"\\+Inf\"} 1\\n.*$"), + + matchesPattern("(?s)^.*test_distributionsummary_count\\{.+} 1\\n.*$"), + matchesPattern("(?s)^.*test_distributionsummary_sum\\{.+} 24\\.0\\n.*$"), + matchesPattern("(?s)^.*test_distributionsummary_bucket\\{.+,le=\"\\+Inf\"} 1\\n.*$")); + } + + private Response whenPrometheusScraped() { + return RestAssured.given().port(container.getMappedPort(9090)).accept(OPENMETRICS_001).when().get("/metrics"); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(OtlpMetricsExportAutoConfiguration.class) + static class TestConfiguration { + + @Bean + Clock customClock() { + return Clock.SYSTEM; + } + + } + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OtlpTracingContainerConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OtlpTracingContainerConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..86900abb77e3 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OtlpTracingContainerConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2023 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.testcontainers.service.connection.otlp; + +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpTracingConnectionDetails; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link OpenTelemetryTracingConnectionDetailsFactory}. + * + * @author Eddú Meléndez + */ +@SpringJUnitConfig +@Testcontainers(disabledWithoutDocker = true) +class OtlpTracingContainerConnectionDetailsFactoryIntegrationTests { + + @Container + @ServiceConnection + static final GenericContainer container = new GenericContainer<>(DockerImageNames.opentelemetry()) + .withExposedPorts(4318); + + @Autowired + private OtlpTracingConnectionDetails connectionDetails; + + @Test + void connectionCanBeMadeToOpenTelemetryContainer() { + assertThat(this.connectionDetails.getEndpoint()) + .isEqualTo("http://" + container.getHost() + ":" + container.getMappedPort(4318) + "/v1/traces"); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(OtlpAutoConfiguration.class) + static class TestConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/resources/collector-config.yml b/spring-boot-project/spring-boot-testcontainers/src/test/resources/collector-config.yml new file mode 100644 index 000000000000..c17a371d66c2 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/test/resources/collector-config.yml @@ -0,0 +1,20 @@ +receivers: + otlp: + protocols: + grpc: + http: + +exporters: + # https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/prometheusexporter + prometheus: + endpoint: '0.0.0.0:9090' + metric_expiration: 1m + enable_open_metrics: true + resource_to_telemetry_conversion: + enabled: true + +service: + pipelines: + metrics: + receivers: [otlp] + exporters: [prometheus] \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java index 780e594e1a8e..497ee76563d5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java @@ -51,6 +51,8 @@ public final class DockerImageNames { private static final String ORACLE_XE_VERSION = "18.4.0-slim"; + private static final String OPENTELEMETRY_VERSION = "0.75.0"; + private static final String PULSAR_VERSION = "3.1.0"; private static final String POSTGRESQL_VERSION = "14.0"; @@ -156,6 +158,14 @@ public static DockerImageName oracleXe() { return DockerImageName.parse("gvenzl/oracle-xe").withTag(ORACLE_XE_VERSION); } + /** + * Return a {@link DockerImageName} suitable for running the Oracle database. + * @return a docker image name for running the Oracle database + */ + public static DockerImageName opentelemetry() { + return DockerImageName.parse("otel/opentelemetry-collector-contrib").withTag(OPENTELEMETRY_VERSION); + } + /** * Return a {@link DockerImageName} suitable for running Apache Pulsar. * @return a docker image name for running pulsar From c387c87fda5adb3f835b3a0f3727517a8ff3af91 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 14 Sep 2023 09:58:19 +0200 Subject: [PATCH 0444/1215] Polish "Add service connection from OpenTelemetry Collector" See gh-35082 --- ...java => OtlpMetricsConnectionDetails.java} | 8 +- .../OtlpMetricsExportAutoConfiguration.java | 25 +++-- .../otlp/OtlpPropertiesConfigAdapter.java | 4 +- .../tracing/otlp/OtlpAutoConfiguration.java | 52 +---------- .../otlp/OtlpTracingConfigurations.java | 93 +++++++++++++++++++ .../otlp/OtlpTracingConnectionDetails.java | 6 +- ...lpMetricsExportAutoConfigurationTests.java | 10 +- .../OtlpPropertiesConfigAdapterTests.java | 6 +- .../otlp/OtlpAutoConfigurationTests.java | 6 +- ...ockerComposeConnectionDetailsFactory.java} | 24 ++--- ...DockerComposeConnectionDetailsFactory.java | 4 +- .../main/resources/META-INF/spring.factories | 2 +- ...nectionDetailsFactoryIntegrationTests.java | 46 +++++++++ ...ectionDetailsFactoryIntegrationTests.java} | 14 +-- .../asciidoc/features/docker-compose.adoc | 6 ++ .../src/docs/asciidoc/features/testing.adoc | 5 +- ...metryMetricsConnectionDetailsFactory.java} | 26 +++--- ...emetryTracingConnectionDetailsFactory.java | 2 +- .../main/resources/META-INF/spring.factories | 2 +- ...ectionDetailsFactoryIntegrationTests.java} | 4 +- ...ectionDetailsFactoryIntegrationTests.java} | 2 +- 21 files changed, 229 insertions(+), 118 deletions(-) rename spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/{OtlpConnectionDetails.java => OtlpMetricsConnectionDetails.java} (81%) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConfigurations.java rename spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/{OpenTelemetryDockerComposeConnectionDetailsFactory.java => OpenTelemetryMetricsDockerComposeConnectionDetailsFactory.java} (62%) create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java rename spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/{OpenTelemetryDockerComposeConnectionDetailsFactoryIntegrationTests.java => OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests.java} (72%) rename spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/{OpenTelemetryConnectionDetailsFactory.java => OpenTelemetryMetricsConnectionDetailsFactory.java} (59%) rename spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/{OtlpContainerConnectionDetailsFactoryIntegrationTests.java => OpenTelemetryMetricsConnectionDetailsFactoryIntegrationTests.java} (97%) rename spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/{OtlpTracingContainerConnectionDetailsFactoryIntegrationTests.java => OpenTelemetryTracingConnectionDetailsFactoryIntegrationTests.java} (97%) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpConnectionDetails.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsConnectionDetails.java similarity index 81% rename from spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpConnectionDetails.java rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsConnectionDetails.java index 7e294e1bac95..16f968a89de0 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpConnectionDetails.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsConnectionDetails.java @@ -22,10 +22,14 @@ * Details required to establish a connection to a OpenTelemetry Collector service. * * @author Eddú Meléndez - * @since 3.1.0 + * @since 3.2.0 */ -public interface OtlpConnectionDetails extends ConnectionDetails { +public interface OtlpMetricsConnectionDetails extends ConnectionDetails { + /** + * Address to where metrics will be published. + * @return the address to where metrics will be published + */ String getUrl(); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java index 8e87dc48994a..546503c6d9ad 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java @@ -50,17 +50,24 @@ @EnableConfigurationProperties({ OtlpProperties.class, OpenTelemetryProperties.class }) public class OtlpMetricsExportAutoConfiguration { + private final OtlpProperties properties; + + OtlpMetricsExportAutoConfiguration(OtlpProperties properties) { + this.properties = properties; + } + @Bean - @ConditionalOnMissingBean(OtlpConnectionDetails.class) - public OtlpConnectionDetails otlpConnectionDetails(OtlpProperties properties) { - return new PropertiesOtlpConnectionDetails(properties); + @ConditionalOnMissingBean(OtlpMetricsConnectionDetails.class) + OtlpMetricsConnectionDetails otlpMetricsConnectionDetails() { + return new PropertiesOtlpMetricsConnectionDetails(this.properties); } @Bean @ConditionalOnMissingBean - OtlpConfig otlpConfig(OtlpProperties properties, OpenTelemetryProperties openTelemetryProperties, - OtlpConnectionDetails connectionDetails, Environment environment) { - return new OtlpPropertiesConfigAdapter(properties, openTelemetryProperties, connectionDetails, environment); + OtlpConfig otlpConfig(OpenTelemetryProperties openTelemetryProperties, + OtlpMetricsConnectionDetails connectionDetails, Environment environment) { + return new OtlpPropertiesConfigAdapter(this.properties, openTelemetryProperties, connectionDetails, + environment); } @Bean @@ -70,13 +77,13 @@ public OtlpMeterRegistry otlpMeterRegistry(OtlpConfig otlpConfig, Clock clock) { } /** - * Adapts {@link OtlpProperties} to {@link OtlpConnectionDetails}. + * Adapts {@link OtlpProperties} to {@link OtlpMetricsConnectionDetails}. */ - static class PropertiesOtlpConnectionDetails implements OtlpConnectionDetails { + static class PropertiesOtlpMetricsConnectionDetails implements OtlpMetricsConnectionDetails { private final OtlpProperties properties; - PropertiesOtlpConnectionDetails(OtlpProperties properties) { + PropertiesOtlpMetricsConnectionDetails(OtlpProperties properties) { this.properties = properties; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java index e6ea8f14ba0c..c77b39a92612 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java @@ -45,12 +45,12 @@ class OtlpPropertiesConfigAdapter extends StepRegistryPropertiesConfigAdapter header : properties.getHeaders().entrySet()) { - builder.addHeader(header.getKey(), header.getValue()); - } - return builder.build(); - } - - /** - * Adapts {@link OtlpProperties} to {@link OtlpTracingConnectionDetails}. - */ - static class PropertiesOtlpTracingConnectionDetails implements OtlpTracingConnectionDetails { - - private final OtlpProperties properties; - - PropertiesOtlpTracingConnectionDetails(OtlpProperties properties) { - this.properties = properties; - } - - @Override - public String getEndpoint() { - return this.properties.getEndpoint(); - } - - } - } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConfigurations.java new file mode 100644 index 000000000000..6fc5f21fd191 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConfigurations.java @@ -0,0 +1,93 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.tracing.otlp; + +import java.util.Map.Entry; + +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder; + +import org.springframework.boot.actuate.autoconfigure.tracing.ConditionalOnEnabledTracing; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configurations imported by {@link OtlpAutoConfiguration}. + * + * @author Moritz Halbritter + */ +final class OtlpTracingConfigurations { + + private OtlpTracingConfigurations() { + } + + @Configuration(proxyBeanMethods = false) + static class ConnectionDetails { + + @Bean + @ConditionalOnMissingBean(OtlpTracingConnectionDetails.class) + @ConditionalOnProperty(prefix = "management.otlp.tracing", name = "endpoint") + OtlpTracingConnectionDetails otlpTracingConnectionDetails(OtlpProperties properties) { + return new PropertiesOtlpTracingConnectionDetails(properties); + } + + /** + * Adapts {@link OtlpProperties} to {@link OtlpTracingConnectionDetails}. + */ + static class PropertiesOtlpTracingConnectionDetails implements OtlpTracingConnectionDetails { + + private final OtlpProperties properties; + + PropertiesOtlpTracingConnectionDetails(OtlpProperties properties) { + this.properties = properties; + } + + @Override + public String getEndpoint() { + return this.properties.getEndpoint(); + } + + } + + } + + @Configuration(proxyBeanMethods = false) + static class Exporters { + + @Bean + @ConditionalOnMissingBean(value = OtlpHttpSpanExporter.class, + type = "io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter") + @ConditionalOnBean(OtlpTracingConnectionDetails.class) + @ConditionalOnEnabledTracing + OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpProperties properties, + OtlpTracingConnectionDetails connectionDetails) { + OtlpHttpSpanExporterBuilder builder = OtlpHttpSpanExporter.builder() + .setEndpoint(connectionDetails.getEndpoint()) + .setTimeout(properties.getTimeout()) + .setCompression(properties.getCompression().name().toLowerCase()); + for (Entry header : properties.getHeaders().entrySet()) { + builder.addHeader(header.getKey(), header.getValue()); + } + return builder.build(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConnectionDetails.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConnectionDetails.java index 9bbcf58aba72..e5c8e5b37536 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConnectionDetails.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConnectionDetails.java @@ -22,10 +22,14 @@ * Details required to establish a connection to a OpenTelemetry service. * * @author Eddú Meléndez - * @since 3.1.0 + * @since 3.2.0 */ public interface OtlpTracingConnectionDetails extends ConnectionDetails { + /** + * Address to where metrics will be published. + * @return the address to where metrics will be published + */ String getEndpoint(); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfigurationTests.java index 9a7648387fc6..f303e4a2cfdc 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfigurationTests.java @@ -21,6 +21,7 @@ import io.micrometer.registry.otlp.OtlpMeterRegistry; import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsExportAutoConfiguration.PropertiesOtlpMetricsConnectionDetails; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -86,16 +87,15 @@ void allowsRegistryToBeCustomized() { @Test void definesPropertiesBasedConnectionDetailsByDefault() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) - .run((context) -> assertThat(context) - .hasSingleBean(OtlpMetricsExportAutoConfiguration.PropertiesOtlpConnectionDetails.class)); + .run((context) -> assertThat(context).hasSingleBean(PropertiesOtlpMetricsConnectionDetails.class)); } @Test void testConnectionFactoryWithOverridesWhenUsingCustomConnectionDetails() { this.contextRunner.withUserConfiguration(BaseConfiguration.class, ConnectionDetailsConfiguration.class) .run((context) -> { - assertThat(context).hasSingleBean(OtlpConnectionDetails.class) - .doesNotHaveBean(OtlpMetricsExportAutoConfiguration.PropertiesOtlpConnectionDetails.class); + assertThat(context).hasSingleBean(OtlpMetricsConnectionDetails.class) + .doesNotHaveBean(PropertiesOtlpMetricsConnectionDetails.class); OtlpConfig config = context.getBean(OtlpConfig.class); assertThat(config.url()).isEqualTo("http://localhost:12345/v1/metrics"); }); @@ -137,7 +137,7 @@ OtlpMeterRegistry customRegistry(OtlpConfig config, Clock clock) { static class ConnectionDetailsConfiguration { @Bean - OtlpConnectionDetails otlpConnectionDetails() { + OtlpMetricsConnectionDetails otlpConnectionDetails() { return () -> "http://localhost:12345/v1/metrics"; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java index 0fd2e2e7cb24..25151b63a64c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java @@ -24,7 +24,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsExportAutoConfiguration.PropertiesOtlpConnectionDetails; +import org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsExportAutoConfiguration.PropertiesOtlpMetricsConnectionDetails; import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryProperties; import org.springframework.mock.env.MockEnvironment; @@ -45,14 +45,14 @@ class OtlpPropertiesConfigAdapterTests { private MockEnvironment environment; - private OtlpConnectionDetails connectionDetails; + private OtlpMetricsConnectionDetails connectionDetails; @BeforeEach void setUp() { this.properties = new OtlpProperties(); this.openTelemetryProperties = new OpenTelemetryProperties(); this.environment = new MockEnvironment(); - this.connectionDetails = new PropertiesOtlpConnectionDetails(this.properties); + this.connectionDetails = new PropertiesOtlpMetricsConnectionDetails(this.properties); } @Test diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationTests.java index 301410b7631f..9ca5956e2254 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpAutoConfigurationTests.java @@ -22,6 +22,7 @@ import okhttp3.HttpUrl; import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpTracingConfigurations.ConnectionDetails.PropertiesOtlpTracingConnectionDetails; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -111,15 +112,14 @@ void shouldNotSupplyOtlpHttpSpanExporterIfTracingIsDisabled() { @Test void definesPropertiesBasedConnectionDetailsByDefault() { this.contextRunner.withPropertyValues("management.otlp.tracing.endpoint=http://localhost:4318/v1/traces") - .run((context) -> assertThat(context) - .hasSingleBean(OtlpAutoConfiguration.PropertiesOtlpTracingConnectionDetails.class)); + .run((context) -> assertThat(context).hasSingleBean(PropertiesOtlpTracingConnectionDetails.class)); } @Test void testConnectionFactoryWithOverridesWhenUsingCustomConnectionDetails() { this.contextRunner.withUserConfiguration(ConnectionDetailsConfiguration.class).run((context) -> { assertThat(context).hasSingleBean(OtlpTracingConnectionDetails.class) - .doesNotHaveBean(OtlpAutoConfiguration.PropertiesOtlpTracingConnectionDetails.class); + .doesNotHaveBean(PropertiesOtlpTracingConnectionDetails.class); OtlpHttpSpanExporter otlpHttpSpanExporter = context.getBean(OtlpHttpSpanExporter.class); assertThat(otlpHttpSpanExporter).extracting("delegate.httpSender.url") .isEqualTo(HttpUrl.get("http://localhost:12345/v1/traces")); diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactory.java similarity index 62% rename from spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryDockerComposeConnectionDetailsFactory.java rename to spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactory.java index e8a1aae3eba9..2df408c968d8 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactory.java @@ -16,40 +16,40 @@ package org.springframework.boot.docker.compose.service.connection.otlp; -import org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpConnectionDetails; +import org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsConnectionDetails; import org.springframework.boot.docker.compose.core.RunningService; import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; /** - * {@link DockerComposeConnectionDetailsFactory} to create {@link OtlpConnectionDetails} - * for a {@code otlp} service. + * {@link DockerComposeConnectionDetailsFactory} to create + * {@link OtlpMetricsConnectionDetails} for a {@code OTLP} service. * * @author Eddú Meléndez */ -class OpenTelemetryDockerComposeConnectionDetailsFactory - extends DockerComposeConnectionDetailsFactory { +class OpenTelemetryMetricsDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { private static final int OTLP_PORT = 4318; - OpenTelemetryDockerComposeConnectionDetailsFactory() { + OpenTelemetryMetricsDockerComposeConnectionDetailsFactory() { super("otel/opentelemetry-collector-contrib", "org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsExportAutoConfiguration"); } @Override - protected OtlpConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { - return new OpenTelemetryContainerConnectionDetails(source.getRunningService()); + protected OtlpMetricsConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new OpenTelemetryContainerMetricsConnectionDetails(source.getRunningService()); } - private static final class OpenTelemetryContainerConnectionDetails extends DockerComposeConnectionDetails - implements OtlpConnectionDetails { + private static final class OpenTelemetryContainerMetricsConnectionDetails extends DockerComposeConnectionDetails + implements OtlpMetricsConnectionDetails { private final String host; private final int port; - private OpenTelemetryContainerConnectionDetails(RunningService source) { + private OpenTelemetryContainerMetricsConnectionDetails(RunningService source) { super(source); this.host = source.host(); this.port = source.ports().get(OTLP_PORT); @@ -57,7 +57,7 @@ private OpenTelemetryContainerConnectionDetails(RunningService source) { @Override public String getUrl() { - return "http://" + this.host + ":" + this.port + "/v1/metrics"; + return "http://%s:%d/v1/metrics".formatted(this.host, this.port); } } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactory.java index e137a17b0f1d..2a1254017fa2 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactory.java @@ -23,7 +23,7 @@ /** * {@link DockerComposeConnectionDetailsFactory} to create - * {@link OtlpTracingConnectionDetails} for a {@code otlp} service. + * {@link OtlpTracingConnectionDetails} for a {@code OTLP} service. * * @author Eddú Meléndez */ @@ -57,7 +57,7 @@ private OpenTelemetryTracingDockerComposeConnectionDetails(RunningService source @Override public String getEndpoint() { - return "http://" + this.host + ":" + this.port + "/v1/traces"; + return "http://%s:%d/v1/traces".formatted(this.host, this.port); } } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories index fad3f7bb0681..80fe3d28309b 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories @@ -17,7 +17,7 @@ org.springframework.boot.docker.compose.service.connection.mysql.MySqlJdbcDocker org.springframework.boot.docker.compose.service.connection.mysql.MySqlR2dbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.oracle.OracleJdbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.oracle.OracleR2dbcDockerComposeConnectionDetailsFactory,\ -org.springframework.boot.docker.compose.service.connection.otlp.OpenTelemetryDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.otlp.OpenTelemetryMetricsDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.otlp.OpenTelemetryTracingDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.postgres.PostgresJdbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.postgres.PostgresR2dbcDockerComposeConnectionDetailsFactory,\ diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..b2fa3e93e786 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.otlp; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for + * {@link OpenTelemetryMetricsDockerComposeConnectionDetailsFactory}. + * + * @author Eddú Meléndez + */ +public class OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests + extends AbstractDockerComposeIntegrationTests { + + OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("otlp-compose.yaml", DockerImageNames.opentelemetry()); + } + + @Test + void runCreatesConnectionDetails() { + OtlpMetricsConnectionDetails connectionDetails = run(OtlpMetricsConnectionDetails.class); + assertThat(connectionDetails.getUrl()).startsWith("http://").endsWith("/v1/metrics"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 72% rename from spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests.java index 4997b96248b0..5eee38a1e49a 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -18,7 +18,6 @@ import org.junit.jupiter.api.Test; -import org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpConnectionDetails; import org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpTracingConnectionDetails; import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; import org.springframework.boot.testsupport.testcontainers.DockerImageNames; @@ -26,25 +25,20 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link OpenTelemetryDockerComposeConnectionDetailsFactory}. + * Integration tests for + * {@link OpenTelemetryTracingDockerComposeConnectionDetailsFactory}. * * @author Eddú Meléndez */ -public class OpenTelemetryDockerComposeConnectionDetailsFactoryIntegrationTests +public class OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { - OpenTelemetryDockerComposeConnectionDetailsFactoryIntegrationTests() { + OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests() { super("otlp-compose.yaml", DockerImageNames.opentelemetry()); } @Test void runCreatesConnectionDetails() { - OtlpConnectionDetails connectionDetails = run(OtlpConnectionDetails.class); - assertThat(connectionDetails.getUrl()).startsWith("http://").endsWith("/v1/metrics"); - } - - @Test - void runCreatesTracingConnectionDetails() { OtlpTracingConnectionDetails connectionDetails = run(OtlpTracingConnectionDetails.class); assertThat(connectionDetails.getEndpoint()).startsWith("http://").endsWith("/v1/traces"); } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc index 4caedf396060..bebc060fa193 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc @@ -76,6 +76,12 @@ The following service connections are currently supported: | `MongoConnectionDetails` | Containers named "mongo" +| `OtlpMetricsConnectionDetails` +| Containers named "otel/opentelemetry-collector-contrib" + +| `OtlpTracingConnectionDetails` +| Containers named "otel/opentelemetry-collector-contrib" + | `PulsarConnectionDetails` | Containers named "apachepulsar/pulsar" diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc index dadaf2d19e0e..71e79813b882 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc @@ -992,7 +992,10 @@ The following service connection factories are provided in the `spring-boot-test | `Neo4jConnectionDetails` | Containers of type `Neo4jContainer` -| `OtlpConnectionDetails` +| `OtlpMetricsConnectionDetails` +| Containers named "otel/opentelemetry-collector-contrib" + +| `OtlpTracingConnectionDetails` | Containers named "otel/opentelemetry-collector-contrib" | `PulsarConnectionDetails` diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsConnectionDetailsFactory.java similarity index 59% rename from spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryConnectionDetailsFactory.java rename to spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsConnectionDetailsFactory.java index f9708636bd5e..a200750e1b16 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsConnectionDetailsFactory.java @@ -19,41 +19,43 @@ import org.testcontainers.containers.Container; import org.testcontainers.containers.GenericContainer; -import org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpConnectionDetails; +import org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsConnectionDetails; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; /** - * {@link ContainerConnectionDetailsFactory} to create {@link OtlpConnectionDetails} from - * a {@link ServiceConnection @ServiceConnection}-annotated {@link GenericContainer} using + * {@link ContainerConnectionDetailsFactory} to create + * {@link OtlpMetricsConnectionDetails} from a + * {@link ServiceConnection @ServiceConnection}-annotated {@link GenericContainer} using * the {@code "otel/opentelemetry-collector-contrib"} image. * * @author Eddú Meléndez */ -class OpenTelemetryConnectionDetailsFactory - extends ContainerConnectionDetailsFactory, OtlpConnectionDetails> { +class OpenTelemetryMetricsConnectionDetailsFactory + extends ContainerConnectionDetailsFactory, OtlpMetricsConnectionDetails> { - OpenTelemetryConnectionDetailsFactory() { + OpenTelemetryMetricsConnectionDetailsFactory() { super("otel/opentelemetry-collector-contrib", "org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsExportAutoConfiguration"); } @Override - protected OtlpConnectionDetails getContainerConnectionDetails(ContainerConnectionSource> source) { - return new OpenTelemetryContainerConnectionDetails(source); + protected OtlpMetricsConnectionDetails getContainerConnectionDetails( + ContainerConnectionSource> source) { + return new OpenTelemetryContainerMetricsConnectionDetails(source); } - private static final class OpenTelemetryContainerConnectionDetails extends ContainerConnectionDetails> - implements OtlpConnectionDetails { + private static final class OpenTelemetryContainerMetricsConnectionDetails + extends ContainerConnectionDetails> implements OtlpMetricsConnectionDetails { - private OpenTelemetryContainerConnectionDetails(ContainerConnectionSource> source) { + private OpenTelemetryContainerMetricsConnectionDetails(ContainerConnectionSource> source) { super(source); } @Override public String getUrl() { - return "http://" + getContainer().getHost() + ":" + getContainer().getMappedPort(4318) + "/v1/metrics"; + return "http://%s:%d/v1/metrics".formatted(getContainer().getHost(), getContainer().getMappedPort(4318)); } } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingConnectionDetailsFactory.java index cc82b3c93207..0a6d9f636987 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingConnectionDetailsFactory.java @@ -55,7 +55,7 @@ private OpenTelemetryTracingConnectionDetails(ContainerConnectionSource Date: Thu, 14 Sep 2023 11:10:19 +0200 Subject: [PATCH 0445/1215] Polish "Add service connection from OpenTelemetry Collector" See gh-35082 --- .../autoconfigure/tracing/otlp/OtlpTracingConfigurations.java | 4 ++-- .../tracing/otlp/OtlpTracingConnectionDetails.java | 2 +- ...TelemetryTracingDockerComposeConnectionDetailsFactory.java | 2 +- ...DockerComposeConnectionDetailsFactoryIntegrationTests.java | 2 +- .../otlp/OpenTelemetryTracingConnectionDetailsFactory.java | 2 +- ...emetryTracingConnectionDetailsFactoryIntegrationTests.java | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConfigurations.java index 6fc5f21fd191..eb136aac2118 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConfigurations.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConfigurations.java @@ -60,7 +60,7 @@ static class PropertiesOtlpTracingConnectionDetails implements OtlpTracingConnec } @Override - public String getEndpoint() { + public String getUrl() { return this.properties.getEndpoint(); } @@ -79,7 +79,7 @@ static class Exporters { OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpProperties properties, OtlpTracingConnectionDetails connectionDetails) { OtlpHttpSpanExporterBuilder builder = OtlpHttpSpanExporter.builder() - .setEndpoint(connectionDetails.getEndpoint()) + .setEndpoint(connectionDetails.getUrl()) .setTimeout(properties.getTimeout()) .setCompression(properties.getCompression().name().toLowerCase()); for (Entry header : properties.getHeaders().entrySet()) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConnectionDetails.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConnectionDetails.java index e5c8e5b37536..fb73b615bb87 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConnectionDetails.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConnectionDetails.java @@ -30,6 +30,6 @@ public interface OtlpTracingConnectionDetails extends ConnectionDetails { * Address to where metrics will be published. * @return the address to where metrics will be published */ - String getEndpoint(); + String getUrl(); } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactory.java index 2a1254017fa2..359f4df19389 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactory.java @@ -56,7 +56,7 @@ private OpenTelemetryTracingDockerComposeConnectionDetails(RunningService source } @Override - public String getEndpoint() { + public String getUrl() { return "http://%s:%d/v1/traces".formatted(this.host, this.port); } diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests.java index 5eee38a1e49a..7afae26469f1 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -40,7 +40,7 @@ public class OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegratio @Test void runCreatesConnectionDetails() { OtlpTracingConnectionDetails connectionDetails = run(OtlpTracingConnectionDetails.class); - assertThat(connectionDetails.getEndpoint()).startsWith("http://").endsWith("/v1/traces"); + assertThat(connectionDetails.getUrl()).startsWith("http://").endsWith("/v1/traces"); } } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingConnectionDetailsFactory.java index 0a6d9f636987..4f4a394cd3a8 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingConnectionDetailsFactory.java @@ -54,7 +54,7 @@ private OpenTelemetryTracingConnectionDetails(ContainerConnectionSource Date: Wed, 13 Sep 2023 15:03:13 -0500 Subject: [PATCH 0446/1215] Add SSL section to Pulsar docs - Add link to Spring Pulsar TLS docs - Update usage of 'Spring Pulsar' to 'Spring for Apache Pulsar' See gh-37375 --- .../src/docs/asciidoc/documentation/messaging.adoc | 2 +- .../src/docs/asciidoc/messaging/pulsar.adoc | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/messaging.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/messaging.adoc index d6ced6c27794..0ccc798988bd 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/messaging.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/messaging.adoc @@ -5,6 +5,6 @@ If your application uses any messaging protocol, see one or more of the followin * *JMS:* <> * *AMQP:* <> * *Kafka:* <> -* *Pulsar:* <> +* *Pulsar:* <> * *RSocket:* <> * *Spring Integration:* <> diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/pulsar.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/pulsar.adoc index 8e3f78ebc871..f2ced63368b1 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/pulsar.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/pulsar.adoc @@ -2,7 +2,7 @@ == Apache Pulsar Support https://pulsar.apache.org/[Apache Pulsar] is supported by providing auto-configuration of the {spring-pulsar-docs}[Spring for Apache Pulsar] project. -Spring Boot will auto-configure and register the classic (imperative) Spring Pulsar components when `org.springframework.pulsar:spring-pulsar` is on the classpath. +Spring Boot will auto-configure and register the classic (imperative) Spring for Apache Pulsar components when `org.springframework.pulsar:spring-pulsar` is on the classpath. It will do the same for the reactive components when `org.springframework.pulsar:spring-pulsar-reactive` is on the classpath. There are `spring-boot-starter-pulsar` and `spring-boot-starter-pulsar-reactive` "`Starters`" for conveniently collecting the dependencies for imperative and reactive use, respectively. @@ -26,7 +26,7 @@ If you need more control over the configuration, consider registering one or mor [[messaging.pulsar.connecting.auth]] ==== Authentication -To connect to a Pulsar cluster that requires authentication, you need to specify which authentication plugin to use by setting the `authPluginClassName` and any parameters required by the plugin. +To connect to a Pulsar cluster that requires authentication, you need to specify which authentication plugin to use by setting the `pluginClassName` and any parameters required by the plugin. You can set the parameters as a map of parameter names to parameter values. The following example shows how to configure the `AuthenticationOAuth2` plugin. @@ -52,7 +52,12 @@ For example, if you want to configure the issuer url for the `AuthenticationOAut If you use other forms, such as `issuerurl` or `issuer-url`, the setting will not be applied to the plugin. ==== -For complete details on the client and authentication see the Spring Pulsar {spring-pulsar-docs}#pulsar-client[reference documentation]. +[[messaging.pulsar.connecting.ssl]] +==== SSL +By default, Pulsar clients communicate with Pulsar services in plain text. +You can follow {spring-pulsar-docs}#tls-encryption[these steps] in the Spring for Apache Pulsar reference documentation to enable TLS encryption. + +For complete details on the client and authentication see the Spring for Apache Pulsar {spring-pulsar-docs}#pulsar-client[reference documentation]. @@ -67,7 +72,7 @@ Therefore, follow the previous section to configure the `PulsarClient` used by t [[messaging.pulsar.admin]] === Connecting to Pulsar Administration -Spring Pulsar's `PulsarAdministration` client is also auto-configured. +Spring for Apache Pulsar's `PulsarAdministration` client is also auto-configured. By default, the application tries to connect to a local Pulsar instance at `\http://localhost:8080`. This can be adjusted by setting the configprop:spring.pulsar.admin.service-url[] property to a different value in the form `(http|https)://:`. From c5f7f11a13db47f09cd3b41d1eb6c18a319b94a3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Sep 2023 11:39:39 +0100 Subject: [PATCH 0447/1215] Align with repackaging of CaffeineCacheProvider in Spring Pulsar See gh-34763 --- .../autoconfigure/pulsar/PulsarAutoConfigurationTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java index ca05f2edde3e..16e66c9a4737 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java @@ -42,6 +42,7 @@ import org.springframework.pulsar.annotation.PulsarBootstrapConfiguration; import org.springframework.pulsar.annotation.PulsarListenerAnnotationBeanPostProcessor; import org.springframework.pulsar.annotation.PulsarReaderAnnotationBeanPostProcessor; +import org.springframework.pulsar.cache.provider.caffeine.CaffeineCacheProvider; import org.springframework.pulsar.config.ConcurrentPulsarListenerContainerFactory; import org.springframework.pulsar.config.DefaultPulsarReaderContainerFactory; import org.springframework.pulsar.config.PulsarListenerContainerFactory; @@ -174,8 +175,7 @@ void whenCachingEnabledAndCaffeineNotOnClasspathStillUsesCaffeine() { assertThat(context).getBean(CachingPulsarProducerFactory.class) .extracting("producerCache") .extracting(Object::getClass) - .extracting(Class::getName) - .isEqualTo("org.springframework.pulsar.core.CaffeineCacheProvider"); + .isEqualTo(CaffeineCacheProvider.class); assertThat(context).getBean(CachingPulsarProducerFactory.class) .extracting("producerCache.cache") .extracting(Object::getClass) From 4f6e50b55a766dd9880e6110b8d5804f6ae0ffd5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Sep 2023 12:58:29 +0100 Subject: [PATCH 0448/1215] Make Spring Pulsar's Caffeine cache provider available to test compile See gh-34763 --- spring-boot-project/spring-boot-autoconfigure/build.gradle | 1 + spring-boot-project/spring-boot-dependencies/build.gradle | 2 ++ 2 files changed, 3 insertions(+) diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index 5eaddf26aab9..4860793c6ccd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -240,6 +240,7 @@ dependencies { testImplementation("org.springframework:spring-core-test") testImplementation("org.springframework.graphql:spring-graphql-test") testImplementation("org.springframework.kafka:spring-kafka-test") + testImplementation("org.springframework.pulsar:spring-pulsar-cache-provider-caffeine") testImplementation("org.springframework.security:spring-security-test") testImplementation("org.testcontainers:cassandra") testImplementation("org.testcontainers:couchbase") diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 949a08de9797..fe1db65b8b34 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1584,6 +1584,8 @@ bom { group("org.springframework.pulsar") { modules = [ "spring-pulsar", + "spring-pulsar-cache-provider", + "spring-pulsar-cache-provider-caffeine", "spring-pulsar-reactive" ] } From e3d884803e07f4692e1a1aed5f8505f19c421765 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Sep 2023 13:23:59 +0100 Subject: [PATCH 0449/1215] Add Docker Compose support for Neo4j Closes gh-37379 --- .../spring-boot-docker-compose/build.gradle | 1 + ...DockerComposeConnectionDetailsFactory.java | 76 +++++++++++++++++++ .../connection/neo4j/Neo4jEnvironment.java | 57 ++++++++++++++ .../connection/neo4j/package-info.java | 20 +++++ .../main/resources/META-INF/spring.factories | 1 + ...nectionDetailsFactoryIntegrationTests.java | 51 +++++++++++++ .../neo4j/Neo4jEnvironmentTests.java | 59 ++++++++++++++ .../connection/neo4j/neo4j-compose.yaml | 8 ++ .../asciidoc/features/docker-compose.adoc | 3 + 9 files changed, 276 insertions(+) create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jEnvironment.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/neo4j/package-info.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jEnvironmentTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/neo4j/neo4j-compose.yaml diff --git a/spring-boot-project/spring-boot-docker-compose/build.gradle b/spring-boot-project/spring-boot-docker-compose/build.gradle index b0429632ffac..ec1665712dec 100644 --- a/spring-boot-project/spring-boot-docker-compose/build.gradle +++ b/spring-boot-project/spring-boot-docker-compose/build.gradle @@ -18,6 +18,7 @@ dependencies { optional(project(":spring-boot-project:spring-boot-actuator-autoconfigure")) optional("io.r2dbc:r2dbc-spi") optional("org.mongodb:mongodb-driver-core") + optional("org.neo4j.driver:neo4j-java-driver") optional("org.springframework.data:spring-data-r2dbc") testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jDockerComposeConnectionDetailsFactory.java new file mode 100644 index 000000000000..33bd622e23ab --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.neo4j; + +import java.net.URI; + +import org.neo4j.driver.AuthToken; + +import org.springframework.boot.autoconfigure.neo4j.Neo4jConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link Neo4jConnectionDetails} + * for a {@code Neo4j} service. + * + * @author Andy Wilkinson + */ +class Neo4jDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { + + Neo4jDockerComposeConnectionDetailsFactory() { + super("neo4j"); + } + + @Override + protected Neo4jConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new Neo4jDockerComposeConnectionDetails(source.getRunningService()); + } + + /** + * {@link Neo4jConnectionDetails} backed by a {@code Neo4j} {@link RunningService}. + */ + static class Neo4jDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements Neo4jConnectionDetails { + + private static final int BOLT_PORT = 7687; + + private final AuthToken authToken; + + private final URI uri; + + Neo4jDockerComposeConnectionDetails(RunningService service) { + super(service); + Neo4jEnvironment neo4jEnvironment = new Neo4jEnvironment(service.env()); + this.authToken = neo4jEnvironment.getAuthToken(); + this.uri = URI.create("neo4j://%s:%d".formatted(service.host(), service.ports().get(BOLT_PORT))); + } + + @Override + public URI getUri() { + return this.uri; + } + + @Override + public AuthToken getAuthToken() { + return this.authToken; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jEnvironment.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jEnvironment.java new file mode 100644 index 000000000000..59e5a90e9230 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jEnvironment.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.neo4j; + +import java.util.Map; + +import org.neo4j.driver.AuthToken; +import org.neo4j.driver.AuthTokens; + +/** + * Neo4j environment details. + * + * @author Andy Wilkinson + */ +class Neo4jEnvironment { + + private final AuthToken authToken; + + Neo4jEnvironment(Map env) { + this.authToken = parse(env.get("NEO4J_AUTH")); + } + + private AuthToken parse(String neo4jAuth) { + if (neo4jAuth == null) { + return null; + } + if ("none".equals(neo4jAuth)) { + return AuthTokens.none(); + } + if (neo4jAuth.startsWith("neo4j/")) { + return AuthTokens.basic("neo4j", neo4jAuth.substring(6)); + } + throw new IllegalStateException( + "Cannot extract auth token from NEO4J_AUTH environment variable with value '" + neo4jAuth + "'." + + " Value should be 'none' to disable authentication or start with 'neo4j/' to specify" + + " the neo4j user's password"); + } + + AuthToken getAuthToken() { + return this.authToken; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/neo4j/package-info.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/neo4j/package-info.java new file mode 100644 index 000000000000..afea67c3cf5c --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/neo4j/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Auto-configuration for docker compose Neo4j service connections. + */ +package org.springframework.boot.docker.compose.service.connection.neo4j; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories index 80fe3d28309b..f28a89272d9e 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories @@ -15,6 +15,7 @@ org.springframework.boot.docker.compose.service.connection.mariadb.MariaDbR2dbcD org.springframework.boot.docker.compose.service.connection.mongo.MongoDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.mysql.MySqlJdbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.mysql.MySqlR2dbcDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.neo4j.Neo4jDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.oracle.OracleJdbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.oracle.OracleR2dbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.otlp.OpenTelemetryMetricsDockerComposeConnectionDetailsFactory,\ diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..ca95c13efa11 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.neo4j; + +import org.junit.jupiter.api.Test; +import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.Driver; +import org.neo4j.driver.GraphDatabase; + +import org.springframework.boot.autoconfigure.neo4j.Neo4jConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; + +/** + * Integration tests for {@link Neo4jDockerComposeConnectionDetailsFactory}. + * + * @author Andy Wilkinson + */ +class Neo4jDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { + + Neo4jDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("neo4j-compose.yaml", DockerImageNames.neo4j()); + } + + @Test + void runCreatesConnectionDetailsThatCanAccessNeo4j() { + Neo4jConnectionDetails connectionDetails = run(Neo4jConnectionDetails.class); + assertThat(connectionDetails.getAuthToken()).isEqualTo(AuthTokens.basic("neo4j", "secret")); + try (Driver driver = GraphDatabase.driver(connectionDetails.getUri(), connectionDetails.getAuthToken())) { + assertThatNoException().isThrownBy(driver::verifyConnectivity); + } + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jEnvironmentTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jEnvironmentTests.java new file mode 100644 index 000000000000..4cbb02d0b608 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jEnvironmentTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.neo4j; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.neo4j.driver.AuthTokens; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link Neo4jEnvironment}. + * + * @author Andy Wilkinson + */ +class Neo4jEnvironmentTests { + + @Test + void whenNeo4jAuthIsNullThenAuthTokenIsNull() { + Neo4jEnvironment environment = new Neo4jEnvironment(Collections.emptyMap()); + assertThat(environment.getAuthToken()).isNull(); + } + + @Test + void whenNeo4jAuthIsNoneThenAuthTokenIsNone() { + Neo4jEnvironment environment = new Neo4jEnvironment(Map.of("NEO4J_AUTH", "none")); + assertThat(environment.getAuthToken()).isEqualTo(AuthTokens.none()); + } + + @Test + void whenNeo4jAuthIsNeo4jSlashPasswordThenAuthTokenIsBasic() { + Neo4jEnvironment environment = new Neo4jEnvironment(Map.of("NEO4J_AUTH", "neo4j/custom-password")); + assertThat(environment.getAuthToken()).isEqualTo(AuthTokens.basic("neo4j", "custom-password")); + } + + @Test + void whenNeo4jAuthIsNeitherNoneNorNeo4jSlashPasswordEnvironmentCreationThrows() { + assertThatIllegalStateException() + .isThrownBy(() -> new Neo4jEnvironment(Map.of("NEO4J_AUTH", "graphdb/custom-password"))); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/neo4j/neo4j-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/neo4j/neo4j-compose.yaml new file mode 100644 index 000000000000..313cce779274 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/neo4j/neo4j-compose.yaml @@ -0,0 +1,8 @@ +services: + neo4j: + image: '{imageName}' + ports: + - '7687' + environment: + - 'NEO4J_AUTH=neo4j/secret' + diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc index bebc060fa193..10df7be6c44a 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc @@ -76,6 +76,9 @@ The following service connections are currently supported: | `MongoConnectionDetails` | Containers named "mongo" +| `Neo4jConnectionDetails` +| Containers named "neo4j" + | `OtlpMetricsConnectionDetails` | Containers named "otel/opentelemetry-collector-contrib" From 3b15d464554d028027146b7cb5392445dbf1b6f6 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 14 Sep 2023 12:04:37 +0200 Subject: [PATCH 0450/1215] Use virtual threads on Spring Data Redis if enabled Closes gh-35942 --- .../redis/JedisConnectionConfiguration.java | 15 ++++++++++++ .../redis/LettuceConnectionConfiguration.java | 23 +++++++++++++++++++ .../RedisAutoConfigurationJedisTests.java | 23 +++++++++++++++++++ .../redis/RedisAutoConfigurationTests.java | 23 +++++++++++++++++++ 4 files changed, 84 insertions(+) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/JedisConnectionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/JedisConnectionConfiguration.java index 80bcde71166a..c1c6fbb6e811 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/JedisConnectionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/JedisConnectionConfiguration.java @@ -26,12 +26,15 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading; +import org.springframework.boot.autoconfigure.thread.Threading; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.ssl.SslOptions; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisSentinelConfiguration; @@ -69,11 +72,23 @@ class JedisConnectionConfiguration extends RedisConnectionConfiguration { } @Bean + @ConditionalOnThreading(Threading.PLATFORM) JedisConnectionFactory redisConnectionFactory( ObjectProvider builderCustomizers) { return createJedisConnectionFactory(builderCustomizers); } + @Bean + @ConditionalOnThreading(Threading.VIRTUAL) + JedisConnectionFactory redisConnectionFactoryVirtualThreads( + ObjectProvider builderCustomizers) { + JedisConnectionFactory factory = createJedisConnectionFactory(builderCustomizers); + SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor("redis-"); + executor.setVirtualThreads(true); + factory.setExecutor(executor); + return factory; + } + private JedisConnectionFactory createJedisConnectionFactory( ObjectProvider builderCustomizers) { JedisClientConfiguration clientConfiguration = getJedisClientConfiguration(builderCustomizers); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java index 0f88d9029684..4a765d1e2004 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java @@ -33,13 +33,16 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading; import org.springframework.boot.autoconfigure.data.redis.RedisProperties.Lettuce.Cluster.Refresh; import org.springframework.boot.autoconfigure.data.redis.RedisProperties.Pool; +import org.springframework.boot.autoconfigure.thread.Threading; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.ssl.SslOptions; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisSentinelConfiguration; @@ -83,9 +86,29 @@ DefaultClientResources lettuceClientResources(ObjectProvider builderCustomizers, ClientResources clientResources) { + return createConnectionFactory(builderCustomizers, clientResources); + } + + @Bean + @ConditionalOnMissingBean(RedisConnectionFactory.class) + @ConditionalOnThreading(Threading.VIRTUAL) + LettuceConnectionFactory redisConnectionFactoryVirtualThreads( + ObjectProvider builderCustomizers, + ClientResources clientResources) { + LettuceConnectionFactory factory = createConnectionFactory(builderCustomizers, clientResources); + SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor("redis-"); + executor.setVirtualThreads(true); + factory.setExecutor(executor); + return factory; + } + + private LettuceConnectionFactory createConnectionFactory( + ObjectProvider builderCustomizers, + ClientResources clientResources) { LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(builderCustomizers, clientResources, getProperties().getLettuce().getPool()); return createLettuceConnectionFactory(clientConfig); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java index 164e222fd800..2cbf2d5b0679 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java @@ -19,14 +19,18 @@ import java.time.Duration; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.assertj.SimpleAsyncTaskExecutorAssert; import org.springframework.boot.testsupport.classpath.ClassPathExclusions; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisClientConfigurationBuilder; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; @@ -270,6 +274,25 @@ void testRedisConfigurationWithSslDisabledAndBundle() { }); } + @Test + void shouldUsePlatformThreadsByDefault() { + this.contextRunner.run((context) -> { + JedisConnectionFactory factory = context.getBean(JedisConnectionFactory.class); + assertThat(factory).extracting("executor").isNull(); + }); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void shouldUseVirtualThreadsIfEnabled() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> { + JedisConnectionFactory factory = context.getBean(JedisConnectionFactory.class); + SimpleAsyncTaskExecutor executor = (SimpleAsyncTaskExecutor) ReflectionTestUtils.getField(factory, + "executor"); + SimpleAsyncTaskExecutorAssert.assertThat(executor).usesVirtualThreads(); + }); + } + private String getUserName(JedisConnectionFactory factory) { return ReflectionTestUtils.invokeMethod(factory, "getRedisUsername"); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java index 3e6ab344b29b..387ffe7da559 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java @@ -31,6 +31,8 @@ import io.lettuce.core.tracing.Tracing; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.data.redis.RedisProperties.Pool; @@ -38,8 +40,10 @@ import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.boot.testsupport.assertj.SimpleAsyncTaskExecutorAssert; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisNode; @@ -582,6 +586,25 @@ void testRedisConfigurationWithSslDisabledBundle() { }); } + @Test + void shouldUsePlatformThreadsByDefault() { + this.contextRunner.run((context) -> { + LettuceConnectionFactory factory = context.getBean(LettuceConnectionFactory.class); + assertThat(factory).extracting("executor").isNull(); + }); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void shouldUseVirtualThreadsIfEnabled() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> { + LettuceConnectionFactory factory = context.getBean(LettuceConnectionFactory.class); + SimpleAsyncTaskExecutor executor = (SimpleAsyncTaskExecutor) ReflectionTestUtils.getField(factory, + "executor"); + SimpleAsyncTaskExecutorAssert.assertThat(executor).usesVirtualThreads(); + }); + } + private ContextConsumer assertClientOptions( Class expectedType, Consumer options) { return (context) -> { From 0ba6af9eb22ea9ce27c91cf57f5d5df2b99c4169 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 14 Sep 2023 15:48:53 +0200 Subject: [PATCH 0451/1215] Upgrade to Flyway 9.22.1 Closes gh-37389 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index fe1db65b8b34..21e7f4919a4d 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -285,7 +285,7 @@ bom { ] } } - library("Flyway", "9.22.0") { + library("Flyway", "9.22.1") { group("org.flywaydb") { modules = [ "flyway-core", From e7089eaa0e2333b7efc2df08daa997b08afcb8a8 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 14 Sep 2023 15:48:58 +0200 Subject: [PATCH 0452/1215] Upgrade to Groovy 4.0.15 Closes gh-37390 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 21e7f4919a4d..104928afc126 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -338,7 +338,7 @@ bom { ] } } - library("Groovy", "4.0.14") { + library("Groovy", "4.0.15") { group("org.apache.groovy") { imports = [ "groovy-bom" From b83655a12226210f9074eabe07cf77d423c839e7 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 14 Sep 2023 15:49:03 +0200 Subject: [PATCH 0453/1215] Upgrade to Jakarta XML Bind 4.0.1 Closes gh-37391 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 104928afc126..6d5700f0d2d4 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -584,7 +584,7 @@ bom { ] } } - library("Jakarta XML Bind", "4.0.0") { + library("Jakarta XML Bind", "4.0.1") { group("jakarta.xml.bind") { modules = [ "jakarta.xml.bind-api" From 0535f1c762c0df3f7ed091830fac13475d16eb51 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 14 Sep 2023 15:49:03 +0200 Subject: [PATCH 0454/1215] Upgrade to Spring Retry 2.0.3 Closes gh-37281 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 6d5700f0d2d4..d646fb20bb1a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1598,7 +1598,7 @@ bom { ] } } - library("Spring Retry", "2.0.3-SNAPSHOT") { + library("Spring Retry", "2.0.3") { considerSnapshots() group("org.springframework.retry") { modules = [ From defe77895af131e204b5172d059510236e3b05fd Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 14 Sep 2023 15:57:03 +0200 Subject: [PATCH 0455/1215] Upgrade to Spring Framework 6.1.0-M5 Closes gh-37231 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 59a5f9a7ef57..8f8637903f6f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 kotlinVersion=1.9.10 nativeBuildToolsVersion=0.9.26 -springFrameworkVersion=6.1.0-SNAPSHOT +springFrameworkVersion=6.1.0-M5 tomcatVersion=10.1.13 kotlin.stdlib.default.dependency=false From 73c25d7156ae0f6bc42f21f5c9466d80fca4550a Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 15 Sep 2023 10:41:37 +0200 Subject: [PATCH 0456/1215] Provide RestClientSsl as a bean Closes gh-37400 --- .../web/client/RestClientAutoConfiguration.java | 12 +++++++++++- .../boot/autoconfigure/web/client/RestClientSsl.java | 2 +- .../web/client/RestClientAutoConfigurationTests.java | 7 +++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java index 31b7f3924415..6198b70bfaee 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfiguration.java @@ -19,10 +19,13 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.web.client.ClientHttpRequestFactories; import org.springframework.boot.web.client.ClientHttpRequestFactorySettings; import org.springframework.boot.web.client.RestClientCustomizer; @@ -44,7 +47,7 @@ * @author Moritz Halbritter * @since 3.2.0 */ -@AutoConfiguration(after = HttpMessageConvertersAutoConfiguration.class) +@AutoConfiguration(after = { HttpMessageConvertersAutoConfiguration.class, SslAutoConfiguration.class }) @ConditionalOnClass(RestClient.class) @Conditional(NotReactiveWebApplicationCondition.class) public class RestClientAutoConfiguration { @@ -57,6 +60,13 @@ HttpMessageConvertersRestClientCustomizer httpMessageConvertersRestClientCustomi return new HttpMessageConvertersRestClientCustomizer(messageConverters.getIfUnique()); } + @Bean + @ConditionalOnMissingBean(RestClientSsl.class) + @ConditionalOnBean(SslBundles.class) + AutoConfiguredRestClientSsl restClientSsl(SslBundles sslBundles) { + return new AutoConfiguredRestClientSsl(sslBundles); + } + @Bean @ConditionalOnMissingBean RestClientBuilderConfigurer restClientBuilderConfigurer(ObjectProvider customizerProvider) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientSsl.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientSsl.java index bcf97ddca19e..fd892efb4326 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientSsl.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestClientSsl.java @@ -32,7 +32,7 @@ * Typically used as follows:
      * @Bean
      * public MyBean myBean(RestClient.Builder restClientBuilder, RestClientSsl ssl) {
    - *     RestClient restClientrestClient= restClientBuilder.apply(ssl.forBundle("mybundle")).build();
    + *     RestClient restClientrestClient= restClientBuilder.apply(ssl.fromBundle("mybundle")).build();
      *     return new MyBean(webClient);
      * }
      * 
    NOTE: Apply SSL configuration will replace any previously diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java index d2fb90dbf9a0..576d6e45808b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestClientAutoConfigurationTests.java @@ -23,6 +23,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.web.client.RestClientCustomizer; import org.springframework.boot.web.codec.CodecCustomizer; @@ -59,6 +60,12 @@ void shouldSupplyBeans() { }); } + @Test + void shouldSupplyRestClientSslIfSslBundlesIsThere() { + this.contextRunner.withBean(SslBundles.class, () -> mock(SslBundles.class)) + .run((context) -> assertThat(context).hasSingleBean(RestClientSsl.class)); + } + @Test void shouldCreateBuilder() { this.contextRunner.run((context) -> { From 8f4ccb0535fe0ac4f893f93bf416ee9aea000866 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Fri, 15 Sep 2023 00:50:04 +0900 Subject: [PATCH 0457/1215] Polish See gh-37393 --- .../rsocket/RSocketProperties.java | 2 +- .../TaskSchedulingAutoConfigurationTests.java | 8 ++-- .../task-execution-and-scheduling.adoc | 2 +- .../task/SimpleAsyncTaskSchedulerBuilder.java | 41 ++++++++----------- .../SimpleAsyncTaskSchedulerBuilderTests.java | 8 +--- 5 files changed, 24 insertions(+), 37 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketProperties.java index dd16caad7198..10096dfddad4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketProperties.java @@ -130,7 +130,7 @@ public Spec getSpec() { public static class Spec { /** - * Sub-protocol to use in websocket handshake signature. + * Sub-protocols to use in websocket handshake signature. */ private String protocols; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java index 5d0cd515e889..ca211b30b965 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java @@ -26,6 +26,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import org.assertj.core.api.InstanceOfAssertFactories; import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledForJreRange; @@ -49,7 +50,6 @@ import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.scheduling.config.ScheduledTaskRegistrar; -import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -161,9 +161,9 @@ void simpleAsyncTaskSchedulerBuilderShouldApplyCustomizers() { .run((context) -> { assertThat(context).hasSingleBean(SimpleAsyncTaskSchedulerBuilder.class); SimpleAsyncTaskSchedulerBuilder builder = context.getBean(SimpleAsyncTaskSchedulerBuilder.class); - Set customizers = (Set) ReflectionTestUtils - .getField(builder, "customizers"); - assertThat(customizers).as("SimpleAsyncTaskSchedulerBuilder.customizers").contains(customizer); + assertThat(builder).extracting("customizers") + .asInstanceOf(InstanceOfAssertFactories.collection(SimpleAsyncTaskSchedulerCustomizer.class)) + .containsExactly(customizer); }); } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc index 1b5e8130d5d8..547be8751408 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/task-execution-and-scheduling.adoc @@ -36,7 +36,7 @@ Those default settings can be fine-tuned using the `spring.task.execution` names This changes the thread pool to use a bounded queue so that when the queue is full (100 tasks), the thread pool increases to maximum 16 threads. Shrinking of the pool is more aggressive as threads are reclaimed when they are idle for 10 seconds (rather than 60 seconds by default). -A scheduler can also be auto-configured if need to be associated to scheduled task execution (using `@EnableScheduling` for instance). +A scheduler can also be auto-configured if it needs to be associated with scheduled task execution (using `@EnableScheduling` for instance). When virtual threads are enabled (using Java 21+ and configprop:spring.threads.virtual.enabled[] set to `true`) this will be a `SimpleAsyncTaskScheduler` that uses virtual threads. Otherwise, it will be a `ThreadPoolTaskScheduler` with sensible defaults. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilder.java index 6c478250e1c8..e5dab60b1e80 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilder.java @@ -49,10 +49,7 @@ public class SimpleAsyncTaskSchedulerBuilder { private final Set customizers; public SimpleAsyncTaskSchedulerBuilder() { - this.threadNamePrefix = null; - this.customizers = null; - this.concurrencyLimit = null; - this.virtualThreads = null; + this(null, null, null, null); } private SimpleAsyncTaskSchedulerBuilder(String threadNamePrefix, Integer concurrencyLimit, Boolean virtualThreads, @@ -94,11 +91,10 @@ public SimpleAsyncTaskSchedulerBuilder virtualThreads(Boolean virtualThreads) { } /** - * Set the {@link SimpleAsyncTaskSchedulerCustomizer - * threadPoolTaskSchedulerCustomizers} that should be applied to the - * {@link SimpleAsyncTaskScheduler}. Customizers are applied in the order that they - * were added after builder configuration has been applied. Setting this value will - * replace any previously configured customizers. + * Set the {@link SimpleAsyncTaskSchedulerCustomizer customizers} that should be + * applied to the {@link SimpleAsyncTaskScheduler}. Customizers are applied in the + * order that they were added after builder configuration has been applied. Setting + * this value will replace any previously configured customizers. * @param customizers the customizers to set * @return a new builder instance * @see #additionalCustomizers(SimpleAsyncTaskSchedulerCustomizer...) @@ -109,14 +105,13 @@ public SimpleAsyncTaskSchedulerBuilder customizers(SimpleAsyncTaskSchedulerCusto } /** - * Set the {@link SimpleAsyncTaskSchedulerCustomizer - * threadPoolTaskSchedulerCustomizers} that should be applied to the - * {@link SimpleAsyncTaskScheduler}. Customizers are applied in the order that they - * were added after builder configuration has been applied. Setting this value will - * replace any previously configured customizers. + * Set the {@link SimpleAsyncTaskSchedulerCustomizer customizers} that should be + * applied to the {@link SimpleAsyncTaskScheduler}. Customizers are applied in the + * order that they were added after builder configuration has been applied. Setting + * this value will replace any previously configured customizers. * @param customizers the customizers to set * @return a new builder instance - * @see #additionalCustomizers(SimpleAsyncTaskSchedulerCustomizer...) + * @see #additionalCustomizers(Iterable) */ public SimpleAsyncTaskSchedulerBuilder customizers( Iterable customizers) { @@ -126,10 +121,9 @@ public SimpleAsyncTaskSchedulerBuilder customizers( } /** - * Add {@link SimpleAsyncTaskSchedulerCustomizer threadPoolTaskSchedulerCustomizers} - * that should be applied to the {@link SimpleAsyncTaskScheduler}. Customizers are - * applied in the order that they were added after builder configuration has been - * applied. + * Add {@link SimpleAsyncTaskSchedulerCustomizer customizers} that should be applied + * to the {@link SimpleAsyncTaskScheduler}. Customizers are applied in the order that + * they were added after builder configuration has been applied. * @param customizers the customizers to add * @return a new builder instance * @see #customizers(SimpleAsyncTaskSchedulerCustomizer...) @@ -140,13 +134,12 @@ public SimpleAsyncTaskSchedulerBuilder additionalCustomizers(SimpleAsyncTaskSche } /** - * Add {@link SimpleAsyncTaskSchedulerCustomizer threadPoolTaskSchedulerCustomizers} - * that should be applied to the {@link SimpleAsyncTaskScheduler}. Customizers are - * applied in the order that they were added after builder configuration has been - * applied. + * Add {@link SimpleAsyncTaskSchedulerCustomizer customizers} that should be applied + * to the {@link SimpleAsyncTaskScheduler}. Customizers are applied in the order that + * they were added after builder configuration has been applied. * @param customizers the customizers to add * @return a new builder instance - * @see #customizers(SimpleAsyncTaskSchedulerCustomizer...) + * @see #customizers(Iterable) */ public SimpleAsyncTaskSchedulerBuilder additionalCustomizers( Iterable customizers) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilderTests.java index 440a42c0d6a4..9b4e7da12c88 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilderTests.java @@ -16,7 +16,6 @@ package org.springframework.boot.task; -import java.lang.reflect.Field; import java.util.Collections; import java.util.Set; @@ -25,7 +24,6 @@ import org.junit.jupiter.api.condition.JRE; import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler; -import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -59,11 +57,7 @@ void concurrencyLimitShouldApply() { @EnabledForJreRange(min = JRE.JAVA_21) void virtualThreadsShouldApply() { SimpleAsyncTaskScheduler scheduler = this.builder.virtualThreads(true).build(); - Field field = ReflectionUtils.findField(SimpleAsyncTaskScheduler.class, "virtualThreadDelegate"); - assertThat(field).as("SimpleAsyncTaskScheduler.virtualThreadDelegate").isNotNull(); - field.setAccessible(true); - Object virtualThreadDelegate = ReflectionUtils.getField(field, scheduler); - assertThat(virtualThreadDelegate).as("SimpleAsyncTaskScheduler.virtualThreadDelegate").isNotNull(); + assertThat(scheduler).extracting("virtualThreadDelegate").isNotNull(); } @Test From 809d92343c3e06b3043c2c15c9dc331cd0d03c7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 15 Sep 2023 15:35:12 +0200 Subject: [PATCH 0458/1215] Start building against Spring WS 4.0.6 snapshots See gh-37427 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d646fb20bb1a..b269181414ae 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1626,7 +1626,7 @@ bom { ] } } - library("Spring WS", "4.0.5") { + library("Spring WS", "4.0.6-SNAPSHOT") { considerSnapshots() group("org.springframework.ws") { imports = [ From 0371f3f8810474ecc84f517f3fb63c8ed6d27eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 15 Sep 2023 15:43:29 +0200 Subject: [PATCH 0459/1215] Upgrade to Byte Buddy 1.14.8 Closes gh-37429 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index b269181414ae..7f6ebee5ebfa 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -123,7 +123,7 @@ bom { ] } } - library("Byte Buddy", "1.14.7") { + library("Byte Buddy", "1.14.8") { group("net.bytebuddy") { modules = [ "byte-buddy", From 6c4dd3b598d035d155bb2e01f3b2448d63595b1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 15 Sep 2023 15:43:35 +0200 Subject: [PATCH 0460/1215] Upgrade to Native Build Tools Plugin 0.9.27 Closes gh-37430 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8f8637903f6f..3fd92d3868c0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ org.gradle.parallel=true org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 kotlinVersion=1.9.10 -nativeBuildToolsVersion=0.9.26 +nativeBuildToolsVersion=0.9.27 springFrameworkVersion=6.1.0-M5 tomcatVersion=10.1.13 From 14d28691acb1fd49ce75aa8def69f5ba040fab93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 15 Sep 2023 15:43:41 +0200 Subject: [PATCH 0461/1215] Upgrade to Pooled JMS 3.1.3 Closes gh-37431 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 7f6ebee5ebfa..8860536fa07b 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1115,7 +1115,7 @@ bom { ] } } - library("Pooled JMS", "3.1.2") { + library("Pooled JMS", "3.1.3") { group("org.messaginghub") { modules = [ "pooled-jms" From b00e6a832f4781c7e4a75152f36747c0c5979981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 15 Sep 2023 15:43:41 +0200 Subject: [PATCH 0462/1215] Upgrade to Spring Data Bom 2023.1.0-M3 Closes gh-37351 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 8860536fa07b..6eb6b7fb32d6 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1517,7 +1517,7 @@ bom { ] } } - library("Spring Data Bom", "2023.1.0-SNAPSHOT") { + library("Spring Data Bom", "2023.1.0-M3") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From e5faa5400647a7f43ee6dbf1217d9e4b6c3d8629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 15 Sep 2023 15:43:41 +0200 Subject: [PATCH 0463/1215] Upgrade to Spring LDAP 3.2.0-M3 Closes gh-37235 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 6eb6b7fb32d6..466d630e58ff 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1569,7 +1569,7 @@ bom { ] } } - library("Spring LDAP", "3.2.0-SNAPSHOT") { + library("Spring LDAP", "3.2.0-M3") { considerSnapshots() group("org.springframework.ldap") { modules = [ From d6daf870748a5a997f4be3631f39d8b767637d64 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 15 Sep 2023 15:53:27 +0200 Subject: [PATCH 0464/1215] Update Antora reference documentation links This commit updates the base URLs for reference documentations when the relevant Spring project is now being published with Antora. This commit updates the following projects: * Spring Framework * Spring Integration * Spring for GraphQL * Spring Security, including Authorization Server * Spring Batch * Spring Data JPA Closes gh-37428 --- .../spring-boot-docs/build.gradle | 18 +++++++++--------- .../src/docs/asciidoc/actuator/metrics.adoc | 4 ++-- .../src/docs/asciidoc/attributes.adoc | 12 ++++++------ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/build.gradle b/spring-boot-project/spring-boot-docs/build.gradle index 0a92048ce5ce..818d34df172f 100644 --- a/spring-boot-project/spring-boot-docs/build.gradle +++ b/spring-boot-project/spring-boot-docs/build.gradle @@ -313,9 +313,9 @@ tasks.withType(org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask) { } doFirst { def versionConstraints = dependencyVersions.versionConstraints - def securityVersion = versionConstraints["org.springframework.security:spring-security-core"] - if (securityVersion.endsWith("-SNAPSHOT")) { - securityVersion = securityVersion.substring(0, securityVersion.length() - "-SNAPSHOT".length()) + def toAntoraVersion = version -> { + def formatted = version.split("\\.").take(2).join('.') + return version.endsWith("-SNAPSHOT") ? formatted + "-SNAPSHOT" : formatted } attributes "hibernate-version": versionConstraints["org.hibernate.orm:hibernate-core"].split("\\.").take(2).join('.'), "jetty-version": versionConstraints["org.eclipse.jetty:jetty-server"], @@ -323,7 +323,7 @@ tasks.withType(org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask) { "lettuce-version": versionConstraints["io.lettuce:lettuce-core"], "native-build-tools-version": nativeBuildToolsVersion, "spring-amqp-version": versionConstraints["org.springframework.amqp:spring-amqp"], - "spring-batch-version": versionConstraints["org.springframework.batch:spring-batch-core"], + "spring-batch-version": toAntoraVersion(versionConstraints["org.springframework.batch:spring-batch-core"]), "spring-boot-version": project.version, "spring-data-commons-version": versionConstraints["org.springframework.data:spring-data-commons"], "spring-data-couchbase-version": versionConstraints["org.springframework.data:spring-data-couchbase"], @@ -334,13 +334,13 @@ tasks.withType(org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask) { "spring-data-neo4j-version": versionConstraints["org.springframework.data:spring-data-neo4j"], "spring-data-r2dbc-version": versionConstraints["org.springframework.data:spring-data-r2dbc"], "spring-data-rest-version": versionConstraints["org.springframework.data:spring-data-rest-core"], - "spring-framework-version": versionConstraints["org.springframework:spring-core"], - "spring-graphql-version": versionConstraints["org.springframework.graphql:spring-graphql"], - "spring-integration-version": versionConstraints["org.springframework.integration:spring-integration-core"], + "spring-framework-version": toAntoraVersion(versionConstraints["org.springframework:spring-core"]), + "spring-graphql-version": toAntoraVersion(versionConstraints["org.springframework.graphql:spring-graphql"]), + "spring-integration-version": toAntoraVersion(versionConstraints["org.springframework.integration:spring-integration-core"]), "spring-kafka-version": versionConstraints["org.springframework.kafka:spring-kafka"], "spring-pulsar-version": versionConstraints["org.springframework.pulsar:spring-pulsar"], - "spring-security-version": securityVersion, - "spring-authorization-server-version": versionConstraints["org.springframework.security:spring-security-oauth2-authorization-server"], + "spring-security-version": toAntoraVersion(versionConstraints["org.springframework.security:spring-security-core"]), + "spring-authorization-server-version": toAntoraVersion(versionConstraints["org.springframework.security:spring-security-oauth2-authorization-server"]), "spring-webservices-version": versionConstraints["org.springframework.ws:spring-ws-core"], "tomcat-version": tomcatVersion.split("\\.").take(2).join('.'), "remote-spring-application-output": runRemoteSpringApplicationExample.outputs.files.singleFile, diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc index 96d671570bc2..853ca9dc4b66 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc @@ -870,7 +870,7 @@ See the {spring-batch-docs}monitoring-and-metrics.html[Spring Batch reference do [[actuator.metrics.supported.spring-graphql]] ==== Spring GraphQL Metrics -See the {spring-graphql-docs}[Spring GraphQL reference documentation]. +See the {spring-graphql-docs}/observability.html[Spring GraphQL reference documentation]. @@ -951,7 +951,7 @@ Auto-configuration enables the instrumentation of all available RabbitMQ connect [[actuator.metrics.supported.spring-integration]] ==== Spring Integration Metrics -Spring Integration automatically provides {spring-integration-docs}system-management.html#micrometer-integration[Micrometer support] whenever a `MeterRegistry` bean is available. +Spring Integration automatically provides {spring-integration-docs}metrics.html#micrometer-integration[Micrometer support] whenever a `MeterRegistry` bean is available. Metrics are published under the `spring.integration.` meter name. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc index f8aab54bfd24..92a2374e94c6 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc @@ -56,7 +56,7 @@ :spring-amqp-api: https://docs.spring.io/spring-amqp/docs/{spring-amqp-version}/api/org/springframework/amqp :spring-batch: https://spring.io/projects/spring-batch :spring-batch-api: https://docs.spring.io/spring-batch/docs/{spring-batch-version}/api/org/springframework/batch -:spring-batch-docs: https://docs.spring.io/spring-batch/docs/{spring-batch-version}/reference/html/ +:spring-batch-docs: https://docs.spring.io/spring-batch/reference/{spring-batch-version}/ :spring-data: https://spring.io/projects/spring-data :spring-data-cassandra: https://spring.io/projects/spring-data-cassandra :spring-data-commons-api: https://docs.spring.io/spring-data/commons/docs/{spring-data-commons-version}/api/org/springframework/data @@ -70,7 +70,7 @@ :spring-data-geode: https://spring.io/projects/spring-data-geode :spring-data-jpa: https://spring.io/projects/spring-data-jpa :spring-data-jpa-api: https://docs.spring.io/spring-data/jpa/docs/{spring-data-jpa-version}/api/org/springframework/data/jpa -:spring-data-jpa-docs: https://docs.spring.io/spring-data/jpa/docs/{spring-data-jpa-version}/reference/html +:spring-data-jpa-docs: https://docs.spring.io/spring-data/jpa/reference/{spring-data-jpa-version}/ :spring-data-jdbc-docs: https://docs.spring.io/spring-data/jdbc/docs/{spring-data-jdbc-version}/reference/html/ :spring-data-ldap: https://spring.io/projects/spring-data-ldap :spring-data-mongodb: https://spring.io/projects/spring-data-mongodb @@ -83,19 +83,19 @@ :spring-data-rest-api: https://docs.spring.io/spring-data/rest/docs/{spring-data-rest-version}/api/org/springframework/data/rest :spring-framework: https://spring.io/projects/spring-framework :spring-framework-api: https://docs.spring.io/spring-framework/docs/{spring-framework-version}/javadoc-api/org/springframework -:spring-framework-docs: https://docs.spring.io/spring-framework/docs/{spring-framework-version}/reference/html +:spring-framework-docs: https://docs.spring.io/spring-framework/reference/{spring-framework-version} :spring-graphql: https://spring.io/projects/spring-graphql :spring-graphql-api: https://docs.spring.io/spring-graphql/docs/{spring-graphql-version}/api/ -:spring-graphql-docs: https://docs.spring.io/spring-graphql/docs/{spring-graphql-version}/reference/html/ +:spring-graphql-docs: https://docs.spring.io/spring-graphql/reference/{spring-graphql-version}/ :spring-integration: https://spring.io/projects/spring-integration -:spring-integration-docs: https://docs.spring.io/spring-integration/docs/{spring-integration-version}/reference/html/ +:spring-integration-docs: https://docs.spring.io/spring-integration/reference/{spring-integration-version}/ :spring-kafka-docs: https://docs.spring.io/spring-kafka/docs/{spring-kafka-version}/reference/html/ :spring-pulsar-docs: https://docs.spring.io/spring-pulsar/docs/{spring-pulsar-version}/reference/html/ :spring-restdocs: https://spring.io/projects/spring-restdocs :spring-security: https://spring.io/projects/spring-security :spring-security-docs: https://docs.spring.io/spring-security/reference/{spring-security-version} :spring-authorization-server: https://spring.io/projects/spring-authorization-server -:spring-authorization-server-docs: https://docs.spring.io/spring-authorization-server/docs/{spring-authorization-server-version}/reference/html +:spring-authorization-server-docs: https://docs.spring.io/spring-authorization-server/reference/{spring-authorization-server-version}/ :spring-session: https://spring.io/projects/spring-session :spring-webservices-docs: https://docs.spring.io/spring-ws/docs/{spring-webservices-version}/reference/html/ :ant-docs: https://ant.apache.org/manual From 0fc97e9315304e99b68c54ca5da8a98e1e2397e7 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 15 Sep 2023 16:28:06 +0200 Subject: [PATCH 0465/1215] Auto-configure ObservationRegistry on JmsTemplate Spring Boot auto-configures both a `JmsTemplate` and a `JmsMessagingTemplate`. As of Spring Framework 6.2, JMS has observability support when publishing messages. This commit creates a bean post-processor that configures an `ObservationRegistry` on the template, if the registry is present. Closes gh-37388 --- ...sTemplateObservationAutoConfiguration.java | 72 +++++++++++++++++++ .../observation/jms/package-info.java | 20 ++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + ...lateObservationAutoConfigurationTests.java | 70 ++++++++++++++++++ .../src/docs/asciidoc/actuator/metrics.adoc | 7 ++ 5 files changed, 170 insertions(+) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/jms/package-info.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfigurationTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfiguration.java new file mode 100644 index 000000000000..faa7db23e62d --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfiguration.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.observation.jms; + +import io.micrometer.core.instrument.binder.jms.JmsPublishObservationContext; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; +import jakarta.jms.Message; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.jms.core.JmsTemplate; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for instrumenting + * {@link JmsTemplate} beans for Observability. + * + * @author Brian Clozel + * @since 3.2.0 + */ +@AutoConfiguration(after = { JmsAutoConfiguration.class, ObservationAutoConfiguration.class }) +@ConditionalOnBean({ ObservationRegistry.class, JmsTemplate.class }) +@ConditionalOnClass({ Observation.class, Message.class, JmsTemplate.class, JmsPublishObservationContext.class }) +public class JmsTemplateObservationAutoConfiguration { + + @Bean + static JmsTemplateObservationPostProcessor jmsTemplateObservationPostProcessor( + ObjectProvider observationRegistry) { + return new JmsTemplateObservationPostProcessor(observationRegistry); + } + + static class JmsTemplateObservationPostProcessor implements BeanPostProcessor { + + private final ObjectProvider observationRegistry; + + JmsTemplateObservationPostProcessor(ObjectProvider observationRegistry) { + this.observationRegistry = observationRegistry; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof JmsTemplate jmsTemplate) { + this.observationRegistry.ifAvailable(jmsTemplate::setObservationRegistry); + } + return bean; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/jms/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/jms/package-info.java new file mode 100644 index 000000000000..417a73aed67f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/jms/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Auto-configuration for JMS observations. + */ +package org.springframework.boot.actuate.autoconfigure.observation.jms; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 79c100062557..9ce3a9092f86 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -70,6 +70,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.export.statsd.StatsdMetri org.springframework.boot.actuate.autoconfigure.metrics.export.wavefront.WavefrontMetricsExportAutoConfiguration org.springframework.boot.actuate.autoconfigure.observation.batch.BatchObservationAutoConfiguration org.springframework.boot.actuate.autoconfigure.observation.graphql.GraphQlObservationAutoConfiguration +org.springframework.boot.actuate.autoconfigure.observation.jms.JmsTemplateObservationAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.integration.IntegrationMetricsAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.jersey.JerseyServerMetricsAutoConfiguration diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfigurationTests.java new file mode 100644 index 000000000000..3abd6c1d64d2 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfigurationTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.observation.jms; + +import jakarta.jms.ConnectionFactory; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.jms.core.JmsTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link JmsTemplateObservationAutoConfiguration}. + * + * @author Brian Clozel + */ +class JmsTemplateObservationAutoConfigurationTests { + + ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(JmsAutoConfiguration.class, ObservationAutoConfiguration.class, + JmsTemplateObservationAutoConfiguration.class)) + .withUserConfiguration(JmsConnectionConfiguration.class); + + @Test + void shouldConfigureObservationRegistryOnTemplate() { + this.contextRunner.run((context) -> { + JmsTemplate jmsTemplate = context.getBean(JmsTemplate.class); + assertThat(jmsTemplate).extracting("observationRegistry").isNotNull(); + }); + } + + @Test + void shouldBackOffWhenMircrometerCoreIsNotPresent() { + this.contextRunner.withClassLoader(new FilteredClassLoader("io.micrometer.core")).run((context) -> { + JmsTemplate jmsTemplate = context.getBean(JmsTemplate.class); + assertThat(jmsTemplate).extracting("observationRegistry").isNull(); + }); + } + + static class JmsConnectionConfiguration { + + @Bean + ConnectionFactory connectionFactory() { + return mock(ConnectionFactory.class); + } + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc index 853ca9dc4b66..14d9c035acc9 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc @@ -739,6 +739,13 @@ Auto-configuration enables the instrumentation of all available `ThreadPoolTaskE Metrics are tagged by the name of the executor, which is derived from the bean name. +[[actuator.metrics.supported.jms]] +==== JMS Metrics +Auto-configuration enables the instrumentation of all available `JmsTemplate` beans. +`JmsMessagingTemplate` instances built with instrumented `JmsTemplate` beans will also record observations. +See the {spring-framework-docs}/integration/observability.html#observability.jms.publish[Spring Framework reference documentation for more information on produced observations]. + + [[actuator.metrics.supported.spring-mvc]] ==== Spring MVC Metrics From 6094212217c1a6e70c1b2182f1ef57d26be7eb20 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 Sep 2023 18:17:10 +0100 Subject: [PATCH 0466/1215] Defer accessing loop resources until web server start Closes gh-37209 --- .../netty/NettyReactiveWebServerFactory.java | 13 ++----- .../web/embedded/netty/NettyWebServer.java | 36 +++++++++++++++++++ .../NettyReactiveWebServerFactoryTests.java | 21 ++++++++++- 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java index f351da622fd4..306077ffbdcf 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java @@ -27,7 +27,6 @@ import reactor.netty.http.HttpProtocol; import reactor.netty.http.server.HttpServer; -import reactor.netty.resources.LoopResources; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory; @@ -78,7 +77,7 @@ public WebServer getWebServer(HttpHandler httpHandler) { NettyWebServer createNettyWebServer(HttpServer httpServer, ReactorHttpHandlerAdapter handlerAdapter, Duration lifecycleTimeout, Shutdown shutdown) { - return new NettyWebServer(httpServer, handlerAdapter, lifecycleTimeout, shutdown); + return new NettyWebServer(httpServer, handlerAdapter, lifecycleTimeout, shutdown, this.resourceFactory); } /** @@ -158,15 +157,7 @@ public Shutdown getShutdown() { } private HttpServer createHttpServer() { - HttpServer server = HttpServer.create(); - if (this.resourceFactory != null) { - LoopResources resources = this.resourceFactory.getLoopResources(); - Assert.notNull(resources, "No LoopResources: is ReactorResourceFactory not initialized yet?"); - server = server.runOn(resources).bindAddress(this::getListenAddress); - } - else { - server = server.bindAddress(this::getListenAddress); - } + HttpServer server = HttpServer.create().bindAddress(this::getListenAddress); if (Ssl.isEnabled(getSsl())) { server = customizeSslConfiguration(server); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java index f21f47a70081..208ce2a2e75d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java @@ -35,6 +35,7 @@ import reactor.netty.http.server.HttpServerRequest; import reactor.netty.http.server.HttpServerResponse; import reactor.netty.http.server.HttpServerRoutes; +import reactor.netty.resources.LoopResources; import org.springframework.boot.web.server.GracefulShutdownCallback; import org.springframework.boot.web.server.GracefulShutdownResult; @@ -42,6 +43,7 @@ import org.springframework.boot.web.server.Shutdown; import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.server.WebServerException; +import org.springframework.http.client.reactive.ReactorResourceFactory; import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; import org.springframework.util.Assert; @@ -74,12 +76,40 @@ public class NettyWebServer implements WebServer { private final GracefulShutdown gracefulShutdown; + private final ReactorResourceFactory resourceFactory; + private List routeProviders = Collections.emptyList(); private volatile DisposableServer disposableServer; + /** + * Creates a new {@code NettyWebServer} instance. + * @param httpServer the HTTP server + * @param handlerAdapter the handler adapter + * @param lifecycleTimeout the lifecycle timeout, may be {@code null} + * @param shutdown the shutdown, may be {@code null} + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of + * {@link #NettyWebServer(HttpServer, ReactorHttpHandlerAdapter, Duration, Shutdown, ReactorResourceFactory)} + */ + @Deprecated(since = "3.2.0", forRemoval = true) public NettyWebServer(HttpServer httpServer, ReactorHttpHandlerAdapter handlerAdapter, Duration lifecycleTimeout, Shutdown shutdown) { + this(httpServer, handlerAdapter, lifecycleTimeout, shutdown, null); + } + + /** + * Creates a new {@code NettyWebServer} instance. + * @param httpServer the HTTP server + * @param handlerAdapter the handler adapter + * @param lifecycleTimeout the lifecycle timeout, may be {@code null} + * @param shutdown the shutdown, may be {@code null} + * @param resourceFactory the factory for the server's {@link LoopResources loop + * resources}, may be {@code null} + * @since 3.2.0 + * {@link #NettyWebServer(HttpServer, ReactorHttpHandlerAdapter, Duration, Shutdown, ReactorResourceFactory)} + */ + public NettyWebServer(HttpServer httpServer, ReactorHttpHandlerAdapter handlerAdapter, Duration lifecycleTimeout, + Shutdown shutdown, ReactorResourceFactory resourceFactory) { Assert.notNull(httpServer, "HttpServer must not be null"); Assert.notNull(handlerAdapter, "HandlerAdapter must not be null"); this.lifecycleTimeout = lifecycleTimeout; @@ -87,6 +117,7 @@ public NettyWebServer(HttpServer httpServer, ReactorHttpHandlerAdapter handlerAd this.httpServer = httpServer.channelGroup(new DefaultChannelGroup(new DefaultEventExecutor())); this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(() -> this.disposableServer) : null; + this.resourceFactory = resourceFactory; } public void setRouteProviders(List routeProviders) { @@ -143,6 +174,11 @@ DisposableServer startHttpServer() { else { server = server.route(this::applyRouteProviders); } + if (this.resourceFactory != null) { + LoopResources resources = this.resourceFactory.getLoopResources(); + Assert.notNull(resources, "No LoopResources: is ReactorResourceFactory not initialized yet?"); + server = server.runOn(resources); + } if (this.lifecycleTimeout != null) { return server.bindNow(this.lifecycleTimeout); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java index b1e8a318aa1b..652042688c85 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java @@ -41,12 +41,14 @@ import org.springframework.boot.web.server.Ssl; import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.http.client.reactive.ReactorResourceFactory; import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.WebClient; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.inOrder; @@ -80,6 +82,23 @@ void getPortWhenDisposableServerPortOperationIsUnsupportedReturnsMinusOne() { assertThat(this.webServer.getPort()).isEqualTo(-1); } + @Test + void resourceFactoryAndWebServerLifecycle() { + NettyReactiveWebServerFactory factory = getFactory(); + factory.setPort(0); + ReactorResourceFactory resourceFactory = new ReactorResourceFactory(); + factory.setResourceFactory(resourceFactory); + this.webServer = factory.getWebServer(new EchoHandler()); + assertThatNoException().isThrownBy(() -> { + resourceFactory.start(); + this.webServer.start(); + this.webServer.stop(); + resourceFactory.stop(); + resourceFactory.start(); + this.webServer.start(); + }); + } + private void portMatchesRequirement(PortInUseException exception) { assertThat(exception.getPort()).isEqualTo(this.webServer.getPort()); } @@ -199,7 +218,7 @@ static class NoPortNettyWebServer extends NettyWebServer { NoPortNettyWebServer(HttpServer httpServer, ReactorHttpHandlerAdapter handlerAdapter, Duration lifecycleTimeout, Shutdown shutdown) { - super(httpServer, handlerAdapter, lifecycleTimeout, shutdown); + super(httpServer, handlerAdapter, lifecycleTimeout, shutdown, null); } @Override From 9cb89e3366b81c97dec9c287d20933b8b99acfc2 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Sat, 16 Sep 2023 19:01:52 +0900 Subject: [PATCH 0467/1215] Polish SimpleAsyncTaskExecutorBuilder See gh-37436 --- .../task/SimpleAsyncTaskExecutorBuilder.java | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilder.java index 12d71da1af18..c040dc640668 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilder.java @@ -55,11 +55,7 @@ public class SimpleAsyncTaskExecutorBuilder { private final Set customizers; public SimpleAsyncTaskExecutorBuilder() { - this.virtualThreads = null; - this.threadNamePrefix = null; - this.concurrencyLimit = null; - this.taskDecorator = null; - this.customizers = null; + this(null, null, null, null, null); } private SimpleAsyncTaskExecutorBuilder(Boolean virtualThreads, String threadNamePrefix, Integer concurrencyLimit, @@ -112,7 +108,7 @@ public SimpleAsyncTaskExecutorBuilder taskDecorator(TaskDecorator taskDecorator) } /** - * Set the {@link SimpleAsyncTaskExecutorCustomizer TaskExecutorCustomizers} that + * Set the {@link SimpleAsyncTaskExecutorCustomizer customizers} that * should be applied to the {@link SimpleAsyncTaskExecutor}. Customizers are applied * in the order that they were added after builder configuration has been applied. * Setting this value will replace any previously configured customizers. @@ -126,13 +122,13 @@ public SimpleAsyncTaskExecutorBuilder customizers(SimpleAsyncTaskExecutorCustomi } /** - * Set the {@link SimpleAsyncTaskExecutorCustomizer TaskExecutorCustomizers} that + * Set the {@link SimpleAsyncTaskExecutorCustomizer customizers} that * should be applied to the {@link SimpleAsyncTaskExecutor}. Customizers are applied * in the order that they were added after builder configuration has been applied. * Setting this value will replace any previously configured customizers. * @param customizers the customizers to set * @return a new builder instance - * @see #additionalCustomizers(SimpleAsyncTaskExecutorCustomizer...) + * @see #additionalCustomizers(Iterable) */ public SimpleAsyncTaskExecutorBuilder customizers( Iterable customizers) { @@ -142,7 +138,7 @@ public SimpleAsyncTaskExecutorBuilder customizers( } /** - * Add {@link SimpleAsyncTaskExecutorCustomizer TaskExecutorCustomizers} that should + * Add {@link SimpleAsyncTaskExecutorCustomizer customizers} that should * be applied to the {@link SimpleAsyncTaskExecutor}. Customizers are applied in the * order that they were added after builder configuration has been applied. * @param customizers the customizers to add @@ -155,12 +151,12 @@ public SimpleAsyncTaskExecutorBuilder additionalCustomizers(SimpleAsyncTaskExecu } /** - * Add {@link SimpleAsyncTaskExecutorCustomizer TaskExecutorCustomizers} that should + * Add {@link SimpleAsyncTaskExecutorCustomizer customizers} that should * be applied to the {@link SimpleAsyncTaskExecutor}. Customizers are applied in the * order that they were added after builder configuration has been applied. * @param customizers the customizers to add * @return a new builder instance - * @see #customizers(SimpleAsyncTaskExecutorCustomizer...) + * @see #customizers(Iterable) */ public SimpleAsyncTaskExecutorBuilder additionalCustomizers( Iterable customizers) { From c1f27de66a1643693f92284650cc7b55af11129c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 18 Sep 2023 19:09:07 +0200 Subject: [PATCH 0468/1215] Upgrade to Spring AMQP 3.1.0-M1 Closes gh-37228 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 466d630e58ff..d3f79693a521 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1493,7 +1493,7 @@ bom { ] } } - library("Spring AMQP", "3.1.0-SNAPSHOT") { + library("Spring AMQP", "3.1.0-M1") { considerSnapshots() group("org.springframework.amqp") { imports = [ From 3c60c1304d7bfb19fbb5f4b5479c79392c2581bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 18 Sep 2023 19:09:07 +0200 Subject: [PATCH 0469/1215] Upgrade to Spring Security 6.2.0-M3 Closes gh-37236 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d3f79693a521..fbf9063dbf6c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1606,7 +1606,7 @@ bom { ] } } - library("Spring Security", "6.2.0-SNAPSHOT") { + library("Spring Security", "6.2.0-M3") { considerSnapshots() group("org.springframework.security") { imports = [ From 68641c3951ceed8143bbfdcf30225f4d131e027b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 19 Sep 2023 09:30:09 +0200 Subject: [PATCH 0470/1215] Upgrade to Spring Kafka 3.1.0-M1 Closes gh-37234 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index fbf9063dbf6c..d9d53d5f32a6 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1560,7 +1560,7 @@ bom { ] } } - library("Spring Kafka", "3.1.0-SNAPSHOT") { + library("Spring Kafka", "3.1.0-M1") { considerSnapshots() group("org.springframework.kafka") { modules = [ From 32b028029473a0a45a26045b4a907c28f1b6a2db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 19 Sep 2023 09:30:14 +0200 Subject: [PATCH 0471/1215] Upgrade to Spring Pulsar 1.0.0-M2 Closes gh-37454 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d9d53d5f32a6..c37a19d7526e 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1580,7 +1580,7 @@ bom { ] } } - library("Spring Pulsar", "1.0.0-SNAPSHOT") { + library("Spring Pulsar", "1.0.0-M2") { group("org.springframework.pulsar") { modules = [ "spring-pulsar", From 50f06af12f301a5ce502e26f76c86b47c4b24664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 19 Sep 2023 09:30:14 +0200 Subject: [PATCH 0472/1215] Upgrade to Spring WS 4.0.6 Closes gh-37427 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c37a19d7526e..06cf80cf8f28 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1626,7 +1626,7 @@ bom { ] } } - library("Spring WS", "4.0.6-SNAPSHOT") { + library("Spring WS", "4.0.6") { considerSnapshots() group("org.springframework.ws") { imports = [ From cc259196029034cc2dd8eb05912d0853716e695e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 19 Sep 2023 15:02:42 +0200 Subject: [PATCH 0473/1215] Upgrade to GraphQL Java 21.1 Closes gh-37458 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 06cf80cf8f28..67714bc37851 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -327,7 +327,7 @@ bom { ] } } - library("GraphQL Java", "20.4") { + library("GraphQL Java", "21.1") { prohibit { startsWith(["2018-", "2019-", "2020-", "2021-", "230521-"]) because "These are snapshots that we don't want to see" From 0e64ed7870ade0fe61130fc64c88070510dff601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 19 Sep 2023 15:02:42 +0200 Subject: [PATCH 0474/1215] Upgrade to Spring Authorization Server 1.2.0-M1 Closes gh-37229 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 67714bc37851..598f1cfac90c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1501,7 +1501,7 @@ bom { ] } } - library("Spring Authorization Server", "1.2.0-SNAPSHOT") { + library("Spring Authorization Server", "1.2.0-M1") { considerSnapshots() group("org.springframework.security") { modules = [ From 2476193eb730d251882500fb986b08143da49ee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Tue, 19 Sep 2023 15:02:43 +0200 Subject: [PATCH 0475/1215] Upgrade to Spring GraphQL 1.2.3 Closes gh-37232 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 598f1cfac90c..56ce5f4f7a19 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1535,7 +1535,7 @@ bom { ] } } - library("Spring GraphQL", "1.2.3-SNAPSHOT") { + library("Spring GraphQL", "1.2.3") { considerSnapshots() group("org.springframework.graphql") { modules = [ From 860f8c8acd4e59ea336ddb233ad5b7ba081a0f2d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 19 Sep 2023 18:01:48 +0100 Subject: [PATCH 0476/1215] Upgrade to Elasticsearch Client 8.10.1 Closes gh-37467 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 56ce5f4f7a19..36a96602f3b3 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -268,7 +268,7 @@ bom { ] } } - library("Elasticsearch Client", "8.10.0") { + library("Elasticsearch Client", "8.10.1") { group("org.elasticsearch.client") { modules = [ "elasticsearch-rest-client" { From d8897b260905f6a2095866893872cf6ad61f44a9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 19 Sep 2023 18:01:54 +0100 Subject: [PATCH 0477/1215] Upgrade to H2 2.2.224 Closes gh-37468 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 36a96602f3b3..7d3f803d4ba6 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -352,7 +352,7 @@ bom { ] } } - library("H2", "2.2.222") { + library("H2", "2.2.224") { group("com.h2database") { modules = [ "h2" From 0d4a6ee6877702432128fd968bc83d3f687b15dc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 19 Sep 2023 18:01:59 +0100 Subject: [PATCH 0478/1215] Upgrade to Maven Javadoc Plugin 3.6.0 Closes gh-37469 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 7d3f803d4ba6..017d5bdcfa55 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -953,7 +953,7 @@ bom { ] } } - library("Maven Javadoc Plugin", "3.5.0") { + library("Maven Javadoc Plugin", "3.6.0") { group("org.apache.maven.plugins") { plugins = [ "maven-javadoc-plugin" From d2fdbb8b47a3e157f35cd2ad0971da60d8705b79 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 19 Sep 2023 18:02:04 +0100 Subject: [PATCH 0479/1215] Upgrade to R2DBC MySQL 1.0.3 Closes gh-37470 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 017d5bdcfa55..5ba5e9d3d4bc 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1266,7 +1266,7 @@ bom { ] } } - library("R2DBC MySQL", "1.0.2") { + library("R2DBC MySQL", "1.0.3") { group("io.asyncer") { modules = [ "r2dbc-mysql" From 0676a53bf7644d7dbc9498db4ff4734024384956 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 19 Sep 2023 18:02:09 +0100 Subject: [PATCH 0480/1215] Upgrade to Thymeleaf Layout Dialect 3.3.0 Closes gh-37471 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5ba5e9d3d4bc..c05c6fc63b08 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1670,7 +1670,7 @@ bom { ] } } - library("Thymeleaf Layout Dialect", "3.2.1") { + library("Thymeleaf Layout Dialect", "3.3.0") { group("nz.net.ultraq.thymeleaf") { modules = [ "thymeleaf-layout-dialect" From f2fb7a025f02615d0752f2888add5d2d89a8ad49 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 19 Sep 2023 19:45:34 +0100 Subject: [PATCH 0481/1215] Upgrade to Artemis 2.31.0 Closes gh-37475 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c05c6fc63b08..77cd6741f1c1 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -65,7 +65,7 @@ bom { ] } } - library("Artemis", "2.30.0") { + library("Artemis", "2.31.0") { group("org.apache.activemq") { modules = [ "artemis-amqp-protocol", From 419cc94653d3b26d3b89055b4c50bc79dfc2fa08 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 19 Sep 2023 19:45:41 +0100 Subject: [PATCH 0482/1215] Upgrade to Hibernate 6.3.1.Final Closes gh-37476 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 77cd6741f1c1..8edc70ef7c54 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -376,7 +376,7 @@ bom { ] } } - library("Hibernate", "6.2.8.Final") { + library("Hibernate", "6.3.1.Final") { group("org.hibernate.orm") { modules = [ "hibernate-agroal", From 787cfac7889330e2f6aaa27c213062871d371af2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 20 Sep 2023 11:24:05 +0100 Subject: [PATCH 0483/1215] Upgrade to Lombok 1.18.30 Closes gh-37489 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 8edc70ef7c54..307633cd6ec8 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -855,7 +855,7 @@ bom { ] } } - library("Lombok", "1.18.28") { + library("Lombok", "1.18.30") { group("org.projectlombok") { modules = [ "lombok" From f60510c1736eecc0d310dacedc77306ac59824d6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 20 Sep 2023 11:24:05 +0100 Subject: [PATCH 0484/1215] Upgrade to Spring Batch 5.1.0-M3 Closes gh-37230 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 307633cd6ec8..9b1fff55b179 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1509,7 +1509,7 @@ bom { ] } } - library("Spring Batch", "5.1.0-SNAPSHOT") { + library("Spring Batch", "5.1.0-M3") { considerSnapshots() group("org.springframework.batch") { imports = [ From 7a803471b5792db9a44e850aa100da74f7ef2968 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 20 Sep 2023 11:24:06 +0100 Subject: [PATCH 0485/1215] Upgrade to Spring Integration 6.2.0-M3 Closes gh-37233 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 9b1fff55b179..58cd2f24947f 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1552,7 +1552,7 @@ bom { ] } } - library("Spring Integration", "6.2.0-SNAPSHOT") { + library("Spring Integration", "6.2.0-M3") { considerSnapshots() group("org.springframework.integration") { imports = [ From d8576f319e9087b35437d1289238f6b9ae273056 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 20 Sep 2023 11:24:11 +0100 Subject: [PATCH 0486/1215] Upgrade to Versions Maven Plugin 2.16.1 Closes gh-37490 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 58cd2f24947f..523e8a4f7bd3 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1710,7 +1710,7 @@ bom { ] } } - library("Versions Maven Plugin", "2.16.0") { + library("Versions Maven Plugin", "2.16.1") { group("org.codehaus.mojo") { plugins = [ "versions-maven-plugin" From 4433fcd1f2c6d9c9a912eb89b2b44ce2d3f7b512 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Wed, 20 Sep 2023 13:30:20 -0500 Subject: [PATCH 0487/1215] Add support for build workspace option when building images Closes gh-37478 --- .../platform/build/BuildRequest.java | 74 ++++++++++++------ .../buildpack/platform/build/Lifecycle.java | 78 +++++++++++++------ .../platform/build/BuildRequestTests.java | 16 ++++ .../platform/build/LifecycleTests.java | 6 +- .../lifecycle-creator-cache-bind-mounts.json | 4 +- .../lifecycle-creator-cache-volumes.json | 4 +- .../docs/asciidoc/packaging-oci-image.adoc | 15 +++- .../boot-build-image-bind-caches.gradle | 6 ++ .../boot-build-image-bind-caches.gradle.kts | 6 ++ .../gradle/tasks/bundling/BootBuildImage.java | 25 ++++++ .../docs/PackagingDocumentationTests.java | 3 +- ...tionTests-buildsImageWithBindCaches.gradle | 5 ++ ...onTests-buildsImageWithVolumeCaches.gradle | 5 ++ .../docs/asciidoc/packaging-oci-image.adoc | 14 +++- .../packaging-oci-image/bind-caches-pom.xml | 5 ++ .../projects/build-image-bind-caches/pom.xml | 5 ++ .../build-image-volume-caches/pom.xml | 5 ++ .../org/springframework/boot/maven/Image.java | 5 ++ .../boot/maven/ImageTests.java | 16 ++++ 19 files changed, 239 insertions(+), 58 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java index f409fcc875ff..c88b7527040a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java @@ -77,6 +77,8 @@ public class BuildRequest { private final List tags; + private final Cache buildWorkspace; + private final Cache buildCache; private final Cache launchCache; @@ -102,6 +104,7 @@ public class BuildRequest { this.bindings = Collections.emptyList(); this.network = null; this.tags = Collections.emptyList(); + this.buildWorkspace = null; this.buildCache = null; this.launchCache = null; this.createdDate = null; @@ -111,8 +114,8 @@ public class BuildRequest { BuildRequest(ImageReference name, Function applicationContent, ImageReference builder, ImageReference runImage, Creator creator, Map env, boolean cleanCache, boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List buildpacks, - List bindings, String network, List tags, Cache buildCache, Cache launchCache, - Instant createdDate, String applicationDirectory) { + List bindings, String network, List tags, Cache buildWorkspace, Cache buildCache, + Cache launchCache, Instant createdDate, String applicationDirectory) { this.name = name; this.applicationContent = applicationContent; this.builder = builder; @@ -127,6 +130,7 @@ public class BuildRequest { this.bindings = bindings; this.network = network; this.tags = tags; + this.buildWorkspace = buildWorkspace; this.buildCache = buildCache; this.launchCache = launchCache; this.createdDate = createdDate; @@ -142,8 +146,8 @@ public BuildRequest withBuilder(ImageReference builder) { Assert.notNull(builder, "Builder must not be null"); return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, - this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.launchCache, - this.createdDate, this.applicationDirectory); + this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, + this.launchCache, this.createdDate, this.applicationDirectory); } /** @@ -154,8 +158,8 @@ public BuildRequest withBuilder(ImageReference builder) { public BuildRequest withRunImage(ImageReference runImageName) { return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(), this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, - this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.launchCache, - this.createdDate, this.applicationDirectory); + this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, + this.launchCache, this.createdDate, this.applicationDirectory); } /** @@ -167,7 +171,7 @@ public BuildRequest withCreator(Creator creator) { Assert.notNull(creator, "Creator must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, + this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); } @@ -184,8 +188,8 @@ public BuildRequest withEnv(String name, String value) { env.put(name, value); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, - this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.launchCache, - this.createdDate, this.applicationDirectory); + this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, + this.launchCache, this.createdDate, this.applicationDirectory); } /** @@ -199,8 +203,8 @@ public BuildRequest withEnv(Map env) { updatedEnv.putAll(env); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy, - this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, - this.launchCache, this.createdDate, this.applicationDirectory); + this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, + this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); } /** @@ -211,7 +215,7 @@ public BuildRequest withEnv(Map env) { public BuildRequest withCleanCache(boolean cleanCache) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, + this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); } @@ -223,7 +227,7 @@ public BuildRequest withCleanCache(boolean cleanCache) { public BuildRequest withVerboseLogging(boolean verboseLogging) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, + this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); } @@ -235,7 +239,7 @@ public BuildRequest withVerboseLogging(boolean verboseLogging) { public BuildRequest withPullPolicy(PullPolicy pullPolicy) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, + this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); } @@ -247,7 +251,7 @@ public BuildRequest withPullPolicy(PullPolicy pullPolicy) { public BuildRequest withPublish(boolean publish) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, + this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); } @@ -272,7 +276,7 @@ public BuildRequest withBuildpacks(List buildpacks) { Assert.notNull(buildpacks, "Buildpacks must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks, this.bindings, - this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, + this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); } @@ -297,7 +301,7 @@ public BuildRequest withBindings(List bindings) { Assert.notNull(bindings, "Bindings must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, bindings, - this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, + this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); } @@ -310,7 +314,8 @@ public BuildRequest withBindings(List bindings) { public BuildRequest withNetwork(String network) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - network, this.tags, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); + network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, + this.applicationDirectory); } /** @@ -332,7 +337,21 @@ public BuildRequest withTags(List tags) { Assert.notNull(tags, "Tags must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, tags, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); + this.network, tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, + this.applicationDirectory); + } + + /** + * Return a new {@link BuildRequest} with an updated build workspace. + * @param buildWorkspace the build workspace + * @return an updated build request + */ + public BuildRequest withBuildWorkspace(Cache buildWorkspace) { + Assert.notNull(buildWorkspace, "BuildWorkspace must not be null"); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, + this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, + this.network, this.tags, buildWorkspace, this.buildCache, this.launchCache, this.createdDate, + this.applicationDirectory); } /** @@ -344,7 +363,8 @@ public BuildRequest withBuildCache(Cache buildCache) { Assert.notNull(buildCache, "BuildCache must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, buildCache, this.launchCache, this.createdDate, this.applicationDirectory); + this.network, this.tags, this.buildWorkspace, buildCache, this.launchCache, this.createdDate, + this.applicationDirectory); } /** @@ -356,7 +376,8 @@ public BuildRequest withLaunchCache(Cache launchCache) { Assert.notNull(launchCache, "LaunchCache must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildCache, launchCache, this.createdDate, this.applicationDirectory); + this.network, this.tags, this.buildWorkspace, this.buildCache, launchCache, this.createdDate, + this.applicationDirectory); } /** @@ -368,8 +389,8 @@ public BuildRequest withCreatedDate(String createdDate) { Assert.notNull(createdDate, "CreatedDate must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildCache, this.launchCache, parseCreatedDate(createdDate), - this.applicationDirectory); + this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, + parseCreatedDate(createdDate), this.applicationDirectory); } private Instant parseCreatedDate(String createdDate) { @@ -393,7 +414,8 @@ public BuildRequest withApplicationDirectory(String applicationDirectory) { Assert.notNull(applicationDirectory, "ApplicationDirectory must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, applicationDirectory); + this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, + applicationDirectory); } /** @@ -513,6 +535,10 @@ public List getTags() { return this.tags; } + public Cache getBuildWorkspace() { + return this.buildWorkspace; + } + /** * Return the custom build cache that should be used by the lifecycle. * @return the build cache diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java index 4d12105764a3..d12c27dab84e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java @@ -18,7 +18,9 @@ import java.io.Closeable; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import java.util.function.Consumer; import com.sun.jna.Platform; @@ -70,9 +72,9 @@ class Lifecycle implements Closeable { private final ApiVersion platformVersion; - private final VolumeName layersVolume; + private final Cache layers; - private final VolumeName applicationVolume; + private final Cache application; private final Cache buildCache; @@ -101,17 +103,13 @@ class Lifecycle implements Closeable { this.builder = builder; this.lifecycleVersion = LifecycleVersion.parse(builder.getBuilderMetadata().getLifecycle().getVersion()); this.platformVersion = getPlatformVersion(builder.getBuilderMetadata().getLifecycle()); - this.layersVolume = createRandomVolumeName("pack-layers-"); - this.applicationVolume = createRandomVolumeName("pack-app-"); + this.layers = getLayersBindingSource(request); + this.application = getApplicationBindingSource(request); this.buildCache = getBuildCache(request); this.launchCache = getLaunchCache(request); this.applicationDirectory = getApplicationDirectory(request); } - protected VolumeName createRandomVolumeName(String prefix) { - return VolumeName.random(prefix); - } - private Cache getBuildCache(BuildRequest request) { if (request.getBuildCache() != null) { return request.getBuildCache(); @@ -130,11 +128,6 @@ private String getApplicationDirectory(BuildRequest request) { return (request.getApplicationDirectory() != null) ? request.getApplicationDirectory() : Directory.APPLICATION; } - private Cache createVolumeCache(BuildRequest request, String suffix) { - return Cache.volume( - VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", "." + suffix, 6)); - } - private ApiVersion getPlatformVersion(BuilderMetadata.Lifecycle lifecycle) { if (lifecycle.getApis().getPlatform() != null) { String[] supportedVersions = lifecycle.getApis().getPlatform(); @@ -153,12 +146,7 @@ void execute() throws IOException { this.executed = true; this.log.executingLifecycle(this.request, this.lifecycleVersion, this.buildCache); if (this.request.isCleanCache()) { - if (this.buildCache.getVolume() != null) { - deleteVolume(this.buildCache.getVolume().getVolumeName()); - } - if (this.buildCache.getBind() != null) { - deleteBind(this.buildCache.getBind().getSource()); - } + deleteCache(this.buildCache); } run(createPhase()); this.log.executedLifecycle(this.request); @@ -183,8 +171,8 @@ private Phase createPhase() { phase.withArgs("-process-type=web"); } phase.withArgs(this.request.getName()); - phase.withBinding(Binding.from(this.layersVolume, Directory.LAYERS)); - phase.withBinding(Binding.from(this.applicationVolume, this.applicationDirectory)); + phase.withBinding(Binding.from(getCacheBindingSource(this.layers), Directory.LAYERS)); + phase.withBinding(Binding.from(getCacheBindingSource(this.application), this.applicationDirectory)); phase.withBinding(Binding.from(getCacheBindingSource(this.buildCache), Directory.CACHE)); phase.withBinding(Binding.from(getCacheBindingSource(this.launchCache), Directory.LAUNCH_CACHE)); if (this.request.getBindings() != null) { @@ -200,10 +188,42 @@ private Phase createPhase() { return phase; } + private Cache getLayersBindingSource(BuildRequest request) { + if (request.getBuildWorkspace() != null) { + return getBuildWorkspaceBindingSource(request.getBuildWorkspace(), "layers"); + } + return createVolumeCache("pack-layers-"); + } + + private Cache getApplicationBindingSource(BuildRequest request) { + if (request.getBuildWorkspace() != null) { + return getBuildWorkspaceBindingSource(request.getBuildWorkspace(), "app"); + } + return createVolumeCache("pack-app-"); + } + + private Cache getBuildWorkspaceBindingSource(Cache buildWorkspace, String suffix) { + return (buildWorkspace.getVolume() != null) ? Cache.volume(buildWorkspace.getVolume().getName() + "-" + suffix) + : Cache.bind(buildWorkspace.getBind().getSource() + "-" + suffix); + } + private String getCacheBindingSource(Cache cache) { return (cache.getVolume() != null) ? cache.getVolume().getName() : cache.getBind().getSource(); } + private Cache createVolumeCache(String prefix) { + return Cache.volume(createRandomVolumeName(prefix)); + } + + private Cache createVolumeCache(BuildRequest request, String suffix) { + return Cache.volume( + VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", "." + suffix, 6)); + } + + protected VolumeName createRandomVolumeName(String prefix) { + return VolumeName.random(prefix); + } + private void configureDaemonAccess(Phase phase) { if (this.dockerHost != null) { if (this.dockerHost.isRemote()) { @@ -255,6 +275,9 @@ private ContainerReference createContainer(ContainerConfig config) throws IOExce return this.docker.container().create(config); } try { + if (this.application.getBind() != null) { + Files.createDirectories(Path.of(this.application.getBind().getSource())); + } TarArchive applicationContent = this.request.getApplicationContent(this.builder.getBuildOwner()); return this.docker.container() .create(config, ContainerContent.of(applicationContent, this.applicationDirectory)); @@ -266,8 +289,17 @@ private ContainerReference createContainer(ContainerConfig config) throws IOExce @Override public void close() throws IOException { - deleteVolume(this.layersVolume); - deleteVolume(this.applicationVolume); + deleteCache(this.layers); + deleteCache(this.application); + } + + private void deleteCache(Cache cache) throws IOException { + if (cache.getVolume() != null) { + deleteVolume(cache.getVolume().getVolumeName()); + } + if (cache.getBind() != null) { + deleteBind(cache.getBind().getSource()); + } } private void deleteVolume(VolumeName name) throws IOException { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java index 55cc096d083d..464d218a50f7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java @@ -233,6 +233,22 @@ void withTagsWhenTagsIsNullThrowsException() throws IOException { .withMessage("Tags must not be null"); } + @Test + void withBuildWorkspaceVolumeAddsWorkspace() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + BuildRequest withWorkspace = request.withBuildWorkspace(Cache.volume("build-workspace")); + assertThat(request.getBuildWorkspace()).isNull(); + assertThat(withWorkspace.getBuildWorkspace()).isEqualTo(Cache.volume("build-workspace")); + } + + @Test + void withBuildWorkspaceBindAddsWorkspace() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + BuildRequest withWorkspace = request.withBuildWorkspace(Cache.bind("/tmp/build-workspace")); + assertThat(request.getBuildWorkspace()).isNull(); + assertThat(withWorkspace.getBuildWorkspace()).isEqualTo(Cache.bind("/tmp/build-workspace")); + } + @Test void withBuildVolumeCacheAddsCache() throws IOException { BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java index 40a2a80caa89..64f54b450e49 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java @@ -211,7 +211,8 @@ void executeWithCacheVolumeNamesExecutesPhases() throws Exception { given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); - BuildRequest request = getTestRequest().withBuildCache(Cache.volume("build-volume")) + BuildRequest request = getTestRequest().withBuildWorkspace(Cache.volume("work-volume")) + .withBuildCache(Cache.volume("build-volume")) .withLaunchCache(Cache.volume("launch-volume")); createLifecycle(request).execute(); assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-cache-volumes.json")); @@ -223,7 +224,8 @@ void executeWithCacheBindMountsExecutesPhases() throws Exception { given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); - BuildRequest request = getTestRequest().withBuildCache(Cache.bind("/tmp/build-cache")) + BuildRequest request = getTestRequest().withBuildWorkspace(Cache.bind("/tmp/work")) + .withBuildCache(Cache.bind("/tmp/build-cache")) .withLaunchCache(Cache.bind("/tmp/launch-cache")); createLifecycle(request).execute(); assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-cache-bind-mounts.json")); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-cache-bind-mounts.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-cache-bind-mounts.json index 2b7814d909c8..7259fc11af77 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-cache-bind-mounts.json +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-cache-bind-mounts.json @@ -27,8 +27,8 @@ "HostConfig": { "Binds": [ "/var/run/docker.sock:/var/run/docker.sock", - "pack-layers-aaaaaaaaaa:/layers", - "pack-app-aaaaaaaaaa:/workspace", + "/tmp/work-layers:/layers", + "/tmp/work-app:/workspace", "/tmp/build-cache:/cache", "/tmp/launch-cache:/launch-cache" ], diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-cache-volumes.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-cache-volumes.json index 7bd3d9a24ca0..0f611d5d059c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-cache-volumes.json +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-cache-volumes.json @@ -27,8 +27,8 @@ "HostConfig": { "Binds": [ "/var/run/docker.sock:/var/run/docker.sock", - "pack-layers-aaaaaaaaaa:/layers", - "pack-app-aaaaaaaaaa:/workspace", + "work-volume-layers:/layers", + "work-volume-app:/workspace", "build-volume:/cache", "launch-volume:/launch-cache" ], diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc index ef6ffc910eb5..3474dc37d4b6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -193,14 +193,22 @@ The value supplied will be passed unvalidated to Docker when creating the builde The values provided to the `tags` option should be full image references in the form of `[image name]:[tag]` or `[repository]/[image name]:[tag]`. | +| `buildWorkspace` +| +| A temporary workspace that will be used by the builder and buildpacks to store files during image building. +The value can be a named volume or a bind mount location. +| A named volume in the Docker daemon, with a name derived from the image name. + | `buildCache` | | A cache containing layers created by buildpacks and used by the image building process. +The value can be a named volume or a bind mount location. | A named volume in the Docker daemon, with a name derived from the image name. | `launchCache` | | A cache containing layers created by buildpacks and used by the image launching process. +The value can be a named volume or a bind mount location. | A named volume in the Docker daemon, with a name derived from the image name. | `createdDate` @@ -420,7 +428,7 @@ The publish option can be specified on the command line as well, as shown in thi ---- [[build-image.examples.caches]] -=== Builder Cache Configuration +=== Builder Cache and Workspace Configuration The CNB builder caches layers that are used when building and launching an image. By default, these caches are stored as named volumes in the Docker daemon with names that are derived from the full name of the target image. @@ -440,7 +448,10 @@ include::../gradle/packaging/boot-build-image-caches.gradle[tags=caches] include::../gradle/packaging/boot-build-image-caches.gradle.kts[tags=caches] ---- -The caches can be configured to use bind mounts instead of named volumes, as shown in the following example: +Builders and buildpacks need a location to store temporary files during image building. +By default, this temporary build workspace is stored in a named volume. + +The caches and the build workspace can be configured to use bind mounts instead of named volumes, as shown in the following example: [source,groovy,indent=0,subs="verbatim,attributes",role="primary"] .Groovy diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle index 5bca082e10fd..875239d07f80 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle @@ -9,6 +9,11 @@ tasks.named("bootJar") { // tag::caches[] tasks.named("bootBuildImage") { + buildWorkspace { + bind { + source = "/tmp/cache-${rootProject.name}.work" + } + } buildCache { bind { source = "/tmp/cache-${rootProject.name}.build" @@ -24,6 +29,7 @@ tasks.named("bootBuildImage") { tasks.register("bootBuildImageCaches") { doFirst { + bootBuildImage.buildWorkspace.asCache().with { print "buildWorkspace=$source" } bootBuildImage.buildCache.asCache().with { println "buildCache=$source" } bootBuildImage.launchCache.asCache().with { println "launchCache=$source" } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle.kts index 008889f51961..e492703c6f96 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle.kts +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-bind-caches.gradle.kts @@ -7,6 +7,11 @@ plugins { // tag::caches[] tasks.named("bootBuildImage") { + buildWorkspace { + bind { + source.set("/tmp/cache-${rootProject.name}.work") + } + } buildCache { bind { source.set("/tmp/cache-${rootProject.name}.build") @@ -22,6 +27,7 @@ tasks.named("bootBuildImage") { tasks.register("bootBuildImageCaches") { doFirst { + println("buildWorkspace=" + tasks.getByName("bootBuildImage").buildWorkspace.asCache().bind.source) println("buildCache=" + tasks.getByName("bootBuildImage").buildCache.asCache().bind.source) println("launchCache=" + tasks.getByName("bootBuildImage").launchCache.asCache().bind.source) } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java index 6b2af0c45a5e..04ebf45c74d2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java @@ -69,6 +69,8 @@ public abstract class BootBuildImage extends DefaultTask { private final String projectName; + private final CacheSpec buildWorkspace; + private final CacheSpec buildCache; private final CacheSpec launchCache; @@ -91,6 +93,7 @@ public BootBuildImage() { getCleanCache().convention(false); getVerboseLogging().convention(false); getPublish().convention(false); + this.buildWorkspace = getProject().getObjects().newInstance(CacheSpec.class); this.buildCache = getProject().getObjects().newInstance(CacheSpec.class); this.launchCache = getProject().getObjects().newInstance(CacheSpec.class); this.docker = getProject().getObjects().newInstance(DockerSpec.class); @@ -222,6 +225,25 @@ public void setPullPolicy(String pullPolicy) { @Option(option = "network", description = "Connect detect and build containers to network") public abstract Property getNetwork(); + /** + * Returns the build temporary workspace that will be used when building the image. + * @return the cache + */ + @Nested + @Optional + public CacheSpec getBuildWorkspace() { + return this.buildWorkspace; + } + + /** + * Customizes the {@link CacheSpec} for the build temporary workspace using the given + * {@code action}. + * @param action the action + */ + public void buildWorkspace(Action action) { + action.execute(this.buildWorkspace); + } + /** * Returns the build cache that will be used when building the image. * @return the cache @@ -400,6 +422,9 @@ private BuildRequest customizeTags(BuildRequest request) { } private BuildRequest customizeCaches(BuildRequest request) { + if (this.buildWorkspace.asCache() != null) { + request = request.withBuildWorkspace((this.buildWorkspace.asCache())); + } if (this.buildCache.asCache() != null) { request = request.withBuildCache(this.buildCache.asCache()); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java index 84a2506cd715..1b1a6531682c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java @@ -343,7 +343,8 @@ void bootBuildImageWithCaches() { void bootBuildImageWithBindCaches() { BuildResult result = this.gradleBuild.script("src/docs/gradle/packaging/boot-build-image-bind-caches") .build("bootBuildImageCaches"); - assertThat(result.getOutput()).containsPattern("buildCache=/tmp/cache-gradle-[\\d]+.build") + assertThat(result.getOutput()).containsPattern("buildWorkspace=/tmp/cache-gradle-[\\d]+.work") + .containsPattern("buildCache=/tmp/cache-gradle-[\\d]+.build") .containsPattern("launchCache=/tmp/cache-gradle-[\\d]+.launch"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBindCaches.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBindCaches.gradle index b1c8c803350a..4ffb3a011faa 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBindCaches.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithBindCaches.gradle @@ -11,6 +11,11 @@ java { bootBuildImage { builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2" pullPolicy = "IF_NOT_PRESENT" + buildWorkspace { + bind { + source = System.getProperty('java.io.tmpdir') + "/junit-image-pack-${rootProject.name}-work" + } + } buildCache { bind { source = System.getProperty('java.io.tmpdir') + "/junit-image-cache-${rootProject.name}-build" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithVolumeCaches.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithVolumeCaches.gradle index c4bc44c6e505..abf2c26ad447 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithVolumeCaches.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithVolumeCaches.gradle @@ -11,6 +11,11 @@ java { bootBuildImage { builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2" pullPolicy = "IF_NOT_PRESENT" + buildWorkspace { + volume { + name = "pack-${rootProject.name}.work" + } + } buildCache { volume { name = "cache-${rootProject.name}.build" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc index 059a9c2f882d..67b23f539b46 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -202,12 +202,19 @@ The value supplied will be passed unvalidated to Docker when creating the builde The values provided to the `tags` option should be full image references in the form of `[image name]:[tag]` or `[repository]/[image name]:[tag]`. | +| `buildWorkspace` +| A temporary workspace that will be used by the builder and buildpacks to store files during image building. +The value can be a named volume or a bind mount location. +| A named volume in the Docker daemon, with a name derived from the image name. + | `buildCache` | A cache containing layers created by buildpacks and used by the image building process. +The value can be a named volume or a bind mount location. | A named volume in the Docker daemon, with a name derived from the image name. | `launchCache` | A cache containing layers created by buildpacks and used by the image launching process. +The value can be a named volume or a bind mount location. | A named volume in the Docker daemon, with a name derived from the image name. | `createdDate` + @@ -403,7 +410,7 @@ include::../maven/packaging-oci-image/docker-pom-authentication-command-line.xml ---- [[build-image.examples.caches]] -=== Builder Cache Configuration +=== Builder Cache and Workspace Configuration The CNB builder caches layers that are used when building and launching an image. By default, these caches are stored as named volumes in the Docker daemon with names that are derived from the full name of the target image. @@ -416,7 +423,10 @@ The cache volumes can be configured to use alternative names to give more contro include::../maven/packaging-oci-image/caches-pom.xml[tags=caches] ---- -The caches can be configured to use bind mounts instead of named volumes, as shown in the following example: +Builders and buildpacks need a location to store temporary files during image building. +By default, this temporary build workspace is stored in a named volume. + +The caches and the build workspace can be configured to use bind mounts instead of named volumes, as shown in the following example: [source,xml,indent=0,subs="verbatim,attributes",tabsize=4] ---- diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/bind-caches-pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/bind-caches-pom.xml index 2cf4941fecbe..a67c45a0ed5d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/bind-caches-pom.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/maven/packaging-oci-image/bind-caches-pom.xml @@ -8,6 +8,11 @@ spring-boot-maven-plugin + + + /tmp/cache-${project.artifactId}.work + + /tmp/cache-${project.artifactId}.build diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bind-caches/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bind-caches/pom.xml index 349d1519e9e1..7f09ff829236 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bind-caches/pom.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-bind-caches/pom.xml @@ -24,6 +24,11 @@ projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2 + + + ${java.io.tmpdir}/junit-image-cache-${test-build-id}-work + + ${java.io.tmpdir}/junit-image-cache-${test-build-id}-build diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-volume-caches/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-volume-caches/pom.xml index 2b92c6dcb825..5a3d3ec76e86 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-volume-caches/pom.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-volume-caches/pom.xml @@ -24,6 +24,11 @@ projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2 + + + cache-${test-build-id}.work + + cache-${test-build-id}.build diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java index 2d5cf6b24728..699c450c6333 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java @@ -69,6 +69,8 @@ public class Image { List tags; + CacheInfo buildWorkspace; + CacheInfo buildCache; CacheInfo launchCache; @@ -243,6 +245,9 @@ private BuildRequest customize(BuildRequest request) { if (!CollectionUtils.isEmpty(this.tags)) { request = request.withTags(this.tags.stream().map(ImageReference::of).toList()); } + if (this.buildWorkspace != null) { + request = request.withBuildWorkspace(this.buildWorkspace.asCache()); + } if (this.buildCache != null) { request = request.withBuildCache(this.buildCache.asCache()); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java index ed5a8e5d8ede..a829b25dbfb2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java @@ -170,6 +170,14 @@ void getBuildRequestWhenHasTagsUsesTags() { ImageReference.of("example.com/my-app:0.0.1-SNAPSHOT"), ImageReference.of("example.com/my-app:latest")); } + @Test + void getBuildRequestWhenHasBuildWorkspaceVolumeUsesWorkspace() { + Image image = new Image(); + image.buildWorkspace = CacheInfo.fromVolume(new VolumeCacheInfo("build-work-vol")); + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getBuildWorkspace()).isEqualTo(Cache.volume("build-work-vol")); + } + @Test void getBuildRequestWhenHasBuildCacheVolumeUsesCache() { Image image = new Image(); @@ -186,6 +194,14 @@ void getBuildRequestWhenHasLaunchCacheVolumeUsesCache() { assertThat(request.getLaunchCache()).isEqualTo(Cache.volume("launch-cache-vol")); } + @Test + void getBuildRequestWhenHasBuildWorkspaceBindUsesWorkspace() { + Image image = new Image(); + image.buildWorkspace = CacheInfo.fromBind(new BindCacheInfo("build-work-dir")); + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getBuildWorkspace()).isEqualTo(Cache.bind("build-work-dir")); + } + @Test void getBuildRequestWhenHasBuildCacheBindUsesCache() { Image image = new Image(); From 7de770f6a191d16e3c120671de8b29349d95d57d Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Wed, 20 Sep 2023 13:30:20 -0500 Subject: [PATCH 0488/1215] Add support for security options in CNB builder container config Closes gh-37479 --- .../platform/build/BuildRequest.java | 63 +++++++++++++------ .../buildpack/platform/build/Lifecycle.java | 17 ++++- .../platform/build/BuildRequestTests.java | 7 +++ .../platform/build/LifecycleTests.java | 12 ++++ .../lifecycle-creator-security-opts.json | 40 ++++++++++++ .../docs/asciidoc/packaging-oci-image.adoc | 5 ++ .../gradle/tasks/bundling/BootBuildImage.java | 18 ++++++ .../BootBuildImageIntegrationTests.java | 13 ++++ ...buildsImageWithEmptySecurityOptions.gradle | 15 +++++ .../docs/asciidoc/packaging-oci-image.adoc | 4 ++ .../boot/maven/BuildImageTests.java | 15 +++++ .../build-image-security-opts/pom.xml | 35 +++++++++++ .../main/java/org/test/SampleApplication.java | 28 +++++++++ .../org/springframework/boot/maven/Image.java | 5 ++ .../boot/maven/ImageTests.java | 17 +++++ 15 files changed, 273 insertions(+), 21 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-security-opts.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithEmptySecurityOptions.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-security-opts/pom.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-security-opts/src/main/java/org/test/SampleApplication.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java index c88b7527040a..7ae4d8419a8c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java @@ -87,6 +87,8 @@ public class BuildRequest { private final String applicationDirectory; + private final List securityOptions; + BuildRequest(ImageReference name, Function applicationContent) { Assert.notNull(name, "Name must not be null"); Assert.notNull(applicationContent, "ApplicationContent must not be null"); @@ -109,13 +111,14 @@ public class BuildRequest { this.launchCache = null; this.createdDate = null; this.applicationDirectory = null; + this.securityOptions = null; } BuildRequest(ImageReference name, Function applicationContent, ImageReference builder, ImageReference runImage, Creator creator, Map env, boolean cleanCache, boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List buildpacks, List bindings, String network, List tags, Cache buildWorkspace, Cache buildCache, - Cache launchCache, Instant createdDate, String applicationDirectory) { + Cache launchCache, Instant createdDate, String applicationDirectory, List securityOptions) { this.name = name; this.applicationContent = applicationContent; this.builder = builder; @@ -135,6 +138,7 @@ public class BuildRequest { this.launchCache = launchCache; this.createdDate = createdDate; this.applicationDirectory = applicationDirectory; + this.securityOptions = securityOptions; } /** @@ -147,7 +151,7 @@ public BuildRequest withBuilder(ImageReference builder) { return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, - this.launchCache, this.createdDate, this.applicationDirectory); + this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions); } /** @@ -159,7 +163,7 @@ public BuildRequest withRunImage(ImageReference runImageName) { return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(), this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, - this.launchCache, this.createdDate, this.applicationDirectory); + this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions); } /** @@ -172,7 +176,7 @@ public BuildRequest withCreator(Creator creator) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -189,7 +193,7 @@ public BuildRequest withEnv(String name, String value) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, - this.launchCache, this.createdDate, this.applicationDirectory); + this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions); } /** @@ -204,7 +208,7 @@ public BuildRequest withEnv(Map env) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, - this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory); + this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions); } /** @@ -216,7 +220,7 @@ public BuildRequest withCleanCache(boolean cleanCache) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -228,7 +232,7 @@ public BuildRequest withVerboseLogging(boolean verboseLogging) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -240,7 +244,7 @@ public BuildRequest withPullPolicy(PullPolicy pullPolicy) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -252,7 +256,7 @@ public BuildRequest withPublish(boolean publish) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -277,7 +281,7 @@ public BuildRequest withBuildpacks(List buildpacks) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -302,7 +306,7 @@ public BuildRequest withBindings(List bindings) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -315,7 +319,7 @@ public BuildRequest withNetwork(String network) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -338,7 +342,7 @@ public BuildRequest withTags(List tags) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -351,7 +355,7 @@ public BuildRequest withBuildWorkspace(Cache buildWorkspace) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -364,7 +368,7 @@ public BuildRequest withBuildCache(Cache buildCache) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, buildCache, this.launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -377,7 +381,7 @@ public BuildRequest withLaunchCache(Cache launchCache) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, launchCache, this.createdDate, - this.applicationDirectory); + this.applicationDirectory, this.securityOptions); } /** @@ -390,7 +394,7 @@ public BuildRequest withCreatedDate(String createdDate) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, - parseCreatedDate(createdDate), this.applicationDirectory); + parseCreatedDate(createdDate), this.applicationDirectory, this.securityOptions); } private Instant parseCreatedDate(String createdDate) { @@ -415,7 +419,20 @@ public BuildRequest withApplicationDirectory(String applicationDirectory) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - applicationDirectory); + applicationDirectory, this.securityOptions); + } + + /** + * Return a new {@link BuildRequest} with an updated security options. + * @param securityOptions the security options + * @return an updated build request + */ + public BuildRequest withSecurityOptions(List securityOptions) { + Assert.notNull(securityOptions, "SecurityOption must not be null"); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, + this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, + this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, + this.applicationDirectory, securityOptions); } /** @@ -571,6 +588,14 @@ public String getApplicationDirectory() { return this.applicationDirectory; } + /** + * Return the security options that should be used by the lifecycle. + * @return the security options + */ + public List getSecurityOptions() { + return this.securityOptions; + } + /** * Factory method to create a new {@link BuildRequest} from a JAR file. * @param jarFile the source jar file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java index d12c27dab84e..1c7e7183e7a8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; import java.util.List; import java.util.function.Consumer; @@ -58,6 +59,8 @@ class Lifecycle implements Closeable { private static final String DOMAIN_SOCKET_PATH = "/var/run/docker.sock"; + private static final List DEFAULT_SECURITY_OPTIONS = List.of("label=disable"); + private final BuildLog log; private final DockerApi docker; @@ -82,6 +85,8 @@ class Lifecycle implements Closeable { private final String applicationDirectory; + private final List securityOptions; + private boolean executed; private boolean applicationVolumePopulated; @@ -108,6 +113,7 @@ class Lifecycle implements Closeable { this.buildCache = getBuildCache(request); this.launchCache = getLaunchCache(request); this.applicationDirectory = getApplicationDirectory(request); + this.securityOptions = getSecurityOptions(request); } private Cache getBuildCache(BuildRequest request) { @@ -128,6 +134,13 @@ private String getApplicationDirectory(BuildRequest request) { return (request.getApplicationDirectory() != null) ? request.getApplicationDirectory() : Directory.APPLICATION; } + private List getSecurityOptions(BuildRequest request) { + if (request.getSecurityOptions() != null) { + return request.getSecurityOptions(); + } + return (Platform.isWindows()) ? Collections.emptyList() : DEFAULT_SECURITY_OPTIONS; + } + private ApiVersion getPlatformVersion(BuilderMetadata.Lifecycle lifecycle) { if (lifecycle.getApis().getPlatform() != null) { String[] supportedVersions = lifecycle.getApis().getPlatform(); @@ -240,8 +253,8 @@ private void configureDaemonAccess(Phase phase) { else { phase.withBinding(Binding.from(DOMAIN_SOCKET_PATH, DOMAIN_SOCKET_PATH)); } - if (!Platform.isWindows()) { - phase.withSecurityOption("label=disable"); + if (this.securityOptions != null) { + this.securityOptions.forEach(phase::withSecurityOption); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java index 464d218a50f7..6e6fce7f50a8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java @@ -333,6 +333,13 @@ void withApplicationDirectorySetsApplicationDirectory() throws Exception { assertThat(withAppDir.getApplicationDirectory()).isEqualTo("/application"); } + @Test + void withSecurityOptionsSetsSecurityOptions() throws Exception { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + BuildRequest withAppDir = request.withSecurityOptions(List.of("label=user:USER", "label=role:ROLE")); + assertThat(withAppDir.getSecurityOptions()).containsExactly("label=user:USER", "label=role:ROLE"); + } + private void hasExpectedJarContent(TarArchive archive) { try { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java index 64f54b450e49..f55e0ed5cda1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java @@ -23,6 +23,7 @@ import java.io.PrintStream; import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import com.fasterxml.jackson.core.JsonProcessingException; @@ -254,6 +255,17 @@ void executeWithApplicationDirectoryExecutesPhases() throws Exception { assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); } + @Test + void executeWithSecurityOptionsExecutesPhases() throws Exception { + given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); + BuildRequest request = getTestRequest().withSecurityOptions(List.of("label=user:USER", "label=role:ROLE")); + createLifecycle(request).execute(); + assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-security-opts.json")); + assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + } + @Test void executeWithDockerHostAndRemoteAddressExecutesPhases() throws Exception { given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-security-opts.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-security-opts.json new file mode 100644 index 000000000000..c47bd7f9ffd7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-security-opts.json @@ -0,0 +1,40 @@ +{ + "User": "root", + "Image": "pack.local/ephemeral-builder", + "Cmd": [ + "/cnb/lifecycle/creator", + "-app", + "/workspace", + "-platform", + "/platform", + "-run-image", + "docker.io/cloudfoundry/run:latest", + "-layers", + "/layers", + "-cache-dir", + "/cache", + "-launch-cache", + "/launch-cache", + "-daemon", + "docker.io/library/my-application:latest" + ], + "Env": [ + "CNB_PLATFORM_API=0.8" + ], + "Labels": { + "author": "spring-boot" + }, + "HostConfig": { + "Binds": [ + "/var/run/docker.sock:/var/run/docker.sock", + "pack-layers-aaaaaaaaaa:/layers", + "pack-app-aaaaaaaaaa:/workspace", + "pack-cache-b35197ac41ea.build:/cache", + "pack-cache-b35197ac41ea.launch:/launch-cache" + ], + "SecurityOpt" : [ + "label=user:USER", + "label=role:ROLE" + ] + } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc index 3474dc37d4b6..f0abfb2dfe25 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -223,6 +223,11 @@ The value must be a string in the ISO 8601 instant format, or `now` to use the c Application contents will also be in this location in the generated image. | `/workspace` +| `securityOptions` +| `--securityOptions` +| https://docs.docker.com/engine/reference/run/#security-configuration[Security options] that will be applied to the builder container, provided as an array of string values +| `["label=disable"]` + |=== NOTE: The plugin detects the target Java compatibility of the project using the JavaPlugin's `targetCompatibility` property. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java index 04ebf45c74d2..143f54d62b8c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java @@ -302,6 +302,15 @@ public void launchCache(Action action) { @Option(option = "applicationDirectory", description = "The directory containing application content in the image") public abstract Property getApplicationDirectory(); + /** + * Returns the security options that will be applied to the builder container. + * @return the security options + */ + @Input + @Optional + @Option(option = "securityOptions", description = "Security options that will be applied to the builder container") + public abstract ListProperty getSecurityOptions(); + /** * Returns the Docker configuration the builder will use. * @return docker configuration. @@ -349,6 +358,7 @@ private BuildRequest customize(BuildRequest request) { request = request.withNetwork(getNetwork().getOrNull()); request = customizeCreatedDate(request); request = customizeApplicationDirectory(request); + request = customizeSecurityOptions(request); return request; } @@ -450,4 +460,12 @@ private BuildRequest customizeApplicationDirectory(BuildRequest request) { return request; } + private BuildRequest customizeSecurityOptions(BuildRequest request) { + List securityOptions = getSecurityOptions().getOrNull(); + if (securityOptions != null) { + return request.withSecurityOptions(securityOptions); + } + return request; + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java index 06bc731d634b..b20924ad7d6b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java @@ -368,6 +368,19 @@ void buildsImageWithApplicationDirectory() throws IOException { removeImages(projectName); } + @TestTemplate + void buildsImageWithEmptySecurityOptions() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.build("bootBuildImage"); + String projectName = this.gradleBuild.getProjectDir().getName(); + assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("docker.io/library/" + projectName); + assertThat(result.getOutput()).contains("---> Test Info buildpack building"); + assertThat(result.getOutput()).contains("---> Test Info buildpack done"); + removeImages(projectName); + } + @TestTemplate void failsWithInvalidCreatedDate() throws IOException { writeMainClass(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithEmptySecurityOptions.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithEmptySecurityOptions.gradle new file mode 100644 index 000000000000..a44b78077564 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithEmptySecurityOptions.gradle @@ -0,0 +1,15 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +java { + sourceCompatibility = '1.8' + targetCompatibility = '1.8' +} + +bootBuildImage { + builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2" + pullPolicy = "IF_NOT_PRESENT" + securityOptions = [] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc index 67b23f539b46..2766e6b12fd3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -230,6 +230,10 @@ The value must be a string in the ISO 8601 instant format, or `now` to use the c Application contents will also be in this location in the generated image. | `/workspace` +| `securityOptions` +| https://docs.docker.com/engine/reference/run/#security-configuration[Security options] that will be applied to the builder container, provided as an array of string values +| `["label=disable"]` + |=== NOTE: The plugin detects the target Java compatibility of the project using the compiler's plugin configuration or the `maven.compiler.target` property. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java index a729404432a5..12ca33cdb26a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java @@ -480,6 +480,21 @@ void whenBuildImageIsInvokedWithApplicationDirectory(MavenBuild mavenBuild) { }); } + @TestTemplate + void whenBuildImageIsInvokedWithEmptySecurityOptions(MavenBuild mavenBuild) { + String testBuildId = randomString(); + mavenBuild.project("build-image-security-opts") + .goals("package") + .systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT") + .systemProperty("test-build-id", testBuildId) + .execute((project) -> { + assertThat(buildLog(project)).contains("Building image") + .contains("docker.io/library/build-image-security-opts:0.0.1.BUILD-SNAPSHOT") + .contains("Successfully built image"); + removeImage("build-image-security-opts", "0.0.1.BUILD-SNAPSHOT"); + }); + } + @TestTemplate void failsWhenBuildImageIsInvokedOnMultiModuleProjectWithBuildImageGoal(MavenBuild mavenBuild) { mavenBuild.project("build-image-multi-module") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-security-opts/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-security-opts/pom.xml new file mode 100644 index 000000000000..5eee589a4660 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-security-opts/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-security-opts + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image-no-fork + + + + projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2 + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-security-opts/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-security-opts/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..58ebebbbb234 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-security-opts/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2023 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.test; + +public class SampleApplication { + + public static void main(String[] args) throws Exception { + System.out.println("Launched"); + synchronized(args) { + args.wait(); // Prevent exit" + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java index 699c450c6333..c19ac62465a4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java @@ -79,6 +79,8 @@ public class Image { String applicationDirectory; + List securityOptions; + /** * The name of the created image. * @return the image name @@ -260,6 +262,9 @@ private BuildRequest customize(BuildRequest request) { if (StringUtils.hasText(this.applicationDirectory)) { request = request.withApplicationDirectory(this.applicationDirectory); } + if (this.securityOptions != null) { + request = request.withSecurityOptions(this.securityOptions); + } return request; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java index a829b25dbfb2..1ec018db8608 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.function.Function; import org.apache.maven.artifact.Artifact; @@ -234,6 +235,22 @@ void getBuildRequestWhenHasApplicationDirectoryUsesApplicationDirectory() { assertThat(request.getApplicationDirectory()).isEqualTo("/application"); } + @Test + void getBuildRequestWhenHasSecurityOptionsUsesSecurityOptions() { + Image image = new Image(); + image.securityOptions = List.of("label=user:USER", "label=role:ROLE"); + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getSecurityOptions()).containsExactly("label=user:USER", "label=role:ROLE"); + } + + @Test + void getBuildRequestWhenHasEmptySecurityOptionsUsesSecurityOptions() { + Image image = new Image(); + image.securityOptions = Collections.emptyList(); + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getSecurityOptions()).isEmpty(); + } + private Artifact createArtifact() { return new DefaultArtifact("com.example", "my-app", VersionRange.createFromVersion("0.0.1-SNAPSHOT"), "compile", "jar", null, new DefaultArtifactHandler()); From f55184a9980e0d513e4d1f44ab72473836b9e51f Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 20 Sep 2023 17:16:31 -0700 Subject: [PATCH 0489/1215] Update copyright year of changed files --- .../boot/autoconfigure/amqp/RabbitStreamConfiguration.java | 2 +- .../test/autoconfigure/jdbc/ExampleJdbcClientRepository.java | 2 +- .../springframework/boot/reactor/InstrumentedFluxProvider.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfiguration.java index dfacce8455ae..569cdb2bf664 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleJdbcClientRepository.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleJdbcClientRepository.java index 3ba42164716e..a104017292ab 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleJdbcClientRepository.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jdbc/ExampleJdbcClientRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-2023 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. diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/InstrumentedFluxProvider.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/InstrumentedFluxProvider.java index d9382b364b41..eaf3abfa3401 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/InstrumentedFluxProvider.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/reactor/InstrumentedFluxProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. From 433bd337f47145066007e4dc779448e7e9a08e97 Mon Sep 17 00:00:00 2001 From: Vedran Pavic Date: Mon, 18 Sep 2023 21:18:40 +0200 Subject: [PATCH 0490/1215] Rename JMS listener minimum concurrency property This commit renames `spring.jms.listener.concurrency` property to `spring.jms.listener.min-concurrency` in order to better align it with `spring.jms.listener.max-concurrency`. See gh-37451 --- .../boot/autoconfigure/jms/JmsProperties.java | 24 ++++++++++++++----- .../jms/JmsAutoConfigurationTests.java | 2 +- .../autoconfigure/jms/JmsPropertiesTests.java | 4 ++-- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java index a4fdc8550366..7b0cb61d45ed 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java @@ -19,6 +19,7 @@ import java.time.Duration; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; /** * Configuration properties for JMS. @@ -148,7 +149,7 @@ public static class Listener { /** * Minimum number of concurrent consumers. */ - private Integer concurrency; + private Integer minConcurrency; /** * Maximum number of concurrent consumers. @@ -178,12 +179,23 @@ public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) { this.acknowledgeMode = acknowledgeMode; } + @DeprecatedConfigurationProperty(replacement = "spring.jms.listener.min-concurrency", since = "3.2.0") + @Deprecated(since = "3.2.0", forRemoval = true) public Integer getConcurrency() { - return this.concurrency; + return this.minConcurrency; } + @Deprecated(since = "3.2.0", forRemoval = true) public void setConcurrency(Integer concurrency) { - this.concurrency = concurrency; + this.minConcurrency = concurrency; + } + + public Integer getMinConcurrency() { + return this.minConcurrency; + } + + public void setMinConcurrency(Integer minConcurrency) { + this.minConcurrency = minConcurrency; } public Integer getMaxConcurrency() { @@ -195,11 +207,11 @@ public void setMaxConcurrency(Integer maxConcurrency) { } public String formatConcurrency() { - if (this.concurrency == null) { + if (this.minConcurrency == null) { return (this.maxConcurrency != null) ? "1-" + this.maxConcurrency : null; } - return ((this.maxConcurrency != null) ? this.concurrency + "-" + this.maxConcurrency - : String.valueOf(this.concurrency)); + return ((this.maxConcurrency != null) ? this.minConcurrency + "-" + this.maxConcurrency + : String.valueOf(this.minConcurrency)); } public Duration getReceiveTimeout() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java index 17692a1b7c62..e89c07125438 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java @@ -143,7 +143,7 @@ void jmsListenerContainerFactoryWhenMultipleConnectionFactoryBeansShouldBackOff( void testJmsListenerContainerFactoryWithCustomSettings() { this.contextRunner.withUserConfiguration(EnableJmsConfiguration.class) .withPropertyValues("spring.jms.listener.autoStartup=false", "spring.jms.listener.acknowledgeMode=client", - "spring.jms.listener.concurrency=2", "spring.jms.listener.receiveTimeout=2s", + "spring.jms.listener.minConcurrency=2", "spring.jms.listener.receiveTimeout=2s", "spring.jms.listener.maxConcurrency=10") .run(this::testJmsListenerContainerFactoryWithCustomSettings); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsPropertiesTests.java index 7ddbecd5c8b2..a30c95d604fe 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsPropertiesTests.java @@ -40,7 +40,7 @@ void formatConcurrencyNull() { @Test void formatConcurrencyOnlyLowerBound() { JmsProperties properties = new JmsProperties(); - properties.getListener().setConcurrency(2); + properties.getListener().setMinConcurrency(2); assertThat(properties.getListener().formatConcurrency()).isEqualTo("2"); } @@ -54,7 +54,7 @@ void formatConcurrencyOnlyHigherBound() { @Test void formatConcurrencyBothBounds() { JmsProperties properties = new JmsProperties(); - properties.getListener().setConcurrency(2); + properties.getListener().setMinConcurrency(2); properties.getListener().setMaxConcurrency(10); assertThat(properties.getListener().formatConcurrency()).isEqualTo("2-10"); } From c5e221143f7b049f5e715407848ab1bff644f867 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Thu, 21 Sep 2023 12:33:02 -0500 Subject: [PATCH 0491/1215] Ignore AOT-related deprecation warnings in Paketo system tests Closes gh-37433 --- .../springframework/boot/image/paketo/PaketoBuilderTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java b/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java index 1778b5d8b766..a6b75a03f72d 100644 --- a/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java +++ b/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java @@ -298,6 +298,7 @@ void plainWarApp() throws Exception { @EnabledForJreRange(max = JRE.JAVA_17) void nativeApp() throws Exception { this.gradleBuild.expectDeprecationMessages("uses or overrides a deprecated API"); + this.gradleBuild.expectDeprecationMessages("has been deprecated and marked for removal"); writeMainClass(); String imageName = "paketo-integration/" + this.gradleBuild.getProjectDir().getName(); ImageReference imageReference = ImageReference.of(ImageName.of(imageName)); From ee9c74556d45b39f097aff8a5fa65c6394d060b5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 25 Sep 2023 11:51:58 +0100 Subject: [PATCH 0492/1215] Make reactive security back off without authentication manager If there's no authentication manager bean or no bean from which one can be created, Spring Security's reactive support may fail to bootstrap due to a null authentication manager. This commit causes the auto-configuration that enables WebFlux security to back off in the absence of an AuthenticationManager bean and a ReactiveUserDetailsService (from which Spring Security can create an AuthenticationManager) bean. Other reactive security auto-configuration that can configure things such that WebFlux security can be bootstrapped without an AuthenticationManager has been updated to enable WebFlux security rather than relying on another auto-configuration class to do so. Fixes gh-37504 --- ...eOAuth2ResourceServerJwkConfiguration.java | 9 +++++++ ...esourceServerOpaqueTokenConfiguration.java | 9 +++++++ .../ReactiveSecurityAutoConfiguration.java | 26 ++++++++++++++++++- ...veUserDetailsServiceAutoConfiguration.java | 4 +-- ...2ResourceServerAutoConfigurationTests.java | 2 +- ...eactiveSecurityAutoConfigurationTests.java | 8 ++++++ 6 files changed, 54 insertions(+), 4 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java index 5f5cba160eaa..31cb13aa60cc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java @@ -35,6 +35,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity.OAuth2ResourceServerSpec; import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; @@ -49,6 +50,7 @@ import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder; import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.WebFilterChainProxy; import org.springframework.util.CollectionUtils; /** @@ -177,6 +179,13 @@ private void customDecoder(OAuth2ResourceServerSpec server, ReactiveJwtDecoder d server.jwt((jwt) -> jwt.jwtDecoder(decoder)); } + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(WebFilterChainProxy.class) + @EnableWebFluxSecurity + static class EnableWebFluxSecurityConfiguration { + + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java index f4d9614253e8..dbeb778d8764 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java @@ -22,10 +22,12 @@ import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; import org.springframework.security.oauth2.server.resource.introspection.SpringReactiveOpaqueTokenIntrospector; import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.WebFilterChainProxy; import static org.springframework.security.config.Customizer.withDefaults; @@ -64,6 +66,13 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { return http.build(); } + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(WebFilterChainProxy.class) + @EnableWebFluxSecurity + static class EnableWebFluxSecurityConfiguration { + + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfiguration.java index f995f66cdd69..b4d4394bfe4a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -20,13 +20,18 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.web.server.WebFilterChainProxy; import org.springframework.web.reactive.config.WebFluxConfigurer; @@ -49,9 +54,28 @@ public class ReactiveSecurityAutoConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(WebFilterChainProxy.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) + @Conditional(ReactiveAuthenticationManagerCondition.class) @EnableWebFluxSecurity static class EnableWebFluxSecurityConfiguration { } + static final class ReactiveAuthenticationManagerCondition extends AnyNestedCondition { + + ReactiveAuthenticationManagerCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnBean(AuthenticationManager.class) + static final class ConditionalOnAuthenticationManagerBean { + + } + + @ConditionalOnBean(ReactiveUserDetailsService.class) + static final class ConditionalOnReactiveUserDetailsService { + + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration.java index c2f4ce2a7323..1c2755526370 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -55,7 +55,7 @@ * @author Madhura Bhave * @since 2.0.0 */ -@AutoConfiguration(after = RSocketMessagingAutoConfiguration.class) +@AutoConfiguration(before = ReactiveSecurityAutoConfiguration.class, after = RSocketMessagingAutoConfiguration.class) @ConditionalOnClass({ ReactiveAuthenticationManager.class }) @ConditionalOnMissingBean( value = { ReactiveAuthenticationManager.class, ReactiveUserDetailsService.class, diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java index e8165ee18916..9583efcbc450 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java @@ -740,7 +740,6 @@ private Consumer> audClaimValidator() { .isEqualTo("aud"); } - @EnableWebFluxSecurity static class TestConfig { @Bean @@ -782,6 +781,7 @@ ReactiveOpaqueTokenIntrospector decoder() { } + @EnableWebFluxSecurity @Configuration(proxyBeanMethods = false) static class SecurityWebFilterChainConfig { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java index 4436ab7360e0..54bdfb739af3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java @@ -20,6 +20,7 @@ import reactor.core.publisher.Flux; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration.EnableWebFluxSecurityConfiguration; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -47,6 +48,13 @@ void backsOffWhenWebFilterChainProxyBeanPresent() { .run((context) -> assertThat(context).hasSingleBean(WebFilterChainProxy.class)); } + @Test + void backsOffWhenReactiveAuthenticationManagerNotPresent() { + this.contextRunner.withConfiguration(AutoConfigurations.of(ReactiveSecurityAutoConfiguration.class)) + .run((context) -> assertThat(context).hasSingleBean(ReactiveSecurityAutoConfiguration.class) + .doesNotHaveBean(EnableWebFluxSecurityConfiguration.class)); + } + @Test void enablesWebFluxSecurity() { this.contextRunner From ab06d10d642b5bacdf0206b2894394a805876d84 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 26 Sep 2023 12:31:12 +0100 Subject: [PATCH 0493/1215] Fix checkpoint-restore with replaced or wrapped HikariDataSource Closes gh-37580 --- .../jdbc/DataSourceConfiguration.java | 2 +- .../HikariDataSourceConfigurationTests.java | 31 +++++++++++++++++++ .../HikariCheckpointRestoreLifecycle.java | 14 ++++++--- ...HikariCheckpointRestoreLifecycleTests.java | 21 +++++++++++++ 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java index d0a1098e2e1a..6389876a24e7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java @@ -126,7 +126,7 @@ HikariDataSource dataSource(DataSourceProperties properties, JdbcConnectionDetai @Bean @ConditionalOnCheckpointRestore - HikariCheckpointRestoreLifecycle hikariCheckpointRestoreLifecycle(HikariDataSource hikariDataSource) { + HikariCheckpointRestoreLifecycle hikariCheckpointRestoreLifecycle(DataSource hikariDataSource) { return new HikariCheckpointRestoreLifecycle(hikariDataSource); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java index 0793585ad383..617f2764562e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java @@ -22,12 +22,15 @@ import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.jdbc.HikariCheckpointRestoreLifecycle; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.testsupport.classpath.ClassPathOverrides; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.DelegatingDataSource; import static org.assertj.core.api.Assertions.assertThat; @@ -131,6 +134,14 @@ void whenCheckpointRestoreIsAvailableHikariAutoConfigRegistersLifecycleBean() { .run((context) -> assertThat(context).hasSingleBean(HikariCheckpointRestoreLifecycle.class)); } + @Test + @ClassPathOverrides("org.crac:crac:1.3.0") + void whenCheckpointRestoreIsAvailableAndDataSourceHasBeenWrappedHikariAutoConfigRegistersLifecycleBean() { + this.contextRunner.withUserConfiguration(DataSourceWrapperConfiguration.class) + .withPropertyValues("spring.datasource.type=" + HikariDataSource.class.getName()) + .run((context) -> assertThat(context).hasSingleBean(HikariCheckpointRestoreLifecycle.class)); + } + @Test void whenCheckpointRestoreIsNotAvailableHikariAutoConfigDoesNotRegisterLifecycleBean() { this.contextRunner.withPropertyValues("spring.datasource.type=" + HikariDataSource.class.getName()) @@ -147,4 +158,24 @@ JdbcConnectionDetails sqlConnectionDetails() { } + @Configuration(proxyBeanMethods = false) + static class DataSourceWrapperConfiguration { + + @Bean + static BeanPostProcessor dataSourceWrapper() { + return new BeanPostProcessor() { + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof DataSource dataSource) { + return new DelegatingDataSource(dataSource); + } + return bean; + } + + }; + } + + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/HikariCheckpointRestoreLifecycle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/HikariCheckpointRestoreLifecycle.java index 4f458e9f5af5..40585e4885d1 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/HikariCheckpointRestoreLifecycle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/HikariCheckpointRestoreLifecycle.java @@ -25,6 +25,8 @@ import java.util.concurrent.TimeoutException; import java.util.function.Function; +import javax.sql.DataSource; + import com.zaxxer.hikari.HikariConfigMXBean; import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariPoolMXBean; @@ -71,10 +73,12 @@ public class HikariCheckpointRestoreLifecycle implements Lifecycle { /** * Creates a new {@code HikariCheckpointRestoreLifecycle} that will allow the given - * {@code dataSource} to participate in checkpoint-restore. + * {@code dataSource} to participate in checkpoint-restore. The {@code dataSource} is + * {@link DataSourceUnwrapper#unwrap unwrapped} to a {@link HikariDataSource}. If such + * unwrapping is not possible, the lifecycle will have no effect. * @param dataSource the checkpoint-restore participant */ - public HikariCheckpointRestoreLifecycle(HikariDataSource dataSource) { + public HikariCheckpointRestoreLifecycle(DataSource dataSource) { this.dataSource = DataSourceUnwrapper.unwrap(dataSource, HikariConfigMXBean.class, HikariDataSource.class); this.hasOpenConnections = (pool) -> { ThreadPoolExecutor closeConnectionExecutor = (ThreadPoolExecutor) ReflectionUtils @@ -86,7 +90,7 @@ public HikariCheckpointRestoreLifecycle(HikariDataSource dataSource) { @Override public void start() { - if (this.dataSource.isRunning()) { + if (this.dataSource == null || this.dataSource.isRunning()) { return; } Assert.state(!this.dataSource.isClosed(), "DataSource has been closed and cannot be restarted"); @@ -98,7 +102,7 @@ public void start() { @Override public void stop() { - if (!this.dataSource.isRunning()) { + if (this.dataSource == null || !this.dataSource.isRunning()) { return; } if (this.dataSource.isAllowPoolSuspension()) { @@ -143,7 +147,7 @@ private void waitForConnectionsToClose() { @Override public boolean isRunning() { - return this.dataSource.isRunning(); + return this.dataSource != null && this.dataSource.isRunning(); } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/HikariCheckpointRestoreLifecycleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/HikariCheckpointRestoreLifecycleTests.java index 05811c5d593d..119af02e6021 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/HikariCheckpointRestoreLifecycleTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/HikariCheckpointRestoreLifecycleTests.java @@ -18,6 +18,8 @@ import java.util.UUID; +import javax.sql.DataSource; + import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.junit.jupiter.api.Test; @@ -25,6 +27,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.mockito.Mockito.mock; /** * Tests for {@link HikariCheckpointRestoreLifecycle}. @@ -82,4 +85,22 @@ void whenDataSourceIsClosedThenStartShouldThrow() { assertThatExceptionOfType(RuntimeException.class).isThrownBy(this.lifecycle::start); } + @Test + void startHasNoEffectWhenDataSourceIsNotAHikariDataSource() { + HikariCheckpointRestoreLifecycle nonHikariLifecycle = new HikariCheckpointRestoreLifecycle( + mock(DataSource.class)); + assertThat(nonHikariLifecycle.isRunning()).isFalse(); + nonHikariLifecycle.start(); + assertThat(nonHikariLifecycle.isRunning()).isFalse(); + } + + @Test + void stopHasNoEffectWhenDataSourceIsNotAHikariDataSource() { + HikariCheckpointRestoreLifecycle nonHikariLifecycle = new HikariCheckpointRestoreLifecycle( + mock(DataSource.class)); + assertThat(nonHikariLifecycle.isRunning()).isFalse(); + nonHikariLifecycle.stop(); + assertThat(nonHikariLifecycle.isRunning()).isFalse(); + } + } From ecc670772a701a6c90b9b29f493f45f03d76512d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 20 Sep 2023 17:27:33 +0100 Subject: [PATCH 0494/1215] Make user details service auto-configs back off more readily Previously auto-configuration of a user details service (imperative or reactive) would only back off on the presence of certain beans. This led to situations where the im-memory service was auto-configured and the default password was logged even though another authentication mechanism was in use. This commit updates the auto-configuration so that it backs off when depending on Spring Security's OAuth2 Client and OAuth2 Resource Server modules. In the imperative case it will also back off when depending on the SAML 2 provider. Closes gh-35338 --- ...activeHealthEndpointWebExtensionTests.java | 23 +++++++--- ...FoundryActuatorAutoConfigurationTests.java | 33 +++++++++----- ...intsAutoConfigurationIntegrationTests.java | 2 + ...mentWebSecurityAutoConfigurationTests.java | 18 ++++++-- ...stractEndpointRequestIntegrationTests.java | 13 ++++-- .../ReactiveOAuth2ClientConfigurations.java | 9 ++++ ...veUserDetailsServiceAutoConfiguration.java | 6 ++- .../UserDetailsServiceAutoConfiguration.java | 17 +++---- ...orizationServerAutoConfigurationTests.java | 6 ++- ...eactiveSecurityAutoConfigurationTests.java | 31 ++++++++----- ...rDetailsServiceAutoConfigurationTests.java | 42 ++++++++++------- ...RSocketSecurityAutoConfigurationTests.java | 22 +++++++-- ...ConfigurationEarlyInitializationTests.java | 7 ++- ...rDetailsServiceAutoConfigurationTests.java | 45 ++++++++++++++----- .../docs/asciidoc/web/spring-security.adoc | 19 +++++++- 15 files changed, 215 insertions(+), 78 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtensionTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtensionTests.java index e4eeaedbd9be..9c68348c9fcf 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtensionTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/CloudFoundryReactiveHealthEndpointWebExtensionTests.java @@ -35,10 +35,13 @@ import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; -import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; import static org.assertj.core.api.Assertions.assertThat; @@ -52,15 +55,14 @@ class CloudFoundryReactiveHealthEndpointWebExtensionTests { private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() .withPropertyValues("VCAP_APPLICATION={}") .withConfiguration(AutoConfigurations.of(ReactiveSecurityAutoConfiguration.class, - ReactiveUserDetailsServiceAutoConfiguration.class, WebFluxAutoConfiguration.class, - JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class, + WebFluxAutoConfiguration.class, JacksonAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class, ReactiveCloudFoundryActuatorAutoConfigurationTests.WebClientCustomizerConfig.class, WebClientAutoConfiguration.class, ManagementContextAutoConfiguration.class, EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, HealthContributorAutoConfiguration.class, HealthEndpointAutoConfiguration.class, ReactiveCloudFoundryActuatorAutoConfiguration.class)) - .withUserConfiguration(TestHealthIndicator.class); + .withUserConfiguration(TestHealthIndicator.class, UserDetailsServiceConfiguration.class); @Test void healthComponentsAlwaysPresent() { @@ -82,4 +84,15 @@ public Health health() { } + @Configuration(proxyBeanMethods = false) + static class UserDetailsServiceConfiguration { + + @Bean + MapReactiveUserDetailsService userDetailsService() { + return new MapReactiveUserDetailsService( + User.withUsername("alice").password("secret").roles("admin").build()); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java index 2f4c99c0bc3a..e9424f872c66 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/reactive/ReactiveCloudFoundryActuatorAutoConfigurationTests.java @@ -50,7 +50,6 @@ import org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; -import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; @@ -61,6 +60,8 @@ import org.springframework.http.HttpMethod; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.WebFilterChainProxy; import org.springframework.test.util.ReflectionTestUtils; @@ -84,15 +85,16 @@ class ReactiveCloudFoundryActuatorAutoConfigurationTests { private static final String V3_JSON = ApiVersion.V3.getProducedMimeType().toString(); private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(ReactiveSecurityAutoConfiguration.class, - ReactiveUserDetailsServiceAutoConfiguration.class, WebFluxAutoConfiguration.class, - JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class, WebClientCustomizerConfig.class, - WebClientAutoConfiguration.class, ManagementContextAutoConfiguration.class, - EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, - HealthContributorAutoConfiguration.class, HealthEndpointAutoConfiguration.class, - InfoContributorAutoConfiguration.class, InfoEndpointAutoConfiguration.class, - ProjectInfoAutoConfiguration.class, ReactiveCloudFoundryActuatorAutoConfiguration.class)); + .withConfiguration( + AutoConfigurations.of(ReactiveSecurityAutoConfiguration.class, WebFluxAutoConfiguration.class, + JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, WebClientCustomizerConfig.class, + WebClientAutoConfiguration.class, ManagementContextAutoConfiguration.class, + EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, + HealthContributorAutoConfiguration.class, HealthEndpointAutoConfiguration.class, + InfoContributorAutoConfiguration.class, InfoEndpointAutoConfiguration.class, + ProjectInfoAutoConfiguration.class, ReactiveCloudFoundryActuatorAutoConfiguration.class)) + .withUserConfiguration(UserDetailsServiceConfiguration.class); private static final String BASE_PATH = "/cloudfoundryapplication"; @@ -358,4 +360,15 @@ WebClientCustomizer webClientCustomizer() { } + @Configuration(proxyBeanMethods = false) + static class UserDetailsServiceConfiguration { + + @Bean + MapReactiveUserDetailsService userDetailsService() { + return new MapReactiveUserDetailsService( + User.withUsername("alice").password("secret").roles("admin").build()); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebEndpointsAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebEndpointsAutoConfigurationIntegrationTests.java index 33aa8931dd26..c131bd263a4e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebEndpointsAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebEndpointsAutoConfigurationIntegrationTests.java @@ -42,6 +42,7 @@ import org.springframework.boot.context.annotation.UserConfigurations; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; import static org.assertj.core.api.Assertions.assertThat; @@ -59,6 +60,7 @@ void healthEndpointWebExtensionIsAutoConfigured() { } @Test + @ClassPathExclusions({ "spring-security-oauth2-client-*.jar", "spring-security-oauth2-resource-server-*.jar" }) void healthEndpointReactiveWebExtensionIsAutoConfigured() { reactiveWebRunner() .run((context) -> assertThat(context).hasSingleBean(ReactiveHealthEndpointWebExtension.class)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfigurationTests.java index fbfe9f23efe9..e41082b893a4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfigurationTests.java @@ -33,7 +33,6 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration; import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; -import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; @@ -48,6 +47,8 @@ import org.springframework.mock.http.server.reactive.MockServerHttpResponse; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.WebFilterChainProxy; import org.springframework.web.server.ServerWebExchange; @@ -70,8 +71,8 @@ class ReactiveManagementWebSecurityAutoConfigurationTests { HealthEndpointAutoConfiguration.class, InfoEndpointAutoConfiguration.class, WebFluxAutoConfiguration.class, EnvironmentEndpointAutoConfiguration.class, EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, - ReactiveSecurityAutoConfiguration.class, ReactiveUserDetailsServiceAutoConfiguration.class, - ReactiveManagementWebSecurityAutoConfiguration.class)); + ReactiveSecurityAutoConfiguration.class, ReactiveManagementWebSecurityAutoConfiguration.class)) + .withUserConfiguration(UserDetailsServiceConfiguration.class); @Test void permitAllForHealth() { @@ -155,6 +156,17 @@ protected ServerWebExchange createExchange(ServerHttpRequest request, ServerHttp } + @Configuration(proxyBeanMethods = false) + static class UserDetailsServiceConfiguration { + + @Bean + MapReactiveUserDetailsService userDetailsService() { + return new MapReactiveUserDetailsService( + User.withUsername("alice").password("secret").roles("admin").build()); + } + + } + @Configuration(proxyBeanMethods = false) static class CustomSecurityConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/AbstractEndpointRequestIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/AbstractEndpointRequestIntegrationTests.java index 35236a3c5696..9417437d92f7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/AbstractEndpointRequestIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/servlet/AbstractEndpointRequestIntegrationTests.java @@ -37,7 +37,6 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; @@ -45,6 +44,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.test.web.reactive.server.WebTestClient; @@ -100,8 +101,8 @@ protected final WebApplicationContextRunner getContextRunner() { return createContextRunner().withPropertyValues("management.endpoints.web.exposure.include=*") .withUserConfiguration(BaseConfiguration.class, SecurityConfiguration.class) .withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class, SecurityAutoConfiguration.class, - UserDetailsServiceAutoConfiguration.class, EndpointAutoConfiguration.class, - WebEndpointAutoConfiguration.class, ManagementContextAutoConfiguration.class)); + EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, + ManagementContextAutoConfiguration.class)); } @@ -189,6 +190,12 @@ public EndpointServlet get() { @Configuration(proxyBeanMethods = false) static class SecurityConfiguration { + @Bean + InMemoryUserDetailsManager userDetailsManager() { + return new InMemoryUserDetailsManager( + User.withUsername("user").password("{noop}password").roles("admin").build()); + } + @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests((requests) -> { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientConfigurations.java index 8eb3871b9377..8d25fbc2e345 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientConfigurations.java @@ -28,6 +28,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; @@ -37,6 +38,7 @@ import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.WebFilterChainProxy; import static org.springframework.security.config.Customizer.withDefaults; @@ -92,6 +94,13 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { return http.build(); } + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(WebFilterChainProxy.class) + @EnableWebFluxSecurity + static class EnableWebFluxSecurityConfiguration { + + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration.java index 1c2755526370..17d6e1315f54 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration.java @@ -28,6 +28,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration; import org.springframework.boot.autoconfigure.security.SecurityProperties; @@ -57,11 +58,12 @@ */ @AutoConfiguration(before = ReactiveSecurityAutoConfiguration.class, after = RSocketMessagingAutoConfiguration.class) @ConditionalOnClass({ ReactiveAuthenticationManager.class }) +@ConditionalOnMissingClass({ "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository", + "org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector" }) @ConditionalOnMissingBean( value = { ReactiveAuthenticationManager.class, ReactiveUserDetailsService.class, ReactiveAuthenticationManagerResolver.class }, - type = { "org.springframework.security.oauth2.jwt.ReactiveJwtDecoder", - "org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector" }) + type = { "org.springframework.security.oauth2.jwt.ReactiveJwtDecoder" }) @Conditional(ReactiveUserDetailsServiceAutoConfiguration.ReactiveUserDetailsServiceCondition.class) @EnableConfigurationProperties(SecurityProperties.class) public class ReactiveUserDetailsServiceAutoConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfiguration.java index 55c3dec9a6a7..396b50856c34 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfiguration.java @@ -28,6 +28,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.context.annotation.Bean; import org.springframework.security.authentication.AuthenticationManager; @@ -43,9 +44,7 @@ /** * {@link EnableAutoConfiguration Auto-configuration} for a Spring Security in-memory * {@link AuthenticationManager}. Adds an {@link InMemoryUserDetailsManager} with a - * default user and generated password. This can be disabled by providing a bean of type - * {@link AuthenticationManager}, {@link AuthenticationProvider} or - * {@link UserDetailsService}. + * default user and generated password. * * @author Dave Syer * @author Rob Winch @@ -54,14 +53,12 @@ */ @AutoConfiguration @ConditionalOnClass(AuthenticationManager.class) +@ConditionalOnMissingClass({ "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository", + "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector", + "org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository" }) @ConditionalOnBean(ObjectPostProcessor.class) -@ConditionalOnMissingBean( - value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class, - AuthenticationManagerResolver.class }, - type = { "org.springframework.security.oauth2.jwt.JwtDecoder", - "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector", - "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository", - "org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository" }) +@ConditionalOnMissingBean(value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class, + AuthenticationManagerResolver.class }, type = "org.springframework.security.oauth2.jwt.JwtDecoder") public class UserDetailsServiceAutoConfiguration { private static final String NOOP_PASSWORD_PREFIX = "{noop}"; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerAutoConfigurationTests.java index 3f2a89e73769..c1c043eecdb2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/server/servlet/OAuth2AuthorizationServerAutoConfigurationTests.java @@ -23,6 +23,7 @@ import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.core.AuthorizationGrantType; @@ -59,8 +60,11 @@ void autoConfigurationConditionalOnClassOauth2Authorization() { } @Test + @ClassPathExclusions({ "spring-security-oauth2-client-*.jar", "spring-security-oauth2-resource-server-*.jar", + "spring-security-saml2-service-provider-*.jar" }) void autoConfigurationDoesNotCauseUserDetailsServiceToBackOff() { - this.contextRunner.run((context) -> assertThat(context).hasBean("inMemoryUserDetailsManager")); + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(UserDetailsServiceAutoConfiguration.class) + .hasBean("inMemoryUserDetailsManager")); } @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java index 54bdfb739af3..b046e89d970c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java @@ -26,6 +26,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; import org.springframework.security.web.server.WebFilterChainProxy; import org.springframework.web.reactive.config.WebFluxConfigurer; @@ -39,27 +41,24 @@ */ class ReactiveSecurityAutoConfigurationTests { - private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner(); + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactiveSecurityAutoConfiguration.class)); @Test void backsOffWhenWebFilterChainProxyBeanPresent() { - this.contextRunner.withConfiguration(AutoConfigurations.of(ReactiveSecurityAutoConfiguration.class)) - .withUserConfiguration(WebFilterChainProxyConfiguration.class) + this.contextRunner.withUserConfiguration(WebFilterChainProxyConfiguration.class) .run((context) -> assertThat(context).hasSingleBean(WebFilterChainProxy.class)); } @Test void backsOffWhenReactiveAuthenticationManagerNotPresent() { - this.contextRunner.withConfiguration(AutoConfigurations.of(ReactiveSecurityAutoConfiguration.class)) - .run((context) -> assertThat(context).hasSingleBean(ReactiveSecurityAutoConfiguration.class) - .doesNotHaveBean(EnableWebFluxSecurityConfiguration.class)); + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ReactiveSecurityAutoConfiguration.class) + .doesNotHaveBean(EnableWebFluxSecurityConfiguration.class)); } @Test void enablesWebFluxSecurity() { - this.contextRunner - .withConfiguration(AutoConfigurations.of(ReactiveSecurityAutoConfiguration.class, - ReactiveUserDetailsServiceAutoConfiguration.class)) + this.contextRunner.withUserConfiguration(UserDetailsServiceConfiguration.class) .run((context) -> assertThat(context).getBean(WebFilterChainProxy.class).isNotNull()); } @@ -68,8 +67,7 @@ void autoConfigurationIsConditionalOnClass() { this.contextRunner .withClassLoader(new FilteredClassLoader(Flux.class, EnableWebFluxSecurity.class, WebFilterChainProxy.class, WebFluxConfigurer.class)) - .withConfiguration(AutoConfigurations.of(ReactiveSecurityAutoConfiguration.class, - ReactiveUserDetailsServiceAutoConfiguration.class)) + .withUserConfiguration(UserDetailsServiceConfiguration.class) .run((context) -> assertThat(context).doesNotHaveBean(WebFilterChainProxy.class)); } @@ -83,4 +81,15 @@ WebFilterChainProxy webFilterChainProxy() { } + @Configuration(proxyBeanMethods = false) + static class UserDetailsServiceConfiguration { + + @Bean + MapReactiveUserDetailsService userDetailsService() { + return new MapReactiveUserDetailsService( + User.withUsername("alice").password("secret").roles("admin").build()); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfigurationTests.java index e2389c322a89..96efa632a667 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfigurationTests.java @@ -25,6 +25,7 @@ import org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -39,6 +40,7 @@ import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; @@ -58,15 +60,21 @@ class ReactiveUserDetailsServiceAutoConfigurationTests { @Test void configuresADefaultUser() { - this.contextRunner.withUserConfiguration(TestSecurityConfiguration.class).run((context) -> { - ReactiveUserDetailsService userDetailsService = context.getBean(ReactiveUserDetailsService.class); - assertThat(userDetailsService.findByUsername("user").block(Duration.ofSeconds(30))).isNotNull(); - }); + this.contextRunner + .withClassLoader( + new FilteredClassLoader(ClientRegistrationRepository.class, ReactiveOpaqueTokenIntrospector.class)) + .withUserConfiguration(TestSecurityConfiguration.class) + .run((context) -> { + ReactiveUserDetailsService userDetailsService = context.getBean(ReactiveUserDetailsService.class); + assertThat(userDetailsService.findByUsername("user").block(Duration.ofSeconds(30))).isNotNull(); + }); } @Test void userDetailsServiceWhenRSocketConfigured() { new ApplicationContextRunner() + .withClassLoader( + new FilteredClassLoader(ClientRegistrationRepository.class, ReactiveOpaqueTokenIntrospector.class)) .withConfiguration(AutoConfigurations.of(ReactiveUserDetailsServiceAutoConfiguration.class, RSocketMessagingAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class)) .withUserConfiguration(TestRSocketSecurityConfiguration.class) @@ -109,20 +117,21 @@ void doesNotConfigureDefaultUserIfResourceServerWithJWTIsUsed() { } @Test - void doesNotConfigureDefaultUserIfResourceServerWithOpaqueIsUsed() { - this.contextRunner.withUserConfiguration(ReactiveOpaqueTokenIntrospectorConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(ReactiveOpaqueTokenIntrospector.class); - assertThat(context).doesNotHaveBean(ReactiveUserDetailsService.class); - }); + void doesNotConfigureDefaultUserIfResourceServerIsPresent() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ReactiveUserDetailsService.class)); } @Test void userDetailsServiceWhenPasswordEncoderAbsentAndDefaultPassword() { - this.contextRunner.withUserConfiguration(TestSecurityConfiguration.class).run(((context) -> { - MapReactiveUserDetailsService userDetailsService = context.getBean(MapReactiveUserDetailsService.class); - String password = userDetailsService.findByUsername("user").block(Duration.ofSeconds(30)).getPassword(); - assertThat(password).startsWith("{noop}"); - })); + this.contextRunner + .withClassLoader( + new FilteredClassLoader(ClientRegistrationRepository.class, ReactiveOpaqueTokenIntrospector.class)) + .withUserConfiguration(TestSecurityConfiguration.class) + .run(((context) -> { + MapReactiveUserDetailsService userDetailsService = context.getBean(MapReactiveUserDetailsService.class); + String password = userDetailsService.findByUsername("user").block(Duration.ofSeconds(30)).getPassword(); + assertThat(password).startsWith("{noop}"); + })); } @Test @@ -142,7 +151,10 @@ void userDetailsServiceWhenPasswordEncoderBeanPresent() { } private void testPasswordEncoding(Class configClass, String providedPassword, String expectedPassword) { - this.contextRunner.withUserConfiguration(configClass) + this.contextRunner + .withClassLoader( + new FilteredClassLoader(ClientRegistrationRepository.class, ReactiveOpaqueTokenIntrospector.class)) + .withUserConfiguration(configClass) .withPropertyValues("spring.security.user.password=" + providedPassword) .run(((context) -> { MapReactiveUserDetailsService userDetailsService = context.getBean(MapReactiveUserDetailsService.class); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/rsocket/RSocketSecurityAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/rsocket/RSocketSecurityAutoConfigurationTests.java index 95d807269000..a479bb5d1da7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/rsocket/RSocketSecurityAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/rsocket/RSocketSecurityAutoConfigurationTests.java @@ -22,12 +22,15 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration; import org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration; -import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration; import org.springframework.boot.rsocket.server.RSocketServerCustomizer; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler; import org.springframework.security.config.annotation.rsocket.RSocketSecurity; +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; import org.springframework.security.messaging.handler.invocation.reactive.AuthenticationPrincipalArgumentResolver; import org.springframework.security.rsocket.core.SecuritySocketAcceptorInterceptor; @@ -42,9 +45,9 @@ class RSocketSecurityAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(ReactiveUserDetailsServiceAutoConfiguration.class, - RSocketSecurityAutoConfiguration.class, RSocketMessagingAutoConfiguration.class, - RSocketStrategiesAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(RSocketSecurityAutoConfiguration.class, + RSocketMessagingAutoConfiguration.class, RSocketStrategiesAutoConfiguration.class)) + .withUserConfiguration(UserDetailsServiceConfiguration.class); @Test void autoConfigurationEnablesRSocketSecurity() { @@ -81,4 +84,15 @@ void autoConfigurationAddsCustomizerForAuthenticationPrincipalArgumentResolver() }); } + @Configuration(proxyBeanMethods = false) + static class UserDetailsServiceConfiguration { + + @Bean + MapReactiveUserDetailsService userDetailsService() { + return new MapReactiveUserDetailsService( + User.withUsername("alice").password("secret").roles("admin").build()); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/SecurityFilterAutoConfigurationEarlyInitializationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/SecurityFilterAutoConfigurationEarlyInitializationTests.java index c186f9c009c2..40bcf8bddc53 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/SecurityFilterAutoConfigurationEarlyInitializationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/SecurityFilterAutoConfigurationEarlyInitializationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. @@ -37,6 +37,8 @@ import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; +import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext; import org.springframework.context.annotation.Bean; @@ -63,6 +65,9 @@ class SecurityFilterAutoConfigurationEarlyInitializationTests { Pattern.MULTILINE); @Test + @DirtiesUrlFactories + @ClassPathExclusions({ "spring-security-oauth2-client-*.jar", "spring-security-oauth2-resource-server-*.jar", + "spring-security-saml2-service-provider-*.jar" }) void testSecurityFilterDoesNotCauseEarlyInitialization(CapturedOutput output) { try (AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext()) { TestPropertyValues.of("server.port:0").applyTo(context); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfigurationTests.java index b0375a3afc85..660a2ea97247 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfigurationTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.security.servlet; import java.util.Collections; +import java.util.function.Function; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -24,6 +25,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; @@ -64,7 +66,7 @@ class UserDetailsServiceAutoConfigurationTests { @Test void testDefaultUsernamePassword(CapturedOutput output) { - this.contextRunner.run((context) -> { + this.contextRunner.with(noOtherFormsOfAuthenticationOnTheClasspath()).run((context) -> { UserDetailsService manager = context.getBean(UserDetailsService.class); assertThat(output).contains("Using generated security password:"); assertThat(manager.loadUserByUsername("user")).isNotNull(); @@ -126,11 +128,13 @@ void defaultUserNotCreatedIfResourceServerWithJWTIsUsed() { @Test void userDetailsServiceWhenPasswordEncoderAbsentAndDefaultPassword() { - this.contextRunner.withUserConfiguration(TestSecurityConfiguration.class).run(((context) -> { - InMemoryUserDetailsManager userDetailsService = context.getBean(InMemoryUserDetailsManager.class); - String password = userDetailsService.loadUserByUsername("user").getPassword(); - assertThat(password).startsWith("{noop}"); - })); + this.contextRunner.with(noOtherFormsOfAuthenticationOnTheClasspath()) + .withUserConfiguration(TestSecurityConfiguration.class) + .run(((context) -> { + InMemoryUserDetailsManager userDetailsService = context.getBean(InMemoryUserDetailsManager.class); + String password = userDetailsService.loadUserByUsername("user").getPassword(); + assertThat(password).startsWith("{noop}"); + })); } @Test @@ -150,20 +154,39 @@ void userDetailsServiceWhenPasswordEncoderBeanPresent() { } @Test - void userDetailsServiceWhenClientRegistrationRepositoryBeanPresent() { - this.contextRunner.withUserConfiguration(TestConfigWithClientRegistrationRepository.class) + void userDetailsServiceWhenClientRegistrationRepositoryPresent() { + this.contextRunner + .withClassLoader( + new FilteredClassLoader(OpaqueTokenIntrospector.class, RelyingPartyRegistrationRepository.class)) + .run(((context) -> assertThat(context).doesNotHaveBean(InMemoryUserDetailsManager.class))); + } + + @Test + void userDetailsServiceWhenOpaqueTokenIntrospectorPresent() { + this.contextRunner + .withClassLoader(new FilteredClassLoader(ClientRegistrationRepository.class, + RelyingPartyRegistrationRepository.class)) .run(((context) -> assertThat(context).doesNotHaveBean(InMemoryUserDetailsManager.class))); } @Test - void userDetailsServiceWhenRelyingPartyRegistrationRepositoryBeanPresent() { + void userDetailsServiceWhenRelyingPartyRegistrationRepositoryPresent() { this.contextRunner - .withBean(RelyingPartyRegistrationRepository.class, () -> mock(RelyingPartyRegistrationRepository.class)) + .withClassLoader(new FilteredClassLoader(ClientRegistrationRepository.class, OpaqueTokenIntrospector.class)) .run(((context) -> assertThat(context).doesNotHaveBean(InMemoryUserDetailsManager.class))); } + private Function noOtherFormsOfAuthenticationOnTheClasspath() { + return (contextRunner) -> contextRunner + .withClassLoader(new FilteredClassLoader(ClientRegistrationRepository.class, OpaqueTokenIntrospector.class, + RelyingPartyRegistrationRepository.class)); + } + private void testPasswordEncoding(Class configClass, String providedPassword, String expectedPassword) { - this.contextRunner.withUserConfiguration(configClass) + this.contextRunner.with(noOtherFormsOfAuthenticationOnTheClasspath()) + .withClassLoader(new FilteredClassLoader(ClientRegistrationRepository.class, OpaqueTokenIntrospector.class, + RelyingPartyRegistrationRepository.class)) + .withUserConfiguration(configClass) .withPropertyValues("spring.security.user.password=" + providedPassword) .run(((context) -> { InMemoryUserDetailsManager userDetailsService = context.getBean(InMemoryUserDetailsManager.class); diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/spring-security.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/spring-security.adoc index 2fc187ddba33..b893db247226 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/spring-security.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/spring-security.adoc @@ -34,10 +34,18 @@ You can provide a different `AuthenticationEventPublisher` by adding a bean for === MVC Security The default security configuration is implemented in `SecurityAutoConfiguration` and `UserDetailsServiceAutoConfiguration`. `SecurityAutoConfiguration` imports `SpringBootWebSecurityConfiguration` for web security and `UserDetailsServiceAutoConfiguration` configures authentication, which is also relevant in non-web applications. -To switch off the default web application security configuration completely or to combine multiple Spring Security components such as OAuth2 Client and Resource Server, add a bean of type `SecurityFilterChain` (doing so does not disable the `UserDetailsService` configuration or Actuator's security). +To switch off the default web application security configuration completely or to combine multiple Spring Security components such as OAuth2 Client and Resource Server, add a bean of type `SecurityFilterChain` (doing so does not disable the `UserDetailsService` configuration or Actuator's security). To also switch off the `UserDetailsService` configuration, you can add a bean of type `UserDetailsService`, `AuthenticationProvider`, or `AuthenticationManager`. +The auto-configuration of a `UserDetailsService` will also back off any of the following Spring Security modules is on the classpath: + +- `spring-security-oauth2-client` +- `spring-security-oauth2-resource-server` +- `spring-security-saml2-service-provider` + +To use `UserDetailsService` in addition to one or more of these dependencies, define your own `InMemoryUserDetailsManager` bean. + Access rules can be overridden by adding a custom `SecurityFilterChain` bean. Spring Boot provides convenience methods that can be used to override access rules for actuator endpoints and static resources. `EndpointRequest` can be used to create a `RequestMatcher` that is based on the configprop:management.endpoints.web.base-path[] property. @@ -50,10 +58,17 @@ Spring Boot provides convenience methods that can be used to override access rul Similar to Spring MVC applications, you can secure your WebFlux applications by adding the `spring-boot-starter-security` dependency. The default security configuration is implemented in `ReactiveSecurityAutoConfiguration` and `UserDetailsServiceAutoConfiguration`. `ReactiveSecurityAutoConfiguration` imports `WebFluxSecurityConfiguration` for web security and `UserDetailsServiceAutoConfiguration` configures authentication, which is also relevant in non-web applications. -To switch off the default web application security configuration completely, you can add a bean of type `WebFilterChainProxy` (doing so does not disable the `UserDetailsService` configuration or Actuator's security). +To switch off the default web application security configuration completely, you can add a bean of type `WebFilterChainProxy` (doing so does not disable the `UserDetailsService` configuration or Actuator's security). To also switch off the `UserDetailsService` configuration, you can add a bean of type `ReactiveUserDetailsService` or `ReactiveAuthenticationManager`. +The auto-configuration will also back off when any of the following Spring Security modules is on the classpath: + +- `spring-security-oauth2-client` +- `spring-security-oauth2-resource-server` + +To use `ReactiveUserDetailsService` in addition to one or more of these dependencies, define your own `MapReactiveUserDetailsService` bean. + Access rules and the use of multiple Spring Security components such as OAuth 2 Client and Resource Server can be configured by adding a custom `SecurityWebFilterChain` bean. Spring Boot provides convenience methods that can be used to override access rules for actuator endpoints and static resources. `EndpointRequest` can be used to create a `ServerWebExchangeMatcher` that is based on the configprop:management.endpoints.web.base-path[] property. From 5ba4e2793d9178c44dd119b4f1b5d2c5a3fbb5c4 Mon Sep 17 00:00:00 2001 From: Vedran Pavic Date: Wed, 20 Sep 2023 22:42:19 +0200 Subject: [PATCH 0495/1215] Add properties for JmsTemplate session's ack mode and transacted flag See gh-37500 --- .../jms/JmsAutoConfiguration.java | 5 ++++ .../boot/autoconfigure/jms/JmsProperties.java | 27 +++++++++++++++++++ .../jms/JmsAutoConfigurationTests.java | 5 +++- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java index e256a0a63d47..21f6fd95a7e6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java @@ -47,6 +47,7 @@ * * @author Greg Turnquist * @author Stephane Nicoll + * @author Vedran Pavic * @since 1.0.0 */ @AutoConfiguration @@ -88,6 +89,10 @@ public JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) { private void mapTemplateProperties(Template properties, JmsTemplate template) { PropertyMapper map = PropertyMapper.get(); + map.from(properties::getAcknowledgeMode) + .whenNonNull() + .to((acknowledgeMode) -> template.setSessionAcknowledgeMode(acknowledgeMode.getMode())); + map.from(properties::getSessionTransacted).whenNonNull().to(template::setSessionTransacted); map.from(properties::getDefaultDestination).whenNonNull().to(template::setDefaultDestinationName); map.from(properties::getDeliveryDelay).whenNonNull().as(Duration::toMillis).to(template::setDeliveryDelay); map.from(properties::determineQosEnabled).to(template::setExplicitQosEnabled); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java index a59218f85a44..b5b4ec7d2efb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java @@ -27,6 +27,7 @@ * @author Greg Turnquist * @author Phillip Webb * @author Stephane Nicoll + * @author Vedran Pavic * @since 1.0.0 */ @ConfigurationProperties(prefix = "spring.jms") @@ -227,6 +228,16 @@ public void setReceiveTimeout(Duration receiveTimeout) { public static class Template { + /** + * Acknowledgement mode used when creating JMS sessions to send a message. + */ + private AcknowledgeMode acknowledgeMode; + + /** + * Whether to use transacted JMS sessions. + */ + private Boolean sessionTransacted; + /** * Default destination to use on send and receive operations that do not have a * destination parameter. @@ -267,6 +278,22 @@ public static class Template { */ private Duration receiveTimeout; + public AcknowledgeMode getAcknowledgeMode() { + return this.acknowledgeMode; + } + + public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) { + this.acknowledgeMode = acknowledgeMode; + } + + public Boolean getSessionTransacted() { + return this.sessionTransacted; + } + + public void setSessionTransacted(Boolean sessionTransacted) { + this.sessionTransacted = sessionTransacted; + } + public String getDefaultDestination() { return this.defaultDestination; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java index e89c07125438..69071f2f136d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java @@ -253,7 +253,8 @@ void testJmsTemplateWithDestinationResolver() { @Test void testJmsTemplateFullCustomization() { this.contextRunner.withUserConfiguration(MessageConvertersConfiguration.class) - .withPropertyValues("spring.jms.template.default-destination=testQueue", + .withPropertyValues("spring.jms.template.acknowledge-mode=client", + "spring.jms.template.session-transacted=true", "spring.jms.template.default-destination=testQueue", "spring.jms.template.delivery-delay=500", "spring.jms.template.delivery-mode=non-persistent", "spring.jms.template.priority=6", "spring.jms.template.time-to-live=6000", "spring.jms.template.receive-timeout=2000") @@ -261,6 +262,8 @@ void testJmsTemplateFullCustomization() { JmsTemplate jmsTemplate = context.getBean(JmsTemplate.class); assertThat(jmsTemplate.getMessageConverter()).isSameAs(context.getBean("myMessageConverter")); assertThat(jmsTemplate.isPubSubDomain()).isFalse(); + assertThat(jmsTemplate.getSessionAcknowledgeMode()).isEqualTo(Session.CLIENT_ACKNOWLEDGE); + assertThat(jmsTemplate.isSessionTransacted()).isTrue(); assertThat(jmsTemplate.getDefaultDestinationName()).isEqualTo("testQueue"); assertThat(jmsTemplate.getDeliveryDelay()).isEqualTo(500); assertThat(jmsTemplate.getDeliveryMode()).isOne(); From 3adc70fd40e720ad9213eba281f50548fee984a1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 Sep 2023 10:19:20 +0100 Subject: [PATCH 0496/1215] Polish "Add properties for JmsTemplate session's ack mode and transacted flag" See gh-37500 --- .../jms/JmsAutoConfiguration.java | 5 +- .../boot/autoconfigure/jms/JmsProperties.java | 60 +++++++++++-------- ...itional-spring-configuration-metadata.json | 4 ++ .../jms/JmsAutoConfigurationTests.java | 4 +- 4 files changed, 43 insertions(+), 30 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java index 21f6fd95a7e6..4add88c74ba1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java @@ -89,10 +89,9 @@ public JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) { private void mapTemplateProperties(Template properties, JmsTemplate template) { PropertyMapper map = PropertyMapper.get(); - map.from(properties::getAcknowledgeMode) - .whenNonNull() + map.from(properties.getSession()::getAcknowledgeMode) .to((acknowledgeMode) -> template.setSessionAcknowledgeMode(acknowledgeMode.getMode())); - map.from(properties::getSessionTransacted).whenNonNull().to(template::setSessionTransacted); + map.from(properties.getSession()::isTransacted).to(template::setSessionTransacted); map.from(properties::getDefaultDestination).whenNonNull().to(template::setDefaultDestinationName); map.from(properties::getDeliveryDelay).whenNonNull().as(Duration::toMillis).to(template::setDeliveryDelay); map.from(properties::determineQosEnabled).to(template::setExplicitQosEnabled); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java index b5b4ec7d2efb..a8a2ed034ab4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java @@ -228,16 +228,6 @@ public void setReceiveTimeout(Duration receiveTimeout) { public static class Template { - /** - * Acknowledgement mode used when creating JMS sessions to send a message. - */ - private AcknowledgeMode acknowledgeMode; - - /** - * Whether to use transacted JMS sessions. - */ - private Boolean sessionTransacted; - /** * Default destination to use on send and receive operations that do not have a * destination parameter. @@ -278,21 +268,7 @@ public static class Template { */ private Duration receiveTimeout; - public AcknowledgeMode getAcknowledgeMode() { - return this.acknowledgeMode; - } - - public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) { - this.acknowledgeMode = acknowledgeMode; - } - - public Boolean getSessionTransacted() { - return this.sessionTransacted; - } - - public void setSessionTransacted(Boolean sessionTransacted) { - this.sessionTransacted = sessionTransacted; - } + private final Session session = new Session(); public String getDefaultDestination() { return this.defaultDestination; @@ -357,6 +333,40 @@ public void setReceiveTimeout(Duration receiveTimeout) { this.receiveTimeout = receiveTimeout; } + public Session getSession() { + return this.session; + } + + public static class Session { + + /** + * Acknowledge mode used when creating sessions. + */ + private AcknowledgeMode acknowledgeMode = AcknowledgeMode.AUTO; + + /** + * Whether to use transacted sessions. + */ + private boolean transacted = false; + + public AcknowledgeMode getAcknowledgeMode() { + return this.acknowledgeMode; + } + + public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) { + this.acknowledgeMode = acknowledgeMode; + } + + public boolean isTransacted() { + return this.transacted; + } + + public void setTransacted(boolean transacted) { + this.transacted = transacted; + } + + } + } /** 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 db3b6221d064..fd6e0a9aa5a6 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 @@ -1571,6 +1571,10 @@ "name": "spring.jersey.type", "defaultValue": "servlet" }, + { + "name": "spring.jms.template.session.acknowledge-mode", + "defaultValue": "auto" + }, { "name": "spring.jpa.hibernate.use-new-id-generator-mappings", "type": "java.lang.Boolean", diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java index 69071f2f136d..a03845da1b63 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java @@ -253,8 +253,8 @@ void testJmsTemplateWithDestinationResolver() { @Test void testJmsTemplateFullCustomization() { this.contextRunner.withUserConfiguration(MessageConvertersConfiguration.class) - .withPropertyValues("spring.jms.template.acknowledge-mode=client", - "spring.jms.template.session-transacted=true", "spring.jms.template.default-destination=testQueue", + .withPropertyValues("spring.jms.template.session.acknowledge-mode=client", + "spring.jms.template.session.transacted=true", "spring.jms.template.default-destination=testQueue", "spring.jms.template.delivery-delay=500", "spring.jms.template.delivery-mode=non-persistent", "spring.jms.template.priority=6", "spring.jms.template.time-to-live=6000", "spring.jms.template.receive-timeout=2000") From b7facec4a1f8d5d4b5401564c13f87c0b49006d0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 Sep 2023 14:41:09 +0100 Subject: [PATCH 0497/1215] Rename spring.jms.listener.acknowledge-mode Closes gh-37602 --- ...JmsListenerContainerFactoryConfigurer.java | 4 +-- .../boot/autoconfigure/jms/JmsProperties.java | 36 ++++++++++++++----- ...itional-spring-configuration-metadata.json | 4 +++ .../jms/JmsAutoConfigurationTests.java | 6 ++-- 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java index 931e8f653d98..265e00167c64 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java @@ -118,9 +118,7 @@ public void configure(DefaultJmsListenerContainerFactory factory, ConnectionFact } JmsProperties.Listener listener = this.jmsProperties.getListener(); factory.setAutoStartup(listener.isAutoStartup()); - if (listener.getAcknowledgeMode() != null) { - factory.setSessionAcknowledgeMode(listener.getAcknowledgeMode().getMode()); - } + factory.setSessionAcknowledgeMode(listener.getSession().getAcknowledgeMode().getMode()); String concurrency = listener.formatConcurrency(); if (concurrency != null) { factory.setConcurrency(concurrency); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java index a8a2ed034ab4..91880a571ee9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java @@ -141,12 +141,6 @@ public static class Listener { */ private boolean autoStartup = true; - /** - * Acknowledge mode of the container. By default, the listener is transacted with - * automatic acknowledgment. - */ - private AcknowledgeMode acknowledgeMode; - /** * Minimum number of concurrent consumers. When max-concurrency is not specified * the minimum will also be used as the maximum. @@ -165,6 +159,8 @@ public static class Listener { */ private Duration receiveTimeout = Duration.ofSeconds(1); + private final Session session = new Session(); + public boolean isAutoStartup() { return this.autoStartup; } @@ -173,12 +169,15 @@ public void setAutoStartup(boolean autoStartup) { this.autoStartup = autoStartup; } + @Deprecated(since = "3.2.0", forRemoval = true) + @DeprecatedConfigurationProperty(replacement = "spring.jms.listener.session.acknowledge-mode", since = "3.2.0") public AcknowledgeMode getAcknowledgeMode() { - return this.acknowledgeMode; + return this.session.getAcknowledgeMode(); } + @Deprecated(since = "3.2.0", forRemoval = true) public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) { - this.acknowledgeMode = acknowledgeMode; + this.session.setAcknowledgeMode(acknowledgeMode); } @DeprecatedConfigurationProperty(replacement = "spring.jms.listener.min-concurrency", since = "3.2.0") @@ -224,6 +223,27 @@ public void setReceiveTimeout(Duration receiveTimeout) { this.receiveTimeout = receiveTimeout; } + public Session getSession() { + return this.session; + } + + public static class Session { + + /** + * Acknowledge mode of the listener container. + */ + private AcknowledgeMode acknowledgeMode = AcknowledgeMode.AUTO; + + public AcknowledgeMode getAcknowledgeMode() { + return this.acknowledgeMode; + } + + public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) { + this.acknowledgeMode = acknowledgeMode; + } + + } + } public static class Template { 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 fd6e0a9aa5a6..317c4c5150e1 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 @@ -1571,6 +1571,10 @@ "name": "spring.jersey.type", "defaultValue": "servlet" }, + { + "name": "spring.jms.listener.session.acknowledge-mode", + "defaultValue": "auto" + }, { "name": "spring.jms.template.session.acknowledge-mode", "defaultValue": "auto" diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java index a03845da1b63..d7cca9e7cef6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java @@ -142,9 +142,9 @@ void jmsListenerContainerFactoryWhenMultipleConnectionFactoryBeansShouldBackOff( @Test void testJmsListenerContainerFactoryWithCustomSettings() { this.contextRunner.withUserConfiguration(EnableJmsConfiguration.class) - .withPropertyValues("spring.jms.listener.autoStartup=false", "spring.jms.listener.acknowledgeMode=client", - "spring.jms.listener.minConcurrency=2", "spring.jms.listener.receiveTimeout=2s", - "spring.jms.listener.maxConcurrency=10") + .withPropertyValues("spring.jms.listener.autoStartup=false", + "spring.jms.listener.session.acknowledgeMode=client", "spring.jms.listener.minConcurrency=2", + "spring.jms.listener.receiveTimeout=2s", "spring.jms.listener.maxConcurrency=10") .run(this::testJmsListenerContainerFactoryWithCustomSettings); } From 79e2cb3ec1f13c44a4b299065583c25882acce6f Mon Sep 17 00:00:00 2001 From: Vedran Pavic Date: Tue, 19 Sep 2023 20:23:22 +0200 Subject: [PATCH 0498/1215] Add config prop for JMS listener's sessionTransacted flag This commit introduces `spring.jms.listener.session-transacted` property in order to enable explicit configuration of `sessionTransacted` on the `DefaultMessageListenerContainer`. Prior to this commit, `sessionTransacted` would be configured implicitly based on presence of `JtaTransactionManager`. See gh-37473 --- ...JmsListenerContainerFactoryConfigurer.java | 10 +++++-- .../boot/autoconfigure/jms/JmsProperties.java | 13 ++++++++ .../jms/JmsAutoConfigurationTests.java | 30 +++++++++++++++++-- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java index 265e00167c64..38288acbd46a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -32,6 +32,7 @@ * * @author Stephane Nicoll * @author Eddú Meléndez + * @author Vedran Pavic * @since 1.3.3 */ public final class DefaultJmsListenerContainerFactoryConfigurer { @@ -101,12 +102,16 @@ public void configure(DefaultJmsListenerContainerFactory factory, ConnectionFact Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); factory.setConnectionFactory(connectionFactory); factory.setPubSubDomain(this.jmsProperties.isPubSubDomain()); + JmsProperties.Listener listener = this.jmsProperties.getListener(); if (this.transactionManager != null) { factory.setTransactionManager(this.transactionManager); } - else { + else if (listener.getSessionTransacted() == null) { factory.setSessionTransacted(true); } + if (listener.getSessionTransacted() != null) { + factory.setSessionTransacted(listener.getSessionTransacted()); + } if (this.destinationResolver != null) { factory.setDestinationResolver(this.destinationResolver); } @@ -116,7 +121,6 @@ public void configure(DefaultJmsListenerContainerFactory factory, ConnectionFact if (this.exceptionListener != null) { factory.setExceptionListener(this.exceptionListener); } - JmsProperties.Listener listener = this.jmsProperties.getListener(); factory.setAutoStartup(listener.isAutoStartup()); factory.setSessionAcknowledgeMode(listener.getSession().getAcknowledgeMode().getMode()); String concurrency = listener.formatConcurrency(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java index 91880a571ee9..46d9a705180c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java @@ -141,6 +141,11 @@ public static class Listener { */ private boolean autoStartup = true; + /** + * Whether the container should use transacted JMS sessions. + */ + private Boolean sessionTransacted; + /** * Minimum number of concurrent consumers. When max-concurrency is not specified * the minimum will also be used as the maximum. @@ -180,6 +185,14 @@ public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) { this.session.setAcknowledgeMode(acknowledgeMode); } + public Boolean getSessionTransacted() { + return this.sessionTransacted; + } + + public void setSessionTransacted(Boolean sessionTransacted) { + this.sessionTransacted = sessionTransacted; + } + @DeprecatedConfigurationProperty(replacement = "spring.jms.listener.min-concurrency", since = "3.2.0") @Deprecated(since = "3.2.0", forRemoval = true) public Integer getConcurrency() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java index d7cca9e7cef6..62836772453e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java @@ -57,6 +57,7 @@ * @author Stephane Nicoll * @author Aurélien Leboulanger * @author Eddú Meléndez + * @author Vedran Pavic */ class JmsAutoConfigurationTests { @@ -143,8 +144,9 @@ void jmsListenerContainerFactoryWhenMultipleConnectionFactoryBeansShouldBackOff( void testJmsListenerContainerFactoryWithCustomSettings() { this.contextRunner.withUserConfiguration(EnableJmsConfiguration.class) .withPropertyValues("spring.jms.listener.autoStartup=false", - "spring.jms.listener.session.acknowledgeMode=client", "spring.jms.listener.minConcurrency=2", - "spring.jms.listener.receiveTimeout=2s", "spring.jms.listener.maxConcurrency=10") + "spring.jms.listener.session.acknowledgeMode=client", "spring.jms.listener.sessionTransacted=false", + "spring.jms.listener.minConcurrency=2", "spring.jms.listener.receiveTimeout=2s", + "spring.jms.listener.maxConcurrency=10") .run(this::testJmsListenerContainerFactoryWithCustomSettings); } @@ -152,6 +154,7 @@ private void testJmsListenerContainerFactoryWithCustomSettings(AssertableApplica DefaultMessageListenerContainer container = getContainer(loaded, "jmsListenerContainerFactory"); assertThat(container.isAutoStartup()).isFalse(); assertThat(container.getSessionAcknowledgeMode()).isEqualTo(Session.CLIENT_ACKNOWLEDGE); + assertThat(container.isSessionTransacted()).isFalse(); assertThat(container.getConcurrentConsumers()).isEqualTo(2); assertThat(container.getMaxConcurrentConsumers()).isEqualTo(10); assertThat(container).hasFieldOrPropertyWithValue("receiveTimeout", 2000L); @@ -179,6 +182,18 @@ void testDefaultContainerFactoryWithJtaTransactionManager() { }); } + @Test + void testDefaultContainerFactoryWithJtaTransactionManagerAndSessionTransactedEnabled() { + this.contextRunner.withUserConfiguration(TestConfiguration7.class, EnableJmsConfiguration.class) + .withPropertyValues("spring.jms.listener.sessionTransacted=true") + .run((context) -> { + DefaultMessageListenerContainer container = getContainer(context, "jmsListenerContainerFactory"); + assertThat(container.isSessionTransacted()).isTrue(); + assertThat(container).hasFieldOrPropertyWithValue("transactionManager", + context.getBean(JtaTransactionManager.class)); + }); + } + @Test void testDefaultContainerFactoryNonJtaTransactionManager() { this.contextRunner.withUserConfiguration(TestConfiguration8.class, EnableJmsConfiguration.class) @@ -198,6 +213,17 @@ void testDefaultContainerFactoryNoTransactionManager() { }); } + @Test + void testDefaultContainerFactoryNoTransactionManagerAndSessionTransactedDisabled() { + this.contextRunner.withUserConfiguration(EnableJmsConfiguration.class) + .withPropertyValues("spring.jms.listener.sessionTransacted=false") + .run((context) -> { + DefaultMessageListenerContainer container = getContainer(context, "jmsListenerContainerFactory"); + assertThat(container.isSessionTransacted()).isFalse(); + assertThat(container).hasFieldOrPropertyWithValue("transactionManager", null); + }); + } + @Test void testDefaultContainerFactoryWithMessageConverters() { this.contextRunner.withUserConfiguration(MessageConvertersConfiguration.class, EnableJmsConfiguration.class) From 0d2eaa716cf743879a496078d43267a2fd603190 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 Sep 2023 15:06:39 +0100 Subject: [PATCH 0499/1215] Polish "Add config prop for JMS listener's sessionTransacted flag" See gh-37473 --- ...JmsListenerContainerFactoryConfigurer.java | 11 +++++--- .../boot/autoconfigure/jms/JmsProperties.java | 27 ++++++++++--------- .../jms/JmsAutoConfigurationTests.java | 10 +++---- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java index 38288acbd46a..ebbf7e3c5669 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java @@ -21,6 +21,7 @@ import jakarta.jms.ConnectionFactory; import jakarta.jms.ExceptionListener; +import org.springframework.boot.autoconfigure.jms.JmsProperties.Listener.Session; import org.springframework.jms.config.DefaultJmsListenerContainerFactory; import org.springframework.jms.support.converter.MessageConverter; import org.springframework.jms.support.destination.DestinationResolver; @@ -103,14 +104,16 @@ public void configure(DefaultJmsListenerContainerFactory factory, ConnectionFact factory.setConnectionFactory(connectionFactory); factory.setPubSubDomain(this.jmsProperties.isPubSubDomain()); JmsProperties.Listener listener = this.jmsProperties.getListener(); + Session session = listener.getSession(); + Boolean sessionTransacted = session.getTransacted(); if (this.transactionManager != null) { factory.setTransactionManager(this.transactionManager); } - else if (listener.getSessionTransacted() == null) { + else if (sessionTransacted == null) { factory.setSessionTransacted(true); } - if (listener.getSessionTransacted() != null) { - factory.setSessionTransacted(listener.getSessionTransacted()); + if (sessionTransacted != null) { + factory.setSessionTransacted(sessionTransacted); } if (this.destinationResolver != null) { factory.setDestinationResolver(this.destinationResolver); @@ -122,7 +125,7 @@ else if (listener.getSessionTransacted() == null) { factory.setExceptionListener(this.exceptionListener); } factory.setAutoStartup(listener.isAutoStartup()); - factory.setSessionAcknowledgeMode(listener.getSession().getAcknowledgeMode().getMode()); + factory.setSessionAcknowledgeMode(session.getAcknowledgeMode().getMode()); String concurrency = listener.formatConcurrency(); if (concurrency != null) { factory.setConcurrency(concurrency); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java index 46d9a705180c..cc6d3140690f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java @@ -141,11 +141,6 @@ public static class Listener { */ private boolean autoStartup = true; - /** - * Whether the container should use transacted JMS sessions. - */ - private Boolean sessionTransacted; - /** * Minimum number of concurrent consumers. When max-concurrency is not specified * the minimum will also be used as the maximum. @@ -185,14 +180,6 @@ public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) { this.session.setAcknowledgeMode(acknowledgeMode); } - public Boolean getSessionTransacted() { - return this.sessionTransacted; - } - - public void setSessionTransacted(Boolean sessionTransacted) { - this.sessionTransacted = sessionTransacted; - } - @DeprecatedConfigurationProperty(replacement = "spring.jms.listener.min-concurrency", since = "3.2.0") @Deprecated(since = "3.2.0", forRemoval = true) public Integer getConcurrency() { @@ -247,6 +234,12 @@ public static class Session { */ private AcknowledgeMode acknowledgeMode = AcknowledgeMode.AUTO; + /** + * Whether the listener container should use transacted JMS sessions. Defaults + * to false in the presence of a JtaTransactionManager and true otherwise. + */ + private Boolean transacted; + public AcknowledgeMode getAcknowledgeMode() { return this.acknowledgeMode; } @@ -255,6 +248,14 @@ public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) { this.acknowledgeMode = acknowledgeMode; } + public Boolean getTransacted() { + return this.transacted; + } + + public void setTransacted(Boolean transacted) { + this.transacted = transacted; + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java index 62836772453e..49f5ddb3b272 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java @@ -144,9 +144,9 @@ void jmsListenerContainerFactoryWhenMultipleConnectionFactoryBeansShouldBackOff( void testJmsListenerContainerFactoryWithCustomSettings() { this.contextRunner.withUserConfiguration(EnableJmsConfiguration.class) .withPropertyValues("spring.jms.listener.autoStartup=false", - "spring.jms.listener.session.acknowledgeMode=client", "spring.jms.listener.sessionTransacted=false", - "spring.jms.listener.minConcurrency=2", "spring.jms.listener.receiveTimeout=2s", - "spring.jms.listener.maxConcurrency=10") + "spring.jms.listener.session.acknowledgeMode=client", + "spring.jms.listener.session.transacted=false", "spring.jms.listener.minConcurrency=2", + "spring.jms.listener.receiveTimeout=2s", "spring.jms.listener.maxConcurrency=10") .run(this::testJmsListenerContainerFactoryWithCustomSettings); } @@ -185,7 +185,7 @@ void testDefaultContainerFactoryWithJtaTransactionManager() { @Test void testDefaultContainerFactoryWithJtaTransactionManagerAndSessionTransactedEnabled() { this.contextRunner.withUserConfiguration(TestConfiguration7.class, EnableJmsConfiguration.class) - .withPropertyValues("spring.jms.listener.sessionTransacted=true") + .withPropertyValues("spring.jms.listener.session.transacted=true") .run((context) -> { DefaultMessageListenerContainer container = getContainer(context, "jmsListenerContainerFactory"); assertThat(container.isSessionTransacted()).isTrue(); @@ -216,7 +216,7 @@ void testDefaultContainerFactoryNoTransactionManager() { @Test void testDefaultContainerFactoryNoTransactionManagerAndSessionTransactedDisabled() { this.contextRunner.withUserConfiguration(EnableJmsConfiguration.class) - .withPropertyValues("spring.jms.listener.sessionTransacted=false") + .withPropertyValues("spring.jms.listener.session.transacted=false") .run((context) -> { DefaultMessageListenerContainer container = getContainer(context, "jmsListenerContainerFactory"); assertThat(container.isSessionTransacted()).isFalse(); From 9811cc030fd62c41eee990c32b6d88d458c996f9 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Wed, 27 Sep 2023 10:09:44 -0500 Subject: [PATCH 0500/1215] Fix LifecycleTests for security options on Windows Fixes gh-37598 --- .../boot/buildpack/platform/build/LifecycleTests.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java index f55e0ed5cda1..6f898d1a9817 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java @@ -262,7 +262,7 @@ void executeWithSecurityOptionsExecutesPhases() throws Exception { given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); BuildRequest request = getTestRequest().withSecurityOptions(List.of("label=user:USER", "label=role:ROLE")); createLifecycle(request).execute(); - assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-security-opts.json")); + assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-security-opts.json", true)); assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); } @@ -370,12 +370,16 @@ private void assertPhaseWasRun(String name, IOConsumer configCo } private IOConsumer withExpectedConfig(String name) { + return withExpectedConfig(name, false); + } + + private IOConsumer withExpectedConfig(String name, boolean expectSecurityOptAlways) { return (config) -> { try { InputStream in = getClass().getResourceAsStream(name); String jsonString = FileCopyUtils.copyToString(new InputStreamReader(in, StandardCharsets.UTF_8)); JSONObject json = new JSONObject(jsonString); - if (Platform.isWindows()) { + if (!expectSecurityOptAlways && Platform.isWindows()) { JSONObject hostConfig = json.getJSONObject("HostConfig"); hostConfig.remove("SecurityOpt"); } From 3cf08e1351eab58da0825a63cd07da69ce74e013 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Wed, 27 Sep 2023 11:05:40 -0500 Subject: [PATCH 0501/1215] Clarify default security options for image building See gh-37479 --- .../src/docs/asciidoc/packaging-oci-image.adoc | 2 +- .../src/docs/asciidoc/packaging-oci-image.adoc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc index f0abfb2dfe25..951ab4beef48 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -226,7 +226,7 @@ Application contents will also be in this location in the generated image. | `securityOptions` | `--securityOptions` | https://docs.docker.com/engine/reference/run/#security-configuration[Security options] that will be applied to the builder container, provided as an array of string values -| `["label=disable"]` +| `["label=disable"]` on Linux and macOS, `[]` on Windows |=== diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc index 2766e6b12fd3..974c1569fc30 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -232,7 +232,7 @@ Application contents will also be in this location in the generated image. | `securityOptions` | https://docs.docker.com/engine/reference/run/#security-configuration[Security options] that will be applied to the builder container, provided as an array of string values -| `["label=disable"]` +| `["label=disable"]` on Linux and macOS, `[]` on Windows |=== From 954f56287fdb169db04d98562ec8fa542efa7255 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 27 Sep 2023 09:40:23 -0400 Subject: [PATCH 0502/1215] Add config prop for Rabbit's max inbound message body size See gh-37603 --- .../RabbitConnectionFactoryBeanConfigurer.java | 3 +++ .../boot/autoconfigure/amqp/RabbitProperties.java | 14 ++++++++++++++ .../amqp/RabbitAutoConfigurationTests.java | 7 ++++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java index f54e91ace5c2..5e839306d5a6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java @@ -133,6 +133,9 @@ public void configure(RabbitConnectionFactoryBean factory) { .to(factory::setChannelRpcTimeout); map.from(this.credentialsProvider).whenNonNull().to(factory::setCredentialsProvider); map.from(this.credentialsRefreshService).whenNonNull().to(factory::setCredentialsRefreshService); + map.from(this.rabbitProperties.getMaxInboundMessageBodySize()) + .whenNonNull() + .to((mimbs) -> factory.setMaxInboundMessageBodySize(Math.toIntExact(mimbs.toBytes()))); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java index a9ae5efc4114..bd9256808099 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java @@ -31,6 +31,7 @@ import org.springframework.boot.convert.DurationUnit; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; +import org.springframework.util.unit.DataSize; /** * Configuration properties for Rabbit. @@ -130,6 +131,11 @@ public class RabbitProperties { */ private Duration channelRpcTimeout = Duration.ofMinutes(10); + /** + * Maximum body size of inbound (received) messages in bytes. + */ + private DataSize maxInboundMessageBodySize; + /** * Cache configuration. */ @@ -360,6 +366,14 @@ public void setChannelRpcTimeout(Duration channelRpcTimeout) { this.channelRpcTimeout = channelRpcTimeout; } + public DataSize getMaxInboundMessageBodySize() { + return this.maxInboundMessageBodySize; + } + + public void setMaxInboundMessageBodySize(DataSize maxInboundMessageBodySize) { + this.maxInboundMessageBodySize = maxInboundMessageBodySize; + } + public Cache getCache() { return this.cache; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java index e9a9b69d94d3..a66472cf9d95 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java @@ -150,6 +150,9 @@ void testDefaultConnectionFactoryConfiguration() { com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory = getTargetConnectionFactory(context); assertThat(rabbitConnectionFactory.getUsername()).isEqualTo(properties.getUsername()); assertThat(rabbitConnectionFactory.getPassword()).isEqualTo(properties.getPassword()); + com.rabbitmq.client.ConnectionFactory defaultCf = new com.rabbitmq.client.ConnectionFactory(); + assertThat(rabbitConnectionFactory).hasFieldOrPropertyWithValue("maxInboundMessageBodySize", + ReflectionTestUtils.getField(defaultCf, "maxInboundMessageBodySize")); }); } @@ -160,7 +163,8 @@ void testConnectionFactoryWithOverrides() { .withPropertyValues("spring.rabbitmq.host:remote-server", "spring.rabbitmq.port:9000", "spring.rabbitmq.address-shuffle-mode=random", "spring.rabbitmq.username:alice", "spring.rabbitmq.password:secret", "spring.rabbitmq.virtual_host:/vhost", - "spring.rabbitmq.connection-timeout:123", "spring.rabbitmq.channel-rpc-timeout:140") + "spring.rabbitmq.connection-timeout:123", "spring.rabbitmq.channel-rpc-timeout:140", + "spring.rabbitmq.max-inbound-message-body-size:128MB") .run((context) -> { CachingConnectionFactory connectionFactory = context.getBean(CachingConnectionFactory.class); assertThat(connectionFactory.getHost()).isEqualTo("remote-server"); @@ -172,6 +176,7 @@ void testConnectionFactoryWithOverrides() { assertThat(rcf.getConnectionTimeout()).isEqualTo(123); assertThat(rcf.getChannelRpcTimeout()).isEqualTo(140); assertThat((List
    ) ReflectionTestUtils.getField(connectionFactory, "addresses")).hasSize(1); + assertThat(rcf).hasFieldOrPropertyWithValue("maxInboundMessageBodySize", 1024 * 1024 * 128); }); } From 4e5f16f2bc9f484eea1ec54b5800b0dcfe22c77c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 Sep 2023 19:21:17 +0100 Subject: [PATCH 0503/1215] Polish "Add config prop for Rabbit's max inbound message body size" See gh-37603 --- .../amqp/RabbitConnectionFactoryBeanConfigurer.java | 4 +++- .../boot/autoconfigure/amqp/RabbitProperties.java | 4 ++-- .../autoconfigure/amqp/RabbitAutoConfigurationTests.java | 5 ++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java index 5e839306d5a6..607ac96462a4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java @@ -26,6 +26,7 @@ import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; +import org.springframework.util.unit.DataSize; /** * Configures {@link RabbitConnectionFactoryBean} with sensible defaults. @@ -135,7 +136,8 @@ public void configure(RabbitConnectionFactoryBean factory) { map.from(this.credentialsRefreshService).whenNonNull().to(factory::setCredentialsRefreshService); map.from(this.rabbitProperties.getMaxInboundMessageBodySize()) .whenNonNull() - .to((mimbs) -> factory.setMaxInboundMessageBodySize(Math.toIntExact(mimbs.toBytes()))); + .asInt(DataSize::toBytes) + .to(factory::setMaxInboundMessageBodySize); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java index bd9256808099..dd9aae0800f8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java @@ -132,9 +132,9 @@ public class RabbitProperties { private Duration channelRpcTimeout = Duration.ofMinutes(10); /** - * Maximum body size of inbound (received) messages in bytes. + * Maximum size of the body of inbound (received) messages. */ - private DataSize maxInboundMessageBodySize; + private DataSize maxInboundMessageBodySize = DataSize.ofMegabytes(64); /** * Cache configuration. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java index a66472cf9d95..c6c32c7daa9c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java @@ -150,9 +150,8 @@ void testDefaultConnectionFactoryConfiguration() { com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory = getTargetConnectionFactory(context); assertThat(rabbitConnectionFactory.getUsername()).isEqualTo(properties.getUsername()); assertThat(rabbitConnectionFactory.getPassword()).isEqualTo(properties.getPassword()); - com.rabbitmq.client.ConnectionFactory defaultCf = new com.rabbitmq.client.ConnectionFactory(); - assertThat(rabbitConnectionFactory).hasFieldOrPropertyWithValue("maxInboundMessageBodySize", - ReflectionTestUtils.getField(defaultCf, "maxInboundMessageBodySize")); + assertThat(rabbitConnectionFactory).extracting("maxInboundMessageBodySize") + .isEqualTo((int) properties.getMaxInboundMessageBodySize().toBytes()); }); } From 3591f4d614bd3a388a9f56b183de0fe0d670b66b Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 27 Sep 2023 18:21:24 -0700 Subject: [PATCH 0504/1215] Restore test ensuring maxInboundMessageBodySize property matches default See gh-37603 --- .../boot/autoconfigure/amqp/RabbitPropertiesTests.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitPropertiesTests.java index 61e89c08222a..c54303336594 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitPropertiesTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.autoconfigure.amqp; +import com.rabbitmq.client.ConnectionFactory; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.config.DirectRabbitListenerContainerFactory; @@ -313,6 +314,13 @@ void determineSslReturnFlagPropertyWhenNoAddresses() { assertThat(this.properties.getSsl().determineEnabled()).isTrue(); } + @Test + void propertiesUseConsistentDefaultValues() { + ConnectionFactory connectionFactory = new ConnectionFactory(); + assertThat(connectionFactory).hasFieldOrPropertyWithValue("maxInboundMessageBodySize", + (int) this.properties.getMaxInboundMessageBodySize().toBytes()); + } + @Test void simpleContainerUseConsistentDefaultValues() { SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); From 052757c2d8effc869c41f85551745df4a5cacb59 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 27 Sep 2023 20:17:44 -0700 Subject: [PATCH 0505/1215] Polish --- .../otlp/OtlpPropertiesConfigAdapter.java | 17 ++++---- .../OpenTelemetryAutoConfiguration.java | 6 +-- ...JmsListenerContainerFactoryConfigurer.java | 43 ++++++------------- .../jms/JmsAutoConfiguration.java | 16 +++---- .../src/docs/asciidoc/actuator/metrics.adoc | 1 + .../src/docs/asciidoc/messaging/pulsar.adoc | 2 + ...nectionDetailsFactoryIntegrationTests.java | 6 --- .../platform/build/BuildRequest.java | 10 ++++- .../gradle/tasks/bundling/BootBuildImage.java | 2 + 9 files changed, 43 insertions(+), 60 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java index c77b39a92612..f329d7bf17ae 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java @@ -75,18 +75,17 @@ public AggregationTemporality aggregationTemporality() { @Override @SuppressWarnings("removal") public Map resourceAttributes() { - Map result; - if (!CollectionUtils.isEmpty(this.openTelemetryProperties.getResourceAttributes())) { - result = new HashMap<>(this.openTelemetryProperties.getResourceAttributes()); - } - else { - result = new HashMap<>(get(OtlpProperties::getResourceAttributes, OtlpConfig.super::resourceAttributes)); - } - result.computeIfAbsent("service.name", - (ignore) -> this.environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME)); + Map resourceAttributes = this.openTelemetryProperties.getResourceAttributes(); + Map result = new HashMap<>((!CollectionUtils.isEmpty(resourceAttributes)) ? resourceAttributes + : get(OtlpProperties::getResourceAttributes, OtlpConfig.super::resourceAttributes)); + result.computeIfAbsent("service.name", (key) -> getApplicationName()); return Collections.unmodifiableMap(result); } + private String getApplicationName() { + return this.environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME); + } + @Override public Map headers() { return get(OtlpProperties::getHeaders, OtlpConfig.super::headers); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java index be533a86892d..b0874f7791f7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java @@ -16,8 +16,6 @@ package org.springframework.boot.actuate.autoconfigure.opentelemetry; -import java.util.Map.Entry; - import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.context.propagation.ContextPropagators; @@ -80,9 +78,7 @@ Resource openTelemetryResource(Environment environment, OpenTelemetryProperties private static Resource toResource(OpenTelemetryProperties properties) { ResourceBuilder builder = Resource.builder(); - for (Entry entry : properties.getResourceAttributes().entrySet()) { - builder.put(entry.getKey(), entry.getValue()); - } + properties.getResourceAttributes().forEach(builder::put); return builder.build(); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java index ebbf7e3c5669..2e5f1cc58a0f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java @@ -22,6 +22,7 @@ import jakarta.jms.ExceptionListener; import org.springframework.boot.autoconfigure.jms.JmsProperties.Listener.Session; +import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.jms.config.DefaultJmsListenerContainerFactory; import org.springframework.jms.support.converter.MessageConverter; import org.springframework.jms.support.destination.DestinationResolver; @@ -103,37 +104,21 @@ public void configure(DefaultJmsListenerContainerFactory factory, ConnectionFact Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); factory.setConnectionFactory(connectionFactory); factory.setPubSubDomain(this.jmsProperties.isPubSubDomain()); - JmsProperties.Listener listener = this.jmsProperties.getListener(); - Session session = listener.getSession(); - Boolean sessionTransacted = session.getTransacted(); - if (this.transactionManager != null) { - factory.setTransactionManager(this.transactionManager); - } - else if (sessionTransacted == null) { + JmsProperties.Listener listenerProperties = this.jmsProperties.getListener(); + Session sessionProperties = listenerProperties.getSession(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(this.transactionManager).to(factory::setTransactionManager); + map.from(this.destinationResolver).to(factory::setDestinationResolver); + map.from(this.messageConverter).to(factory::setMessageConverter); + map.from(this.exceptionListener).to(factory::setExceptionListener); + map.from(sessionProperties.getAcknowledgeMode()::getMode).to(factory::setSessionAcknowledgeMode); + if (this.transactionManager == null && sessionProperties.getTransacted() == null) { factory.setSessionTransacted(true); } - if (sessionTransacted != null) { - factory.setSessionTransacted(sessionTransacted); - } - if (this.destinationResolver != null) { - factory.setDestinationResolver(this.destinationResolver); - } - if (this.messageConverter != null) { - factory.setMessageConverter(this.messageConverter); - } - if (this.exceptionListener != null) { - factory.setExceptionListener(this.exceptionListener); - } - factory.setAutoStartup(listener.isAutoStartup()); - factory.setSessionAcknowledgeMode(session.getAcknowledgeMode().getMode()); - String concurrency = listener.formatConcurrency(); - if (concurrency != null) { - factory.setConcurrency(concurrency); - } - Duration receiveTimeout = listener.getReceiveTimeout(); - if (receiveTimeout != null) { - factory.setReceiveTimeout(receiveTimeout.toMillis()); - } + map.from(sessionProperties::getTransacted).to(factory::setSessionTransacted); + map.from(listenerProperties::isAutoStartup).to(factory::setAutoStartup); + map.from(listenerProperties::formatConcurrency).to(factory::setConcurrency); + map.from(listenerProperties::getReceiveTimeout).as(Duration::toMillis).to(factory::setReceiveTimeout); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java index 4add88c74ba1..389023053ea0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java @@ -28,6 +28,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.autoconfigure.jms.JmsProperties.AcknowledgeMode; import org.springframework.boot.autoconfigure.jms.JmsProperties.DeliveryMode; import org.springframework.boot.autoconfigure.jms.JmsProperties.Template; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -88,23 +89,18 @@ public JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) { } private void mapTemplateProperties(Template properties, JmsTemplate template) { - PropertyMapper map = PropertyMapper.get(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(properties.getSession()::getAcknowledgeMode) - .to((acknowledgeMode) -> template.setSessionAcknowledgeMode(acknowledgeMode.getMode())); + .asInt(AcknowledgeMode::getMode) + .to(template::setSessionAcknowledgeMode); map.from(properties.getSession()::isTransacted).to(template::setSessionTransacted); map.from(properties::getDefaultDestination).whenNonNull().to(template::setDefaultDestinationName); map.from(properties::getDeliveryDelay).whenNonNull().as(Duration::toMillis).to(template::setDeliveryDelay); map.from(properties::determineQosEnabled).to(template::setExplicitQosEnabled); - map.from(properties::getDeliveryMode) - .whenNonNull() - .as(DeliveryMode::getValue) - .to(template::setDeliveryMode); + map.from(properties::getDeliveryMode).as(DeliveryMode::getValue).to(template::setDeliveryMode); map.from(properties::getPriority).whenNonNull().to(template::setPriority); map.from(properties::getTimeToLive).whenNonNull().as(Duration::toMillis).to(template::setTimeToLive); - map.from(properties::getReceiveTimeout) - .whenNonNull() - .as(Duration::toMillis) - .to(template::setReceiveTimeout); + map.from(properties::getReceiveTimeout).as(Duration::toMillis).to(template::setReceiveTimeout); } } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc index 14d9c035acc9..2b0a5dd18042 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc @@ -739,6 +739,7 @@ Auto-configuration enables the instrumentation of all available `ThreadPoolTaskE Metrics are tagged by the name of the executor, which is derived from the bean name. + [[actuator.metrics.supported.jms]] ==== JMS Metrics Auto-configuration enables the instrumentation of all available `JmsTemplate` beans. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/pulsar.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/pulsar.adoc index f2ced63368b1..0294fab362b9 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/pulsar.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/pulsar.adoc @@ -52,6 +52,8 @@ For example, if you want to configure the issuer url for the `AuthenticationOAut If you use other forms, such as `issuerurl` or `issuer-url`, the setting will not be applied to the plugin. ==== + + [[messaging.pulsar.connecting.ssl]] ==== SSL By default, Pulsar clients communicate with Pulsar services in plain text. diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsConnectionDetailsFactoryIntegrationTests.java index d2c85b301476..c1a88d38df5f 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsConnectionDetailsFactoryIntegrationTests.java @@ -79,7 +79,6 @@ void connectionCanBeMadeToOpenTelemetryCollectorContainer() { Gauge.builder("test.gauge", () -> 12).register(this.meterRegistry); Timer.builder("test.timer").register(this.meterRegistry).record(Duration.ofMillis(123)); DistributionSummary.builder("test.distributionsummary").register(this.meterRegistry).record(24); - Awaitility.await() .atMost(Duration.ofSeconds(5)) .pollDelay(Duration.ofMillis(100)) @@ -88,19 +87,14 @@ void connectionCanBeMadeToOpenTelemetryCollectorContainer() { .statusCode(200) .contentType(OPENMETRICS_001) .body(endsWith("# EOF\n"))); - whenPrometheusScraped().then() .body(containsString( "{job=\"test\",service_name=\"test\",telemetry_sdk_language=\"java\",telemetry_sdk_name=\"io.micrometer\""), - matchesPattern("(?s)^.*test_counter\\{.+} 42\\.0\\n.*$"), matchesPattern("(?s)^.*test_gauge\\{.+} 12\\.0\\n.*$"), - matchesPattern("(?s)^.*test_timer_count\\{.+} 1\\n.*$"), - matchesPattern("(?s)^.*test_timer_sum\\{.+} 123\\.0\\n.*$"), matchesPattern("(?s)^.*test_timer_bucket\\{.+,le=\"\\+Inf\"} 1\\n.*$"), - matchesPattern("(?s)^.*test_distributionsummary_count\\{.+} 1\\n.*$"), matchesPattern("(?s)^.*test_distributionsummary_sum\\{.+} 24\\.0\\n.*$"), matchesPattern("(?s)^.*test_distributionsummary_bucket\\{.+,le=\"\\+Inf\"} 1\\n.*$")); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java index 7ae4d8419a8c..476f0a8917e2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java @@ -349,6 +349,7 @@ public BuildRequest withTags(List tags) { * Return a new {@link BuildRequest} with an updated build workspace. * @param buildWorkspace the build workspace * @return an updated build request + * @since 3.2.0 */ public BuildRequest withBuildWorkspace(Cache buildWorkspace) { Assert.notNull(buildWorkspace, "BuildWorkspace must not be null"); @@ -426,6 +427,7 @@ public BuildRequest withApplicationDirectory(String applicationDirectory) { * Return a new {@link BuildRequest} with an updated security options. * @param securityOptions the security options * @return an updated build request + * @since 3.2.0 */ public BuildRequest withSecurityOptions(List securityOptions) { Assert.notNull(securityOptions, "SecurityOption must not be null"); @@ -552,6 +554,11 @@ public List getTags() { return this.tags; } + /** + * Return the build workspace that should be used by the lifecycle. + * @return the build workspace or {@code null} + * @since 3.2.0 + */ public Cache getBuildWorkspace() { return this.buildWorkspace; } @@ -590,7 +597,8 @@ public String getApplicationDirectory() { /** * Return the security options that should be used by the lifecycle. - * @return the security options + * @return the security options or {@code null} + * @since 3.2.0 */ public List getSecurityOptions() { return this.securityOptions; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java index 143f54d62b8c..2ba12dae88e8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java @@ -228,6 +228,7 @@ public void setPullPolicy(String pullPolicy) { /** * Returns the build temporary workspace that will be used when building the image. * @return the cache + * @since 3.2.0 */ @Nested @Optional @@ -239,6 +240,7 @@ public CacheSpec getBuildWorkspace() { * Customizes the {@link CacheSpec} for the build temporary workspace using the given * {@code action}. * @param action the action + * @since 3.2.0 */ public void buildWorkspace(Action action) { action.execute(this.buildWorkspace); From 8fad59466cd1dc894e66180ea60a390d06fe9f1a Mon Sep 17 00:00:00 2001 From: Sreekara Reddy Date: Thu, 21 Sep 2023 03:29:47 +0530 Subject: [PATCH 0506/1215] Don't call setValidateConnectionOnBorrow on Oracle UCP datasource See gh-37501 --- .../boot/autoconfigure/jdbc/DataSourceConfiguration.java | 1 - .../autoconfigure/jdbc/DataSourceAutoConfigurationTests.java | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java index 6389876a24e7..dc6ec5d11b62 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java @@ -180,7 +180,6 @@ PoolDataSourceImpl dataSource(DataSourceProperties properties, JdbcConnectionDet throws SQLException { PoolDataSourceImpl dataSource = createDataSource(connectionDetails, PoolDataSourceImpl.class, properties.getClassLoader()); - dataSource.setValidateConnectionOnBorrow(true); if (StringUtils.hasText(properties.getName())) { dataSource.setConnectionPoolName(properties.getName()); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java index 75cf07536762..1e2cbca31086 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java @@ -152,10 +152,10 @@ void oracleUcpIsFallback() { } @Test - void oracleUcpValidatesConnectionByDefault() { + void oracleUcpDoesNotValidateConnectionByDefault() { assertDataSource(PoolDataSourceImpl.class, Arrays.asList("com.zaxxer.hikari", "org.apache.tomcat", "org.apache.commons.dbcp2"), (dataSource) -> { - assertThat(dataSource.getValidateConnectionOnBorrow()).isTrue(); + assertThat(dataSource.getValidateConnectionOnBorrow()).isFalse(); // Use an internal ping when using an Oracle JDBC driver assertThat(dataSource.getSQLForValidateConnection()).isNull(); }); From 8eac7a91f6b80e05b24730c4657b90ead8fecab1 Mon Sep 17 00:00:00 2001 From: shin-mallang Date: Sat, 16 Sep 2023 05:22:32 +0900 Subject: [PATCH 0507/1215] Remove duplicate code in NettyWebServerFactoryCustomizer Since the PropertyMapper's alwaysApplyingWhenNonNull() has already been called, the subsequent whenNonNull() is unnecessary. See gh-37434 --- .../web/embedded/NettyWebServerFactoryCustomizer.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java index 39f14ad41fe7..6dc657b3d7cb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java @@ -65,7 +65,6 @@ public void customize(NettyReactiveWebServerFactory factory) { .to((maxKeepAliveRequests) -> customizeMaxKeepAliveRequests(factory, maxKeepAliveRequests)); if (this.serverProperties.getHttp2() != null && this.serverProperties.getHttp2().isEnabled()) { map.from(this.serverProperties.getMaxHttpRequestHeaderSize()) - .whenNonNull() .to((size) -> customizeHttp2MaxHeaderSize(factory, size.toBytes())); } customizeRequestDecoder(factory, map); @@ -87,24 +86,18 @@ private void customizeConnectionTimeout(NettyReactiveWebServerFactory factory, D private void customizeRequestDecoder(NettyReactiveWebServerFactory factory, PropertyMapper propertyMapper) { factory.addServerCustomizers((httpServer) -> httpServer.httpRequestDecoder((httpRequestDecoderSpec) -> { propertyMapper.from(this.serverProperties.getMaxHttpRequestHeaderSize()) - .whenNonNull() .to((maxHttpRequestHeader) -> httpRequestDecoderSpec .maxHeaderSize((int) maxHttpRequestHeader.toBytes())); ServerProperties.Netty nettyProperties = this.serverProperties.getNetty(); propertyMapper.from(nettyProperties.getMaxInitialLineLength()) - .whenNonNull() .to((maxInitialLineLength) -> httpRequestDecoderSpec .maxInitialLineLength((int) maxInitialLineLength.toBytes())); propertyMapper.from(nettyProperties.getH2cMaxContentLength()) - .whenNonNull() .to((h2cMaxContentLength) -> httpRequestDecoderSpec .h2cMaxContentLength((int) h2cMaxContentLength.toBytes())); propertyMapper.from(nettyProperties.getInitialBufferSize()) - .whenNonNull() .to((initialBufferSize) -> httpRequestDecoderSpec.initialBufferSize((int) initialBufferSize.toBytes())); - propertyMapper.from(nettyProperties.isValidateHeaders()) - .whenNonNull() - .to(httpRequestDecoderSpec::validateHeaders); + propertyMapper.from(nettyProperties.isValidateHeaders()).to(httpRequestDecoderSpec::validateHeaders); return httpRequestDecoderSpec; })); } From bebca55a8ffee3b4cd4d099fb6be199655cf8c7d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 28 Sep 2023 13:23:48 +0100 Subject: [PATCH 0508/1215] Add testAndDevelopmentOnly configuration Closes gh-35436 --- .../src/docs/asciidoc/features/testing.adoc | 2 +- .../src/docs/asciidoc/reacting.adoc | 7 +-- .../boot/gradle/plugin/JavaPluginAction.java | 38 ++++++++++++--- .../plugin/NativeImagePluginAction.java | 3 +- .../gradle/plugin/SpringBootAotPlugin.java | 23 +++++---- .../boot/gradle/plugin/SpringBootPlugin.java | 6 +++ .../boot/gradle/plugin/WarPluginAction.java | 3 ++ .../JavaPluginActionIntegrationTests.java | 47 ++++++++++++++++++- ...tiveImagePluginActionIntegrationTests.java | 8 ++++ .../SpringBootAotPluginIntegrationTests.java | 14 +++++- .../AbstractBootArchiveIntegrationTests.java | 35 ++++++++++++++ .../tasks/run/BootRunIntegrationTests.java | 16 +++++++ .../run/BootTestRunIntegrationTests.java | 16 +++++++ ...TestAndDevelopmentOnlyConfiguration.gradle | 12 +++++ ...tIncludeDevelopmentOnlyDependencies.gradle | 20 ++++++++ ...eTestAndDevelopmentOnlyDependencies.gradle | 20 ++++++++ ...IncludesDevelopmentOnlyDependencies.gradle | 20 ++++++++ ...sTestAndDevelopmentOnlyDependencies.gradle | 20 ++++++++ ...tIncludeDevelopmentOnlyDependencies.gradle | 20 ++++++++ ...sTestAndDevelopmentOnlyDependencies.gradle | 20 ++++++++ ...tIncludeDevelopmentOnlyDependencies.gradle | 20 ++++++++ ...sTestAndDevelopmentOnlyDependencies.gradle | 20 ++++++++ ...esDoNotAppearInNativeImageClasspath.gradle | 20 ++++++++ ...pmentOnlyDependenciesOnItsClasspath.gradle | 20 ++++++++ ...pmentOnlyDependenciesOnItsClasspath.gradle | 31 ++++++++++++ ...AreNotIncludedInTheArchiveByDefault.gradle | 24 ++++++++++ ...pendenciesCanBeIncludedInTheArchive.gradle | 27 +++++++++++ ...AreNotIncludedInTheArchiveByDefault.gradle | 24 ++++++++++ ...pendenciesCanBeIncludedInTheArchive.gradle | 27 +++++++++++ ...ntOnlyDependenciesAreOnTheClasspath.gradle | 12 +++++ ...ntOnlyDependenciesAreOnTheClasspath.gradle | 12 +++++ ...nlyDependenciesAreNotOnTheClasspath.gradle | 12 +++++ ...ntOnlyDependenciesAreOnTheClasspath.gradle | 12 +++++ 33 files changed, 587 insertions(+), 24 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-applyingJavaPluginCreatesTestAndDevelopmentOnlyConfiguration.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-compileClasspathDoesNotIncludeDevelopmentOnlyDependencies.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-compileClasspathDoesNotIncludeTestAndDevelopmentOnlyDependencies.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-runtimeClasspathIncludesDevelopmentOnlyDependencies.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-runtimeClasspathIncludesTestAndDevelopmentOnlyDependencies.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-testCompileClasspathDoesNotIncludeDevelopmentOnlyDependencies.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-testCompileClasspathIncludesTestAndDevelopmentOnlyDependencies.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-testRuntimeClasspathDoesNotIncludeDevelopmentOnlyDependencies.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-testRuntimeClasspathIncludesTestAndDevelopmentOnlyDependencies.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests-testAndDevelopmentOnlyDependenciesDoNotAppearInNativeImageClasspath.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/SpringBootAotPluginIntegrationTests-processAotDoesNotHaveTestAndDevelopmentOnlyDependenciesOnItsClasspath.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/SpringBootAotPluginIntegrationTests-processTestAotHasTestAndDevelopmentOnlyDependenciesOnItsClasspath.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-developmentOnlyDependenciesAreOnTheClasspath.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-testAndDevelopmentOnlyDependenciesAreOnTheClasspath.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootTestRunIntegrationTests-developmentOnlyDependenciesAreNotOnTheClasspath.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootTestRunIntegrationTests-testAndDevelopmentOnlyDependenciesAreOnTheClasspath.gradle diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc index d62ecfef915d..d440e4cc5aa7 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc @@ -1115,7 +1115,7 @@ This is especially useful for Testcontainer `Container` beans, as they keep thei include::code:MyContainersConfiguration[] -WARNING: If you're using Gradle and want to use this feature, you need to change the configuration of the `spring-boot-devtools` dependency from `developmentOnly` to `testImplementation`. +WARNING: If you're using Gradle and want to use this feature, you need to change the configuration of the `spring-boot-devtools` dependency from `developmentOnly` to `testAndDevelopmentOnly`. With the default scope of `developmentOnly`, the `bootTestRun` task will not pick up changes in your code, as the devtools are not active. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc index bfb245f2d427..7da583578b38 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc @@ -18,9 +18,10 @@ When Gradle's {java-plugin}[`java` plugin] is applied to a project, the Spring B 6. Creates a {boot-run-javadoc}['BootRun`] task named `bootTestRun` that can be used to run your application using the `test` source set to find its main method and provide its runtime classpath. 7. Creates a configuration named `bootArchives` that contains the artifact produced by the `bootJar` task. 8. Creates a configuration named `developmentOnly` for dependencies that are only required at development time, such as Spring Boot's Devtools, and should not be packaged in executable jars and wars. -9. Creates a configuration named `productionRuntimeClasspath`. It is equivalent to `runtimeClasspath` minus any dependencies that only appear in the `developmentOnly` configuration. -10. Configures any `JavaCompile` tasks with no configured encoding to use `UTF-8`. -11. Configures any `JavaCompile` tasks to use the `-parameters` compiler argument. +9. Creats a configuration named `testAndDevelopmentOnly` for dependencies that are only required at development time and when writing and running tests and that should not be packaged in executable jars and wars. +10. Creates a configuration named `productionRuntimeClasspath`. It is equivalent to `runtimeClasspath` minus any dependencies that only appear in the `developmentOnly` or `testDevelopmentOnly` configurations. +11. Configures any `JavaCompile` tasks with no configured encoding to use `UTF-8`. +12. Configures any `JavaCompile` tasks to use the `-parameters` compiler argument. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java index 945657605240..fd9ee0edd215 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java @@ -78,7 +78,9 @@ public Class> getPluginClass() { public void execute(Project project) { classifyJarTask(project); configureBuildTask(project); + configureProductionRuntimeClasspathConfiguration(project); configureDevelopmentOnlyConfiguration(project); + configureTestAndDevelopmentOnlyConfiguration(project); TaskProvider resolveMainClassName = configureResolveMainClassNameTask(project); TaskProvider bootJar = configureBootJarTask(project, resolveMainClassName); configureBootBuildImageTask(project, bootJar); @@ -160,12 +162,15 @@ private TaskProvider configureBootJarTask(Project project, .getByName(SourceSet.MAIN_SOURCE_SET_NAME); Configuration developmentOnly = project.getConfigurations() .getByName(SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME); + Configuration testAndDevelopmentOnly = project.getConfigurations() + .getByName(SpringBootPlugin.TEST_AND_DEVELOPMENT_ONLY_CONFIGURATION_NAME); Configuration productionRuntimeClasspath = project.getConfigurations() .getByName(SpringBootPlugin.PRODUCTION_RUNTIME_CLASSPATH_CONFIGURATION_NAME); Configuration runtimeClasspath = project.getConfigurations() .getByName(mainSourceSet.getRuntimeClasspathConfigurationName()); Callable classpath = () -> mainSourceSet.getRuntimeClasspath() .minus((developmentOnly.minus(productionRuntimeClasspath))) + .minus((testAndDevelopmentOnly.minus(productionRuntimeClasspath))) .filter(new JarTypeFileSpec()); return project.getTasks().register(SpringBootPlugin.BOOT_JAR_TASK_NAME, BootJar.class, (bootJar) -> { bootJar.setDescription( @@ -270,13 +275,7 @@ private void configureAdditionalMetadataLocations(JavaCompile compile) { .ifPresent((locations) -> compile.doFirst(new AdditionalMetadataLocationsConfigurer(locations))); } - private void configureDevelopmentOnlyConfiguration(Project project) { - Configuration developmentOnly = project.getConfigurations() - .create(SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME); - developmentOnly - .setDescription("Configuration for development-only dependencies such as Spring Boot's DevTools."); - Configuration runtimeClasspath = project.getConfigurations() - .getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); + private void configureProductionRuntimeClasspathConfiguration(Project project) { Configuration productionRuntimeClasspath = project.getConfigurations() .create(SpringBootPlugin.PRODUCTION_RUNTIME_CLASSPATH_CONFIGURATION_NAME); AttributeContainer attributes = productionRuntimeClasspath.getAttributes(); @@ -286,12 +285,37 @@ private void configureDevelopmentOnlyConfiguration(Project project) { attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objectFactory.named(LibraryElements.class, LibraryElements.JAR)); productionRuntimeClasspath.setVisible(false); + Configuration runtimeClasspath = project.getConfigurations() + .getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); productionRuntimeClasspath.setExtendsFrom(runtimeClasspath.getExtendsFrom()); productionRuntimeClasspath.setCanBeResolved(runtimeClasspath.isCanBeResolved()); productionRuntimeClasspath.setCanBeConsumed(runtimeClasspath.isCanBeConsumed()); + } + + private void configureDevelopmentOnlyConfiguration(Project project) { + Configuration developmentOnly = project.getConfigurations() + .create(SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME); + developmentOnly + .setDescription("Configuration for development-only dependencies such as Spring Boot's DevTools."); + Configuration runtimeClasspath = project.getConfigurations() + .getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); + runtimeClasspath.extendsFrom(developmentOnly); } + private void configureTestAndDevelopmentOnlyConfiguration(Project project) { + Configuration testAndDevelopmentOnly = project.getConfigurations() + .create(SpringBootPlugin.TEST_AND_DEVELOPMENT_ONLY_CONFIGURATION_NAME); + testAndDevelopmentOnly + .setDescription("Configuration for test and development-only dependencies such as Spring Boot's DevTools."); + Configuration runtimeClasspath = project.getConfigurations() + .getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); + runtimeClasspath.extendsFrom(testAndDevelopmentOnly); + Configuration testImplementation = project.getConfigurations() + .getByName(JavaPlugin.TEST_IMPLEMENTATION_CONFIGURATION_NAME); + testImplementation.extendsFrom(testAndDevelopmentOnly); + } + /** * Task {@link Action} to add additional meta-data locations. We need to use an * inner-class rather than a lambda due to diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java index 7c3ead680b39..c4689c80b242 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java @@ -83,7 +83,8 @@ private Iterable removeDevelopmentOnly(Set configu } private boolean isNotDevelopmentOnly(Configuration configuration) { - return !SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME.equals(configuration.getName()); + return !SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME.equals(configuration.getName()) + && !SpringBootPlugin.TEST_AND_DEVELOPMENT_ONLY_CONFIGURATION_NAME.equals(configuration.getName()); } private void configureTestNativeBinaryClasspath(SourceSetContainer sourceSets, GraalVMExtension graalVmExtension) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootAotPlugin.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootAotPlugin.java index f1cfc947283c..e9c9c7258a97 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootAotPlugin.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootAotPlugin.java @@ -117,7 +117,9 @@ private void configureJavaRuntimeUsageAttribute(Project project, AttributeContai private void registerProcessAotTask(Project project, SourceSet aotSourceSet, SourceSet mainSourceSet) { TaskProvider resolveMainClassName = project.getTasks() .named(SpringBootPlugin.RESOLVE_MAIN_CLASS_NAME_TASK_NAME, ResolveMainClassName.class); - Configuration aotClasspath = createAotProcessingClasspath(project, PROCESS_AOT_TASK_NAME, mainSourceSet); + Configuration aotClasspath = createAotProcessingClasspath(project, PROCESS_AOT_TASK_NAME, mainSourceSet, + Set.of(SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME, + SpringBootPlugin.TEST_AND_DEVELOPMENT_ONLY_CONFIGURATION_NAME)); project.getDependencies().add(aotClasspath.getName(), project.files(mainSourceSet.getOutput())); Configuration compileClasspath = project.getConfigurations() .getByName(aotSourceSet.getCompileClasspathConfigurationName()); @@ -152,14 +154,16 @@ private void configureAotTask(Project project, SourceSet sourceSet, AbstractAot } @SuppressWarnings({ "unchecked", "rawtypes" }) - private Configuration createAotProcessingClasspath(Project project, String taskName, SourceSet inputSourceSet) { + private Configuration createAotProcessingClasspath(Project project, String taskName, SourceSet inputSourceSet, + Set developmentOnlyConfigurationNames) { Configuration base = project.getConfigurations() .getByName(inputSourceSet.getRuntimeClasspathConfigurationName()); return project.getConfigurations().create(taskName + "Classpath", (classpath) -> { classpath.setCanBeConsumed(false); classpath.setCanBeResolved(true); classpath.setDescription("Classpath of the " + taskName + " task."); - removeDevelopmentOnly(base.getExtendsFrom()).forEach(classpath::extendsFrom); + removeDevelopmentOnly(base.getExtendsFrom(), developmentOnlyConfigurationNames) + .forEach(classpath::extendsFrom); classpath.attributes((attributes) -> { ProviderFactory providers = project.getProviders(); AttributeContainer baseAttributes = base.getAttributes(); @@ -171,12 +175,10 @@ private Configuration createAotProcessingClasspath(Project project, String taskN }); } - private Stream removeDevelopmentOnly(Set configurations) { - return configurations.stream().filter(this::isNotDevelopmentOnly); - } - - private boolean isNotDevelopmentOnly(Configuration configuration) { - return !SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME.equals(configuration.getName()); + private Stream removeDevelopmentOnly(Set configurations, + Set developmentOnlyConfigurationNames) { + return configurations.stream() + .filter((configuration) -> !developmentOnlyConfigurationNames.contains(configuration.getName())); } private void configureDependsOn(Project project, SourceSet aotSourceSet, @@ -188,7 +190,8 @@ private void configureDependsOn(Project project, SourceSet aotSourceSet, private void registerProcessTestAotTask(Project project, SourceSet mainSourceSet, SourceSet aotTestSourceSet, SourceSet testSourceSet) { - Configuration aotClasspath = createAotProcessingClasspath(project, PROCESS_TEST_AOT_TASK_NAME, testSourceSet); + Configuration aotClasspath = createAotProcessingClasspath(project, PROCESS_TEST_AOT_TASK_NAME, testSourceSet, + Set.of(SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME)); addJUnitPlatformLauncherDependency(project, aotClasspath); Configuration compileClasspath = project.getConfigurations() .getByName(aotTestSourceSet.getCompileClasspathConfigurationName()); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java index 3e78fb3cee21..b85ccfc86356 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java @@ -80,6 +80,12 @@ public class SpringBootPlugin implements Plugin { */ public static final String DEVELOPMENT_ONLY_CONFIGURATION_NAME = "developmentOnly"; + /** + * The name of the {@code testAndDevelopmentOnly} configuration. + * @since 3.2.0 + */ + public static final String TEST_AND_DEVELOPMENT_ONLY_CONFIGURATION_NAME = "testAndDevelopmentOnly"; + /** * The name of the {@code productionRuntimeClasspath} configuration. */ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/WarPluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/WarPluginAction.java index 2e1382700117..6e2517ebaee9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/WarPluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/WarPluginAction.java @@ -72,6 +72,8 @@ private void classifyWarTask(Project project) { private TaskProvider configureBootWarTask(Project project) { Configuration developmentOnly = project.getConfigurations() .getByName(SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME); + Configuration testAndDevelopmentOnly = project.getConfigurations() + .getByName(SpringBootPlugin.TEST_AND_DEVELOPMENT_ONLY_CONFIGURATION_NAME); Configuration productionRuntimeClasspath = project.getConfigurations() .getByName(SpringBootPlugin.PRODUCTION_RUNTIME_CLASSPATH_CONFIGURATION_NAME); SourceSet mainSourceSet = project.getExtensions() @@ -82,6 +84,7 @@ private TaskProvider configureBootWarTask(Project project) { Callable classpath = () -> mainSourceSet.getRuntimeClasspath() .minus(providedRuntimeConfiguration(project)) .minus((developmentOnly.minus(productionRuntimeClasspath))) + .minus((testAndDevelopmentOnly.minus(productionRuntimeClasspath))) .filter(new JarTypeFileSpec()); TaskProvider resolveMainClassName = project.getTasks() .named(SpringBootPlugin.RESOLVE_MAIN_CLASS_NAME_TASK_NAME, ResolveMainClassName.class); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests.java index 0b62f88b5d5c..d630e687d2db 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests.java @@ -142,7 +142,52 @@ void additionalMetadataLocationsNotConfiguredWhenProcessorIsAbsent() throws IOEx @TestTemplate void applyingJavaPluginCreatesDevelopmentOnlyConfiguration() { - assertThat(this.gradleBuild.build("build").getOutput()).contains("developmentOnly exists = true"); + assertThat(this.gradleBuild.build("help").getOutput()).contains("developmentOnly exists = true"); + } + + @TestTemplate + void applyingJavaPluginCreatesTestAndDevelopmentOnlyConfiguration() { + assertThat(this.gradleBuild.build("help").getOutput()).contains("testAndDevelopmentOnly exists = true"); + } + + @TestTemplate + void testCompileClasspathIncludesTestAndDevelopmentOnlyDependencies() { + assertThat(this.gradleBuild.build("help").getOutput()).contains("commons-lang3-3.12.0.jar"); + } + + @TestTemplate + void testRuntimeClasspathIncludesTestAndDevelopmentOnlyDependencies() { + assertThat(this.gradleBuild.build("help").getOutput()).contains("commons-lang3-3.12.0.jar"); + } + + @TestTemplate + void testCompileClasspathDoesNotIncludeDevelopmentOnlyDependencies() { + assertThat(this.gradleBuild.build("help").getOutput()).doesNotContain("commons-lang3-3.12.0.jar"); + } + + @TestTemplate + void testRuntimeClasspathDoesNotIncludeDevelopmentOnlyDependencies() { + assertThat(this.gradleBuild.build("help").getOutput()).doesNotContain("commons-lang3-3.12.0.jar"); + } + + @TestTemplate + void compileClasspathDoesNotIncludeTestAndDevelopmentOnlyDependencies() { + assertThat(this.gradleBuild.build("help").getOutput()).doesNotContain("commons-lang3-3.12.0.jar"); + } + + @TestTemplate + void runtimeClasspathIncludesTestAndDevelopmentOnlyDependencies() { + assertThat(this.gradleBuild.build("help").getOutput()).contains("commons-lang3-3.12.0.jar"); + } + + @TestTemplate + void compileClasspathDoesNotIncludeDevelopmentOnlyDependencies() { + assertThat(this.gradleBuild.build("help").getOutput()).doesNotContain("commons-lang3-3.12.0.jar"); + } + + @TestTemplate + void runtimeClasspathIncludesDevelopmentOnlyDependencies() { + assertThat(this.gradleBuild.build("help").getOutput()).contains("commons-lang3-3.12.0.jar"); } @TestTemplate diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java index 4d15614a7057..87cc9458aec1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java @@ -105,6 +105,14 @@ void developmentOnlyDependenciesDoNotAppearInNativeImageClasspath() { assertThat(result.getOutput()).doesNotContain("commons-lang"); } + @TestTemplate + void testAndDevelopmentOnlyDependenciesDoNotAppearInNativeImageClasspath() { + writeDummySpringApplicationAotProcessorMainClass(); + BuildResult result = this.gradleBuild.expectDeprecationWarningsWithAtLeastVersion("8.2-rc-1") + .build("checkNativeImageClasspath"); + assertThat(result.getOutput()).doesNotContain("commons-lang"); + } + @TestTemplate void classesGeneratedDuringAotProcessingAreOnTheNativeImageClasspath() { BuildResult result = this.gradleBuild.expectDeprecationWarningsWithAtLeastVersion("8.2-rc-1") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/SpringBootAotPluginIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/SpringBootAotPluginIntegrationTests.java index aead5ebcc535..7abf855bbeae 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/SpringBootAotPluginIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/SpringBootAotPluginIntegrationTests.java @@ -106,10 +106,22 @@ void processAotDoesNotHaveDevelopmentOnlyDependenciesOnItsClasspath() { @TestTemplate void processTestAotDoesNotHaveDevelopmentOnlyDependenciesOnItsClasspath() { - String output = this.gradleBuild.build("processTestAotClasspath", "--stacktrace").getOutput(); + String output = this.gradleBuild.build("processTestAotClasspath").getOutput(); + assertThat(output).doesNotContain("commons-lang"); + } + + @TestTemplate + void processAotDoesNotHaveTestAndDevelopmentOnlyDependenciesOnItsClasspath() { + String output = this.gradleBuild.build("processAotClasspath").getOutput(); assertThat(output).doesNotContain("commons-lang"); } + @TestTemplate + void processTestAotHasTestAndDevelopmentOnlyDependenciesOnItsClasspath() { + String output = this.gradleBuild.build("processTestAotClasspath").getOutput(); + assertThat(output).contains("commons-lang"); + } + @TestTemplate void processAotRunsWhenProjectHasMainSource() throws IOException { writeMainClass("org.springframework.boot", "SpringApplicationAotProcessor"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java index fa1b22e88a2b..a01d3bd4157e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java @@ -221,6 +221,41 @@ void developmentOnlyDependenciesCanBeIncludedInTheArchive() throws IOException { } } + @TestTemplate + void testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault() throws IOException { + File srcMainResources = new File(this.gradleBuild.getProjectDir(), "src/main/resources"); + srcMainResources.mkdirs(); + new File(srcMainResources, "resource").createNewFile(); + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) + .isEqualTo(TaskOutcome.SUCCESS); + try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { + Stream libEntryNames = jarFile.stream() + .filter((entry) -> !entry.isDirectory()) + .map(JarEntry::getName) + .filter((name) -> name.startsWith(this.libPath)); + assertThat(libEntryNames).containsExactly(this.libPath + "commons-io-2.6.jar"); + Stream classesEntryNames = jarFile.stream() + .filter((entry) -> !entry.isDirectory()) + .map(JarEntry::getName) + .filter((name) -> name.startsWith(this.classesPath)); + assertThat(classesEntryNames).containsExactly(this.classesPath + "resource"); + } + } + + @TestTemplate + void testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive() throws IOException { + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) + .isEqualTo(TaskOutcome.SUCCESS); + try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { + Stream libEntryNames = jarFile.stream() + .filter((entry) -> !entry.isDirectory()) + .map(JarEntry::getName) + .filter((name) -> name.startsWith(this.libPath)); + assertThat(libEntryNames).containsExactly(this.libPath + "commons-io-2.6.jar", + this.libPath + "commons-lang3-3.9.jar"); + } + } + @TestTemplate void jarTypeFilteringIsApplied() throws IOException { File flatDirRepository = new File(this.gradleBuild.getProjectDir(), "repository"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests.java index 7550399b8824..54ee2a33f0f5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests.java @@ -146,6 +146,22 @@ void classesFromASecondarySourceSetCanBeOnTheClasspath() throws IOException { assertThat(result.getOutput()).contains("com.example.bootrun.main.CustomMainClass"); } + @TestTemplate + void developmentOnlyDependenciesAreOnTheClasspath() throws IOException { + copyClasspathApplication(); + BuildResult result = this.gradleBuild.build("bootRun"); + assertThat(result.task(":bootRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("commons-lang3-3.12.0.jar"); + } + + @TestTemplate + void testAndDevelopmentOnlyDependenciesAreOnTheClasspath() throws IOException { + copyClasspathApplication(); + BuildResult result = this.gradleBuild.build("bootRun"); + assertThat(result.task(":bootRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("commons-lang3-3.12.0.jar"); + } + private void copyMainClassApplication() throws IOException { copyApplication("main"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/run/BootTestRunIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/run/BootTestRunIntegrationTests.java index 8bb449d09543..79b884dcb7ef 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/run/BootTestRunIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/run/BootTestRunIntegrationTests.java @@ -117,6 +117,22 @@ void failsGracefullyWhenNoTestMainMethodIsFound() throws IOException { } } + @TestTemplate + void developmentOnlyDependenciesAreNotOnTheClasspath() throws IOException { + copyClasspathApplication(); + BuildResult result = this.gradleBuild.build("bootTestRun"); + assertThat(result.task(":bootTestRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).doesNotContain("commons-lang3-3.12.0.jar"); + } + + @TestTemplate + void testAndDevelopmentOnlyDependenciesAreOnTheClasspath() throws IOException { + copyClasspathApplication(); + BuildResult result = this.gradleBuild.build("bootTestRun"); + assertThat(result.task(":bootTestRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("commons-lang3-3.12.0.jar"); + } + private void copyClasspathApplication() throws IOException { copyApplication("classpath"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-applyingJavaPluginCreatesTestAndDevelopmentOnlyConfiguration.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-applyingJavaPluginCreatesTestAndDevelopmentOnlyConfiguration.gradle new file mode 100644 index 000000000000..ebf12ae42908 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-applyingJavaPluginCreatesTestAndDevelopmentOnlyConfiguration.gradle @@ -0,0 +1,12 @@ +plugins { + id 'org.springframework.boot' version '{version}' + id 'java' +} + +springBoot { + mainClass = "com.example.Main" +} + +gradle.taskGraph.whenReady { + println "testAndDevelopmentOnly exists = ${configurations.findByName('testAndDevelopmentOnly') != null}" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-compileClasspathDoesNotIncludeDevelopmentOnlyDependencies.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-compileClasspathDoesNotIncludeDevelopmentOnlyDependencies.gradle new file mode 100644 index 000000000000..b956631b4634 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-compileClasspathDoesNotIncludeDevelopmentOnlyDependencies.gradle @@ -0,0 +1,20 @@ +plugins { + id 'org.springframework.boot' version '{version}' + id 'java' +} + +springBoot { + mainClass = "com.example.Main" +} + +repositories { + mavenCentral() +} + +dependencies { + developmentOnly("org.apache.commons:commons-lang3:3.12.0") +} + +gradle.taskGraph.whenReady { + configurations.compileClasspath.resolve().each { println it } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-compileClasspathDoesNotIncludeTestAndDevelopmentOnlyDependencies.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-compileClasspathDoesNotIncludeTestAndDevelopmentOnlyDependencies.gradle new file mode 100644 index 000000000000..6e53332c97f5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-compileClasspathDoesNotIncludeTestAndDevelopmentOnlyDependencies.gradle @@ -0,0 +1,20 @@ +plugins { + id 'org.springframework.boot' version '{version}' + id 'java' +} + +springBoot { + mainClass = "com.example.Main" +} + +repositories { + mavenCentral() +} + +dependencies { + testAndDevelopmentOnly("org.apache.commons:commons-lang3:3.12.0") +} + +gradle.taskGraph.whenReady { + configurations.compileClasspath.resolve().each { println it } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-runtimeClasspathIncludesDevelopmentOnlyDependencies.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-runtimeClasspathIncludesDevelopmentOnlyDependencies.gradle new file mode 100644 index 000000000000..a8b1f4d3bffd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-runtimeClasspathIncludesDevelopmentOnlyDependencies.gradle @@ -0,0 +1,20 @@ +plugins { + id 'org.springframework.boot' version '{version}' + id 'java' +} + +springBoot { + mainClass = "com.example.Main" +} + +repositories { + mavenCentral() +} + +dependencies { + developmentOnly("org.apache.commons:commons-lang3:3.12.0") +} + +gradle.taskGraph.whenReady { + configurations.runtimeClasspath.resolve().each { println it } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-runtimeClasspathIncludesTestAndDevelopmentOnlyDependencies.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-runtimeClasspathIncludesTestAndDevelopmentOnlyDependencies.gradle new file mode 100644 index 000000000000..6a51fe371128 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-runtimeClasspathIncludesTestAndDevelopmentOnlyDependencies.gradle @@ -0,0 +1,20 @@ +plugins { + id 'org.springframework.boot' version '{version}' + id 'java' +} + +springBoot { + mainClass = "com.example.Main" +} + +repositories { + mavenCentral() +} + +dependencies { + testAndDevelopmentOnly("org.apache.commons:commons-lang3:3.12.0") +} + +gradle.taskGraph.whenReady { + configurations.runtimeClasspath.resolve().each { println it } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-testCompileClasspathDoesNotIncludeDevelopmentOnlyDependencies.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-testCompileClasspathDoesNotIncludeDevelopmentOnlyDependencies.gradle new file mode 100644 index 000000000000..264944e602a6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-testCompileClasspathDoesNotIncludeDevelopmentOnlyDependencies.gradle @@ -0,0 +1,20 @@ +plugins { + id 'org.springframework.boot' version '{version}' + id 'java' +} + +springBoot { + mainClass = "com.example.Main" +} + +repositories { + mavenCentral() +} + +dependencies { + developmentOnly("org.apache.commons:commons-lang3:3.12.0") +} + +gradle.taskGraph.whenReady { + configurations.testCompileClasspath.resolve().each { println it } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-testCompileClasspathIncludesTestAndDevelopmentOnlyDependencies.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-testCompileClasspathIncludesTestAndDevelopmentOnlyDependencies.gradle new file mode 100644 index 000000000000..1f934deadb11 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-testCompileClasspathIncludesTestAndDevelopmentOnlyDependencies.gradle @@ -0,0 +1,20 @@ +plugins { + id 'org.springframework.boot' version '{version}' + id 'java' +} + +springBoot { + mainClass = "com.example.Main" +} + +repositories { + mavenCentral() +} + +dependencies { + testAndDevelopmentOnly("org.apache.commons:commons-lang3:3.12.0") +} + +gradle.taskGraph.whenReady { + configurations.testCompileClasspath.resolve().each { println it } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-testRuntimeClasspathDoesNotIncludeDevelopmentOnlyDependencies.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-testRuntimeClasspathDoesNotIncludeDevelopmentOnlyDependencies.gradle new file mode 100644 index 000000000000..4334e2a97bd2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-testRuntimeClasspathDoesNotIncludeDevelopmentOnlyDependencies.gradle @@ -0,0 +1,20 @@ +plugins { + id 'org.springframework.boot' version '{version}' + id 'java' +} + +springBoot { + mainClass = "com.example.Main" +} + +repositories { + mavenCentral() +} + +dependencies { + developmentOnly("org.apache.commons:commons-lang3:3.12.0") +} + +gradle.taskGraph.whenReady { + configurations.testRuntimeClasspath.resolve().each { println it } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-testRuntimeClasspathIncludesTestAndDevelopmentOnlyDependencies.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-testRuntimeClasspathIncludesTestAndDevelopmentOnlyDependencies.gradle new file mode 100644 index 000000000000..581c58617968 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-testRuntimeClasspathIncludesTestAndDevelopmentOnlyDependencies.gradle @@ -0,0 +1,20 @@ +plugins { + id 'org.springframework.boot' version '{version}' + id 'java' +} + +springBoot { + mainClass = "com.example.Main" +} + +repositories { + mavenCentral() +} + +dependencies { + testAndDevelopmentOnly("org.apache.commons:commons-lang3:3.12.0") +} + +gradle.taskGraph.whenReady { + configurations.testRuntimeClasspath.resolve().each { println it } +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests-testAndDevelopmentOnlyDependenciesDoNotAppearInNativeImageClasspath.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests-testAndDevelopmentOnlyDependenciesDoNotAppearInNativeImageClasspath.gradle new file mode 100644 index 000000000000..62d4912299a1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests-testAndDevelopmentOnlyDependenciesDoNotAppearInNativeImageClasspath.gradle @@ -0,0 +1,20 @@ +plugins { + id 'org.springframework.boot' version '{version}' + id 'java' +} + +apply plugin: 'org.graalvm.buildtools.native' + +repositories { + mavenCentral() +} + +dependencies { + testAndDevelopmentOnly("org.apache.commons:commons-lang3:3.12.0") +} + +task('checkNativeImageClasspath') { + doFirst { + tasks.nativeCompile.options.get().classpath.each { println it } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/SpringBootAotPluginIntegrationTests-processAotDoesNotHaveTestAndDevelopmentOnlyDependenciesOnItsClasspath.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/SpringBootAotPluginIntegrationTests-processAotDoesNotHaveTestAndDevelopmentOnlyDependenciesOnItsClasspath.gradle new file mode 100644 index 000000000000..0568dc1a9c21 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/SpringBootAotPluginIntegrationTests-processAotDoesNotHaveTestAndDevelopmentOnlyDependenciesOnItsClasspath.gradle @@ -0,0 +1,20 @@ +plugins { + id 'org.springframework.boot' version '{version}' + id 'java' +} + +apply plugin: 'org.springframework.boot.aot' + +repositories { + mavenCentral() +} + +dependencies { + testAndDevelopmentOnly("org.apache.commons:commons-lang3:3.12.0") +} + +task('processAotClasspath') { + doFirst { + tasks.processAot.classpath.each { println it } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/SpringBootAotPluginIntegrationTests-processTestAotHasTestAndDevelopmentOnlyDependenciesOnItsClasspath.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/SpringBootAotPluginIntegrationTests-processTestAotHasTestAndDevelopmentOnlyDependenciesOnItsClasspath.gradle new file mode 100644 index 000000000000..fe8815af3f30 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/SpringBootAotPluginIntegrationTests-processTestAotHasTestAndDevelopmentOnlyDependenciesOnItsClasspath.gradle @@ -0,0 +1,31 @@ +plugins { + id 'org.springframework.boot' version '{version}' + id 'java' +} + +apply plugin: 'org.springframework.boot.aot' + +repositories { + mavenCentral() + maven { url 'file:repository' } +} + +configurations.all { + resolutionStrategy { + eachDependency { + if (it.requested.group == 'org.springframework.boot') { + it.useVersion project.bootVersion + } + } + } +} + +dependencies { + testAndDevelopmentOnly("org.apache.commons:commons-lang3:3.12.0") +} + +task('processTestAotClasspath') { + doFirst { + tasks.processTestAot.classpath.each { println it } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle new file mode 100644 index 000000000000..7f4ca313065c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle @@ -0,0 +1,24 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' +} + +repositories { + mavenCentral() +} + +dependencies { + testAndDevelopmentOnly("org.apache.commons:commons-lang3:3.9") + testAndDevelopmentOnly("commons-io:commons-io:2.6") + implementation("commons-io:commons-io:2.6") +} + +bootJar { + layered { + enabled = false + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle new file mode 100644 index 000000000000..45041d1c1908 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle @@ -0,0 +1,27 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' +} + +repositories { + mavenCentral() +} + +dependencies { + testAndDevelopmentOnly("org.apache.commons:commons-lang3:3.9") + implementation("commons-io:commons-io:2.6") +} + +bootJar { + classpath configurations.testAndDevelopmentOnly +} + +bootJar { + layered { + enabled = false + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle new file mode 100644 index 000000000000..f2d285e40810 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle @@ -0,0 +1,24 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{version}' +} + +bootWar { + mainClass = 'com.example.Application' +} + +repositories { + mavenCentral() +} + +dependencies { + testAndDevelopmentOnly("org.apache.commons:commons-lang3:3.9") + testAndDevelopmentOnly("commons-io:commons-io:2.6") + implementation("commons-io:commons-io:2.6") +} + +bootWar { + layered { + enabled = false + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle new file mode 100644 index 000000000000..de8e9d652170 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle @@ -0,0 +1,27 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{version}' +} + +bootWar { + mainClass = 'com.example.Application' +} + +repositories { + mavenCentral() +} + +dependencies { + testAndDevelopmentOnly("org.apache.commons:commons-lang3:3.9") + implementation("commons-io:commons-io:2.6") +} + +bootWar { + classpath configurations.testAndDevelopmentOnly +} + +bootWar { + layered { + enabled = false + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-developmentOnlyDependenciesAreOnTheClasspath.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-developmentOnlyDependenciesAreOnTheClasspath.gradle new file mode 100644 index 000000000000..39b58bb700d6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-developmentOnlyDependenciesAreOnTheClasspath.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +repositories { + mavenCentral() +} + +dependencies { + developmentOnly("org.apache.commons:commons-lang3:3.12.0") +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-testAndDevelopmentOnlyDependenciesAreOnTheClasspath.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-testAndDevelopmentOnlyDependenciesAreOnTheClasspath.gradle new file mode 100644 index 000000000000..6d4b5ce828fb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-testAndDevelopmentOnlyDependenciesAreOnTheClasspath.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +repositories { + mavenCentral() +} + +dependencies { + testAndDevelopmentOnly("org.apache.commons:commons-lang3:3.12.0") +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootTestRunIntegrationTests-developmentOnlyDependenciesAreNotOnTheClasspath.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootTestRunIntegrationTests-developmentOnlyDependenciesAreNotOnTheClasspath.gradle new file mode 100644 index 000000000000..39b58bb700d6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootTestRunIntegrationTests-developmentOnlyDependenciesAreNotOnTheClasspath.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +repositories { + mavenCentral() +} + +dependencies { + developmentOnly("org.apache.commons:commons-lang3:3.12.0") +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootTestRunIntegrationTests-testAndDevelopmentOnlyDependenciesAreOnTheClasspath.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootTestRunIntegrationTests-testAndDevelopmentOnlyDependenciesAreOnTheClasspath.gradle new file mode 100644 index 000000000000..6d4b5ce828fb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootTestRunIntegrationTests-testAndDevelopmentOnlyDependenciesAreOnTheClasspath.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +repositories { + mavenCentral() +} + +dependencies { + testAndDevelopmentOnly("org.apache.commons:commons-lang3:3.12.0") +} \ No newline at end of file From 72a4e1ebaef072c5a8f77319667cf4e22cc3812c Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Mon, 25 Sep 2023 14:20:09 +0200 Subject: [PATCH 0509/1215] Honor timeout in ZipkinWebClientSender Unfortunately there's no good way to configure connect and read timeout separately, which works for all supported reactive clients. This implementation applies a timeout through Reactor's timeout method. The timeout from the properties is summed together and this is the applied timeout. While not perfect, this is better than no timeout at all. Closes gh-31496 --- .../tracing/zipkin/ZipkinConfigurations.java | 3 +- .../tracing/zipkin/ZipkinWebClientSender.java | 20 +++-- .../tracing/zipkin/ZipkinHttpSenderTests.java | 33 +++++--- .../zipkin/ZipkinRestTemplateSenderTests.java | 10 ++- .../zipkin/ZipkinWebClientSenderTests.java | 76 +++++++++++++++---- 5 files changed, 110 insertions(+), 32 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurations.java index b4a1802b7bc6..f4ecc9503125 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurations.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinConfigurations.java @@ -119,7 +119,8 @@ ZipkinWebClientSender webClientSender(ZipkinProperties properties, .getIfAvailable(() -> new PropertiesZipkinConnectionDetails(properties)); WebClient.Builder builder = WebClient.builder(); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); - return new ZipkinWebClientSender(connectionDetails.getSpanEndpoint(), builder.build()); + return new ZipkinWebClientSender(connectionDetails.getSpanEndpoint(), builder.build(), + properties.getConnectTimeout().plus(properties.getReadTimeout())); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSender.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSender.java index b9992bd5754d..2ef8cb74c09a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSender.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSender.java @@ -16,6 +16,8 @@ package org.springframework.boot.actuate.autoconfigure.tracing.zipkin; +import java.time.Duration; + import reactor.core.publisher.Mono; import zipkin2.Call; import zipkin2.Callback; @@ -28,6 +30,7 @@ * An {@link HttpSender} which uses {@link WebClient} for HTTP communication. * * @author Stefan Bratanov + * @author Moritz Halbritter */ class ZipkinWebClientSender extends HttpSender { @@ -35,14 +38,17 @@ class ZipkinWebClientSender extends HttpSender { private final WebClient webClient; - ZipkinWebClientSender(String endpoint, WebClient webClient) { + private final Duration timeout; + + ZipkinWebClientSender(String endpoint, WebClient webClient, Duration timeout) { this.endpoint = endpoint; this.webClient = webClient; + this.timeout = timeout; } @Override public HttpPostCall sendSpans(byte[] batchedEncodedSpans) { - return new WebClientHttpPostCall(this.endpoint, batchedEncodedSpans, this.webClient); + return new WebClientHttpPostCall(this.endpoint, batchedEncodedSpans, this.webClient, this.timeout); } private static class WebClientHttpPostCall extends HttpPostCall { @@ -51,15 +57,18 @@ private static class WebClientHttpPostCall extends HttpPostCall { private final WebClient webClient; - WebClientHttpPostCall(String endpoint, byte[] body, WebClient webClient) { + private final Duration timeout; + + WebClientHttpPostCall(String endpoint, byte[] body, WebClient webClient, Duration timeout) { super(body); this.endpoint = endpoint; this.webClient = webClient; + this.timeout = timeout; } @Override public Call clone() { - return new WebClientHttpPostCall(this.endpoint, getUncompressedBody(), this.webClient); + return new WebClientHttpPostCall(this.endpoint, getUncompressedBody(), this.webClient, this.timeout); } @Override @@ -79,7 +88,8 @@ private Mono> sendRequest() { .headers(this::addDefaultHeaders) .bodyValue(getBody()) .retrieve() - .toBodilessEntity(); + .toBodilessEntity() + .timeout(this.timeout); } private void addDefaultHeaders(HttpHeaders headers) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinHttpSenderTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinHttpSenderTests.java index 970a8c300950..80654a3e55d4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinHttpSenderTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinHttpSenderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -25,6 +25,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import zipkin2.Callback; @@ -42,19 +43,25 @@ */ abstract class ZipkinHttpSenderTests { - protected Sender sut; + protected Sender sender; - abstract Sender createSut(); + abstract Sender createSender(); @BeforeEach - void setUp() { - this.sut = createSut(); + void beforeEach() throws Exception { + this.sender = createSender(); + } + + @AfterEach + void afterEach() throws IOException { + this.sender.close(); } @Test void sendSpansShouldThrowIfCloseWasCalled() throws IOException { - this.sut.close(); - assertThatThrownBy(() -> this.sut.sendSpans(Collections.emptyList())).isInstanceOf(ClosedSenderException.class); + this.sender.close(); + assertThatThrownBy(() -> this.sender.sendSpans(Collections.emptyList())) + .isInstanceOf(ClosedSenderException.class); } protected void makeRequest(List encodedSpans, boolean async) throws IOException { @@ -68,8 +75,12 @@ protected void makeRequest(List encodedSpans, boolean async) throws IOEx } protected CallbackResult makeAsyncRequest(List encodedSpans) { + return makeAsyncRequest(this.sender, encodedSpans); + } + + protected CallbackResult makeAsyncRequest(Sender sender, List encodedSpans) { AtomicReference callbackResult = new AtomicReference<>(); - this.sut.sendSpans(encodedSpans).enqueue(new Callback<>() { + sender.sendSpans(encodedSpans).enqueue(new Callback<>() { @Override public void onSuccess(Void value) { callbackResult.set(new CallbackResult(true, null)); @@ -84,7 +95,11 @@ public void onError(Throwable t) { } protected void makeSyncRequest(List encodedSpans) throws IOException { - this.sut.sendSpans(encodedSpans).execute(); + makeSyncRequest(this.sender, encodedSpans); + } + + protected void makeSyncRequest(Sender sender, List encodedSpans) throws IOException { + sender.sendSpans(encodedSpans).execute(); } protected byte[] toByteArray(String input) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinRestTemplateSenderTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinRestTemplateSenderTests.java index ec945ac3da38..c5809f04150e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinRestTemplateSenderTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinRestTemplateSenderTests.java @@ -54,14 +54,16 @@ class ZipkinRestTemplateSenderTests extends ZipkinHttpSenderTests { private MockRestServiceServer mockServer; @Override - Sender createSut() { + Sender createSender() { RestTemplate restTemplate = new RestTemplate(); this.mockServer = MockRestServiceServer.createServer(restTemplate); return new ZipkinRestTemplateSender(ZIPKIN_URL, restTemplate); } @AfterEach - void tearDown() { + @Override + void afterEach() throws IOException { + super.afterEach(); this.mockServer.verify(); } @@ -71,7 +73,7 @@ void checkShouldSendEmptySpanList() { .andExpect(method(HttpMethod.POST)) .andExpect(content().string("[]")) .andRespond(withStatus(HttpStatus.ACCEPTED)); - assertThat(this.sut.check()).isEqualTo(CheckResult.OK); + assertThat(this.sender.check()).isEqualTo(CheckResult.OK); } @Test @@ -79,7 +81,7 @@ void checkShouldNotRaiseException() { this.mockServer.expect(requestTo(ZIPKIN_URL)) .andExpect(method(HttpMethod.POST)) .andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR)); - CheckResult result = this.sut.check(); + CheckResult result = this.sender.check(); assertThat(result.ok()).isFalse(); assertThat(result.error()).hasMessageContaining("500 Internal Server Error"); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSenderTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSenderTests.java index dc19d987cab7..b5b1b42b7e29 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSenderTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSenderTests.java @@ -17,16 +17,21 @@ package org.springframework.boot.actuate.autoconfigure.tracing.zipkin; import java.io.IOException; +import java.time.Duration; import java.util.Base64; import java.util.Collections; import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.QueueDispatcher; import okhttp3.mockwebserver.RecordedRequest; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -45,33 +50,48 @@ */ class ZipkinWebClientSenderTests extends ZipkinHttpSenderTests { + private static ClearableDispatcher dispatcher; + private static MockWebServer mockBackEnd; private static String ZIPKIN_URL; @BeforeAll static void beforeAll() throws IOException { + dispatcher = new ClearableDispatcher(); mockBackEnd = new MockWebServer(); + mockBackEnd.setDispatcher(dispatcher); mockBackEnd.start(); - ZIPKIN_URL = "http://localhost:%s/api/v2/spans".formatted(mockBackEnd.getPort()); + ZIPKIN_URL = mockBackEnd.url("/api/v2/spans").toString(); } @AfterAll - static void tearDown() throws IOException { + static void afterAll() throws IOException { mockBackEnd.shutdown(); } @Override - Sender createSut() { + @BeforeEach + void beforeEach() throws Exception { + super.beforeEach(); + clearResponses(); + clearRequests(); + } + + @Override + Sender createSender() { + return createSender(Duration.ofSeconds(10)); + } + + Sender createSender(Duration timeout) { WebClient webClient = WebClient.builder().build(); - return new ZipkinWebClientSender(ZIPKIN_URL, webClient); + return new ZipkinWebClientSender(ZIPKIN_URL, webClient, timeout); } @Test void checkShouldSendEmptySpanList() throws InterruptedException { mockBackEnd.enqueue(new MockResponse()); - assertThat(this.sut.check()).isEqualTo(CheckResult.OK); - + assertThat(this.sender.check()).isEqualTo(CheckResult.OK); requestAssertions((request) -> { assertThat(request.getMethod()).isEqualTo("POST"); assertThat(request.getBody().readUtf8()).isEqualTo("[]"); @@ -81,10 +101,9 @@ void checkShouldSendEmptySpanList() throws InterruptedException { @Test void checkShouldNotRaiseException() throws InterruptedException { mockBackEnd.enqueue(new MockResponse().setResponseCode(500)); - CheckResult result = this.sut.check(); + CheckResult result = this.sender.check(); assertThat(result.ok()).isFalse(); assertThat(result.error()).hasMessageContaining("500 Internal Server Error"); - requestAssertions((request) -> assertThat(request.getMethod()).isEqualTo("POST")); } @@ -94,7 +113,6 @@ void sendSpansShouldSendSpansToZipkin(boolean async) throws IOException, Interru mockBackEnd.enqueue(new MockResponse()); List encodedSpans = List.of(toByteArray("span1"), toByteArray("span2")); makeRequest(encodedSpans, async); - requestAssertions((request) -> { assertThat(request.getMethod()).isEqualTo("POST"); assertThat(request.getHeader("Content-Type")).isEqualTo("application/json"); @@ -115,7 +133,6 @@ void sendSpansShouldHandleHttpFailures(boolean async) throws InterruptedExceptio assertThatThrownBy(() -> makeSyncRequest(Collections.emptyList())) .hasMessageContaining("500 Internal Server Error"); } - requestAssertions((request) -> assertThat(request.getMethod()).isEqualTo("POST")); } @@ -126,18 +143,31 @@ void sendSpansShouldCompressData(boolean async) throws IOException, InterruptedE // This is gzip compressed 10000 times 'a' byte[] compressed = Base64.getDecoder() .decode("H4sIAAAAAAAA/+3BMQ0AAAwDIKFLj/k3UR8NcA8AAAAAAAAAAAADUsAZfeASJwAA"); - mockBackEnd.enqueue(new MockResponse()); - makeRequest(List.of(toByteArray(uncompressed)), async); - requestAssertions((request) -> { assertThat(request.getMethod()).isEqualTo("POST"); assertThat(request.getHeader("Content-Type")).isEqualTo("application/json"); assertThat(request.getHeader("Content-Encoding")).isEqualTo("gzip"); assertThat(request.getBody().readByteArray()).isEqualTo(compressed); }); + } + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void shouldTimeout(boolean async) { + Sender sender = createSender(Duration.ofMillis(1)); + MockResponse response = new MockResponse().setResponseCode(200).setHeadersDelay(100, TimeUnit.MILLISECONDS); + mockBackEnd.enqueue(response); + if (async) { + CallbackResult callbackResult = makeAsyncRequest(sender, Collections.emptyList()); + assertThat(callbackResult.success()).isFalse(); + assertThat(callbackResult.error()).isNotNull().isInstanceOf(TimeoutException.class); + } + else { + assertThatThrownBy(() -> makeSyncRequest(sender, Collections.emptyList())) + .hasCauseInstanceOf(TimeoutException.class); + } } private void requestAssertions(Consumer assertions) throws InterruptedException { @@ -145,4 +175,24 @@ private void requestAssertions(Consumer assertions) throws Inte assertThat(request).satisfies(assertions); } + private static void clearRequests() throws InterruptedException { + RecordedRequest request; + do { + request = mockBackEnd.takeRequest(0, TimeUnit.SECONDS); + } + while (request != null); + } + + private static void clearResponses() { + dispatcher.clear(); + } + + private static class ClearableDispatcher extends QueueDispatcher { + + void clear() { + getResponseQueue().clear(); + } + + } + } From 96986a6b51959e862a16dba6da6366479f7d4997 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 28 Sep 2023 14:48:37 +0100 Subject: [PATCH 0510/1215] Break cycle between TransactionManagerCustomizers and TransactionManager Closes gh-36801 --- .../neo4j/Neo4jDataAutoConfiguration.java | 4 +- ...ceTransactionManagerAutoConfiguration.java | 4 +- .../jpa/HibernateJpaAutoConfiguration.java | 5 +- .../TransactionAutoConfiguration.java | 10 --- ...ManagerCustomizationAutoConfiguration.java | 46 ++++++++++++ .../transaction/jta/JtaAutoConfiguration.java | 4 +- ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../batch/BatchAutoConfigurationTests.java | 7 +- ...nsactionManagerAutoConfigurationTests.java | 2 + .../AbstractJpaAutoConfigurationTests.java | 4 +- .../TransactionAutoConfigurationTests.java | 12 ---- ...erCustomizationAutoConfigurationTests.java | 70 +++++++++++++++++++ 12 files changed, 139 insertions(+), 30 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfigurationTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataAutoConfiguration.java index 742577d68ab1..837372f86213 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataAutoConfiguration.java @@ -29,6 +29,7 @@ import org.springframework.boot.autoconfigure.domain.EntityScanner; import org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration; import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; +import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizationAutoConfiguration; import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; @@ -58,7 +59,8 @@ * @author Michael J. Simons * @since 1.4.0 */ -@AutoConfiguration(before = TransactionAutoConfiguration.class, after = Neo4jAutoConfiguration.class) +@AutoConfiguration(before = TransactionAutoConfiguration.class, + after = { Neo4jAutoConfiguration.class, TransactionManagerCustomizationAutoConfiguration.class }) @ConditionalOnClass({ Driver.class, Neo4jTransactionManager.class, PlatformTransactionManager.class }) @EnableConfigurationProperties(Neo4jDataProperties.class) @ConditionalOnBean(Driver.class) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfiguration.java index e144521271d1..55bbd9cfeb3a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfiguration.java @@ -26,6 +26,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; +import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizationAutoConfiguration; import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -46,7 +47,8 @@ * @author Kazuki Shimizu * @since 1.0.0 */ -@AutoConfiguration(before = TransactionAutoConfiguration.class) +@AutoConfiguration(before = TransactionAutoConfiguration.class, + after = TransactionManagerCustomizationAutoConfiguration.class) @ConditionalOnClass({ JdbcTemplate.class, TransactionManager.class }) @AutoConfigureOrder(Ordered.LOWEST_PRECEDENCE) @EnableConfigurationProperties(DataSourceProperties.class) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java index 0dfdd3eb8bab..9eb2c932e526 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java @@ -24,6 +24,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; +import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizationAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Import; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; @@ -37,7 +38,9 @@ * @author Andy Wilkinson * @since 1.0.0 */ -@AutoConfiguration(after = DataSourceAutoConfiguration.class, before = TransactionAutoConfiguration.class) +@AutoConfiguration( + after = { DataSourceAutoConfiguration.class, TransactionManagerCustomizationAutoConfiguration.class }, + before = TransactionAutoConfiguration.class) @ConditionalOnClass({ LocalContainerEntityManagerFactoryBean.class, EntityManager.class, SessionImplementor.class }) @EnableConfigurationProperties(JpaProperties.class) @Import(HibernateJpaConfiguration.class) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionAutoConfiguration.java index 2c605e050a75..1742d8ea0bb0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionAutoConfiguration.java @@ -16,14 +16,12 @@ package org.springframework.boot.autoconfigure.transaction; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; -import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.PlatformTransactionManager; @@ -44,16 +42,8 @@ */ @AutoConfiguration @ConditionalOnClass(PlatformTransactionManager.class) -@EnableConfigurationProperties(TransactionProperties.class) public class TransactionAutoConfiguration { - @Bean - @ConditionalOnMissingBean - public TransactionManagerCustomizers platformTransactionManagerCustomizers( - ObjectProvider> customizers) { - return new TransactionManagerCustomizers(customizers.orderedStream().toList()); - } - @Bean @ConditionalOnMissingBean @ConditionalOnSingleCandidate(ReactiveTransactionManager.class) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfiguration.java new file mode 100644 index 000000000000..692123bae4d1 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfiguration.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2023 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.transaction; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionManager; + +/** + * Auto-configuration for the customization of a {@link TransactionManager}. + * + * @author Andy Wilkinson + * @since 3.2.0 + */ +@ConditionalOnClass(PlatformTransactionManager.class) +@AutoConfiguration(before = TransactionAutoConfiguration.class) +@EnableConfigurationProperties(TransactionProperties.class) +public class TransactionManagerCustomizationAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + TransactionManagerCustomizers platformTransactionManagerCustomizers( + ObjectProvider> customizers) { + return new TransactionManagerCustomizers(customizers.orderedStream().toList()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfiguration.java index a9040ac4ba1f..480e32b81dfa 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfiguration.java @@ -25,6 +25,7 @@ import org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; +import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizationAutoConfiguration; import org.springframework.context.annotation.Import; /** @@ -36,7 +37,8 @@ * @since 1.2.0 */ @AutoConfiguration(before = { XADataSourceAutoConfiguration.class, ActiveMQAutoConfiguration.class, - ArtemisAutoConfiguration.class, HibernateJpaAutoConfiguration.class, TransactionAutoConfiguration.class }) + ArtemisAutoConfiguration.class, HibernateJpaAutoConfiguration.class, TransactionAutoConfiguration.class, + TransactionManagerCustomizationAutoConfiguration.class }) @ConditionalOnClass(jakarta.transaction.Transaction.class) @ConditionalOnProperty(prefix = "spring.jta", value = "enabled", matchIfMissing = true) @Import(JndiJtaConfiguration.class) 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 7f6c606cd0e3..c8fbc3b8d57b 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 @@ -124,6 +124,7 @@ org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration +org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizationAutoConfiguration org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java index 9d508642d239..b57be44564d2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java @@ -61,6 +61,7 @@ import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.test.City; import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; +import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizationAutoConfiguration; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer; import org.springframework.boot.logging.LogLevel; @@ -100,9 +101,9 @@ @ExtendWith(OutputCaptureExtension.class) class BatchAutoConfigurationTests { - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(BatchAutoConfiguration.class, TransactionAutoConfiguration.class, - DataSourceTransactionManagerAutoConfiguration.class)); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( + AutoConfigurations.of(BatchAutoConfiguration.class, TransactionManagerCustomizationAutoConfiguration.class, + TransactionAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class)); @Test void testDefaultContext() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfigurationTests.java index 90044440432a..a289503b0240 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfigurationTests.java @@ -24,6 +24,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; +import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizationAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.jdbc.support.JdbcTransactionManager; @@ -44,6 +45,7 @@ class DataSourceTransactionManagerAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(TransactionAutoConfiguration.class, + TransactionManagerCustomizationAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class)) .withPropertyValues("spring.datasource.url:jdbc:hsqldb:mem:test-" + UUID.randomUUID()); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/AbstractJpaAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/AbstractJpaAutoConfigurationTests.java index 38909c284b3a..869607e7e875 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/AbstractJpaAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/AbstractJpaAutoConfigurationTests.java @@ -39,6 +39,7 @@ import org.springframework.boot.autoconfigure.orm.jpa.test.City; import org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration; import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; +import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizationAutoConfiguration; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -82,7 +83,8 @@ protected AbstractJpaAutoConfigurationTests(Class autoConfiguredClass) { "spring.jta.log-dir=" + new File(new BuildOutput(getClass()).getRootLocation(), "transaction-logs")) .withUserConfiguration(TestConfiguration.class) .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - TransactionAutoConfiguration.class, SqlInitializationAutoConfiguration.class, autoConfiguredClass)); + TransactionAutoConfiguration.class, TransactionManagerCustomizationAutoConfiguration.class, + SqlInitializationAutoConfiguration.class, autoConfiguredClass)); } protected ApplicationContextRunner contextRunner() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionAutoConfigurationTests.java index 502124eb177a..717ba08b303b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionAutoConfigurationTests.java @@ -127,18 +127,6 @@ void whenAUserProvidesATransactionalOperatorTheAutoConfiguredOperatorBacksOff() }); } - @Test - void platformTransactionManagerCustomizers() { - this.contextRunner.withUserConfiguration(SeveralPlatformTransactionManagersConfiguration.class) - .run((context) -> { - TransactionManagerCustomizers customizers = context.getBean(TransactionManagerCustomizers.class); - assertThat(customizers).extracting("customizers") - .asList() - .singleElement() - .isInstanceOf(TransactionProperties.class); - }); - } - @Test void transactionNotManagedWithNoTransactionManager() { this.contextRunner.withUserConfiguration(BaseConfiguration.class) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfigurationTests.java new file mode 100644 index 000000000000..9fe13d8bf07d --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfigurationTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-2023 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.transaction; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TransactionManagerCustomizationAutoConfiguration}. + * + * @author Andy Wilkinson + */ +class TransactionManagerCustomizationAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(TransactionManagerCustomizationAutoConfiguration.class)); + + @Test + void autoConfiguresTransactionManagerCustomizers() { + this.contextRunner.run((context) -> { + TransactionManagerCustomizers customizers = context.getBean(TransactionManagerCustomizers.class); + assertThat(customizers).extracting("customizers") + .asList() + .singleElement() + .isInstanceOf(TransactionProperties.class); + }); + } + + @Test + void autoConfiguredTransactionManagerCustomizersBacksOff() { + this.contextRunner.withUserConfiguration(CustomTransactionManagerCustomizersConfiguration.class) + .run((context) -> { + TransactionManagerCustomizers customizers = context.getBean(TransactionManagerCustomizers.class); + assertThat(customizers).extracting("customizers").asList().isEmpty(); + }); + } + + @Configuration(proxyBeanMethods = false) + static class CustomTransactionManagerCustomizersConfiguration { + + @Bean + TransactionManagerCustomizers customTransactionManagerCustomizers() { + return new TransactionManagerCustomizers(Collections.emptyList()); + } + + } + +} From 1a22415c019f3323cd42bd6d505dd14f0fe0d010 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 29 Sep 2023 07:56:10 +0100 Subject: [PATCH 0511/1215] Expand customization to any type of TransactionManager Closes gh-37628 --- .../neo4j/Neo4jDataAutoConfiguration.java | 2 +- ...ceTransactionManagerAutoConfiguration.java | 3 +- .../orm/jpa/JpaBaseConfiguration.java | 3 +- .../PlatformTransactionManagerCustomizer.java | 12 ++--- ...ManagerCustomizationAutoConfiguration.java | 4 +- .../TransactionManagerCustomizer.java | 38 +++++++++++++ .../TransactionManagerCustomizers.java | 53 +++++++++++++++++-- .../transaction/TransactionProperties.java | 2 +- .../transaction/jta/JndiJtaConfiguration.java | 4 +- .../HibernateJpaAutoConfigurationTests.java | 32 ++++++----- ...erCustomizationAutoConfigurationTests.java | 2 +- .../TransactionManagerCustomizersTests.java | 12 ++--- 12 files changed, 127 insertions(+), 40 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizer.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataAutoConfiguration.java index 837372f86213..38cad56487c8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataAutoConfiguration.java @@ -113,7 +113,7 @@ public Neo4jTemplate neo4jTemplate(Neo4jClient neo4jClient, Neo4jMappingContext public Neo4jTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider, ObjectProvider optionalCustomizers) { Neo4jTransactionManager transactionManager = new Neo4jTransactionManager(driver, databaseNameProvider); - optionalCustomizers.ifAvailable((customizer) -> customizer.customize(transactionManager)); + optionalCustomizers.ifAvailable((customizer) -> customizer.customize((TransactionManager) transactionManager)); return transactionManager; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfiguration.java index 55bbd9cfeb3a..b0741f09d214 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceTransactionManagerAutoConfiguration.java @@ -63,7 +63,8 @@ static class JdbcTransactionManagerConfiguration { DataSourceTransactionManager transactionManager(Environment environment, DataSource dataSource, ObjectProvider transactionManagerCustomizers) { DataSourceTransactionManager transactionManager = createTransactionManager(environment, dataSource); - transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(transactionManager)); + transactionManagerCustomizers + .ifAvailable((customizers) -> customizers.customize((TransactionManager) transactionManager)); return transactionManager; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java index 0520ff094866..90355acc6692 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/orm/jpa/JpaBaseConfiguration.java @@ -93,7 +93,8 @@ protected JpaBaseConfiguration(DataSource dataSource, JpaProperties properties, public PlatformTransactionManager transactionManager( ObjectProvider transactionManagerCustomizers) { JpaTransactionManager transactionManager = new JpaTransactionManager(); - transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(transactionManager)); + transactionManagerCustomizers + .ifAvailable((customizers) -> customizers.customize((TransactionManager) transactionManager)); return transactionManager; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/PlatformTransactionManagerCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/PlatformTransactionManagerCustomizer.java index 64c7fd927bdd..b2e2eb78ae17 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/PlatformTransactionManagerCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/PlatformTransactionManagerCustomizer.java @@ -26,14 +26,12 @@ * @param the transaction manager type * @author Phillip Webb * @since 1.5.0 + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of + * {@link TransactionManagerCustomizer}. */ +@Deprecated(since = "3.2.0", forRemoval = true) @FunctionalInterface -public interface PlatformTransactionManagerCustomizer { - - /** - * Customize the given transaction manager. - * @param transactionManager the transaction manager to customize - */ - void customize(T transactionManager); +public interface PlatformTransactionManagerCustomizer + extends TransactionManagerCustomizer { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfiguration.java index 692123bae4d1..8106fee22427 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfiguration.java @@ -39,8 +39,8 @@ public class TransactionManagerCustomizationAutoConfiguration { @Bean @ConditionalOnMissingBean TransactionManagerCustomizers platformTransactionManagerCustomizers( - ObjectProvider> customizers) { - return new TransactionManagerCustomizers(customizers.orderedStream().toList()); + ObjectProvider> customizers) { + return TransactionManagerCustomizers.of(customizers.orderedStream().toList()); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizer.java new file mode 100644 index 000000000000..e268fe87a49f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2023 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.transaction; + +import org.springframework.transaction.TransactionManager; + +/** + * Callback interface that can be implemented by beans wishing to customize + * {@link TransactionManager TransactionManagers} while retaining default + * auto-configuration. + * + * @param the transaction manager type + * @author Andy Wilkinson + * @since 3.2.0 + */ +public interface TransactionManagerCustomizer { + + /** + * Customize the given transaction manager. + * @param transactionManager the transaction manager to customize + */ + void customize(T transactionManager); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizers.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizers.java index 2bb603c4bc60..88f513a3c9d0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizers.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizers.java @@ -23,26 +23,69 @@ import org.springframework.boot.util.LambdaSafe; import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionManager; /** - * A collection of {@link PlatformTransactionManagerCustomizer}. + * A collection of {@link TransactionManagerCustomizer TransactionManagerCustomizers}. * * @author Phillip Webb + * @author Andy Wilkinson * @since 1.5.0 */ public class TransactionManagerCustomizers { - private final List> customizers; + private final List> customizers; + /** + * Creates a new {@code TransactionManagerCustomizers} instance containing the given + * {@code customizers}. + * @param customizers the customizers + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of {@link #of(Collection)} + */ + @SuppressWarnings("removal") + @Deprecated(since = "3.2.0", forRemoval = true) public TransactionManagerCustomizers(Collection> customizers) { - this.customizers = (customizers != null) ? new ArrayList<>(customizers) : Collections.emptyList(); + this((customizers != null) ? new ArrayList<>(customizers) + : Collections.>emptyList()); } + private TransactionManagerCustomizers(List> customizers) { + this.customizers = customizers; + } + + /** + * Customize the given {@code platformTransactionManager}. + * @param platformTransactionManager the platform transaction manager to customize + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of + * {@link #customize(TransactionManager)} + */ + @Deprecated(since = "3.2.0", forRemoval = true) + public void customize(PlatformTransactionManager platformTransactionManager) { + customize((TransactionManager) platformTransactionManager); + } + + /** + * Customize the given {@code transactionManager}. + * @param transactionManager the transaction manager to customize + * @since 3.2.0 + */ @SuppressWarnings("unchecked") - public void customize(PlatformTransactionManager transactionManager) { - LambdaSafe.callbacks(PlatformTransactionManagerCustomizer.class, this.customizers, transactionManager) + public void customize(TransactionManager transactionManager) { + LambdaSafe.callbacks(TransactionManagerCustomizer.class, this.customizers, transactionManager) .withLogger(TransactionManagerCustomizers.class) .invoke((customizer) -> customizer.customize(transactionManager)); } + /** + * Returns a new {@code TransactionManagerCustomizers} instance containing the given + * {@code customizers}. + * @param customizers the customizers + * @return the new instance + * @since 3.2.0 + */ + public static TransactionManagerCustomizers of(Collection> customizers) { + return new TransactionManagerCustomizers((customizers != null) ? new ArrayList<>(customizers) + : Collections.>emptyList()); + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionProperties.java index d48200a06f39..808f281879cc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionProperties.java @@ -32,7 +32,7 @@ * @since 1.5.0 */ @ConfigurationProperties(prefix = "spring.transaction") -public class TransactionProperties implements PlatformTransactionManagerCustomizer { +public class TransactionProperties implements TransactionManagerCustomizer { /** * Default transaction timeout. If a duration suffix is not specified, seconds will be diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JndiJtaConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JndiJtaConfiguration.java index 91b59dd02af0..075a2c681653 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JndiJtaConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JndiJtaConfiguration.java @@ -23,6 +23,7 @@ import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.TransactionManager; import org.springframework.transaction.jta.JtaTransactionManager; /** @@ -43,7 +44,8 @@ class JndiJtaConfiguration { JtaTransactionManager transactionManager( ObjectProvider transactionManagerCustomizers) { JtaTransactionManager jtaTransactionManager = new JtaTransactionManager(); - transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(jtaTransactionManager)); + transactionManagerCustomizers + .ifAvailable((customizers) -> customizers.customize((TransactionManager) jtaTransactionManager)); return jtaTransactionManager; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java index 100f98b045db..b5c385699f95 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java @@ -40,7 +40,8 @@ import org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy; import org.hibernate.boot.model.naming.ImplicitNamingStrategy; import org.hibernate.boot.model.naming.PhysicalNamingStrategy; -import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.ManagedBeanSettings; +import org.hibernate.cfg.SchemaToolingSettings; import org.hibernate.dialect.H2Dialect; import org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform; import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform; @@ -129,7 +130,8 @@ void testDmlScript() { void testDmlScriptRunsEarly() { contextRunner().withUserConfiguration(TestInitializedJpaConfiguration.class) .withClassLoader(new HideDataScriptClassLoader()) - .withPropertyValues("spring.jpa.show-sql=true", "spring.jpa.hibernate.ddl-auto:create-drop", + .withPropertyValues("spring.jpa.show-sql=true", "spring.jpa.properties.hibernate.format_sql=true", + "spring.jpa.properties.hibernate.highlight_sql=true", "spring.jpa.hibernate.ddl-auto:create-drop", "spring.sql.init.data-locations:/city.sql", "spring.jpa.defer-datasource-initialization=true") .run((context) -> assertThat(context.getBean(TestInitializedJpaConfiguration.class).called).isTrue()); } @@ -386,8 +388,8 @@ void hibernatePropertiesCustomizerCanDisableBeanContainer() { @Test void vendorPropertiesWithEmbeddedDatabaseAndNoDdlProperty() { contextRunner().run(vendorProperties((vendorProperties) -> { - assertThat(vendorProperties).doesNotContainKeys(AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION); - assertThat(vendorProperties).containsEntry(AvailableSettings.HBM2DDL_AUTO, "create-drop"); + assertThat(vendorProperties).doesNotContainKeys(SchemaToolingSettings.JAKARTA_HBM2DDL_DATABASE_ACTION); + assertThat(vendorProperties).containsEntry(SchemaToolingSettings.HBM2DDL_AUTO, "create-drop"); })); } @@ -395,8 +397,8 @@ void vendorPropertiesWithEmbeddedDatabaseAndNoDdlProperty() { void vendorPropertiesWhenDdlAutoPropertyIsSet() { contextRunner().withPropertyValues("spring.jpa.hibernate.ddl-auto=update") .run(vendorProperties((vendorProperties) -> { - assertThat(vendorProperties).doesNotContainKeys(AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION); - assertThat(vendorProperties).containsEntry(AvailableSettings.HBM2DDL_AUTO, "update"); + assertThat(vendorProperties).doesNotContainKeys(SchemaToolingSettings.JAKARTA_HBM2DDL_DATABASE_ACTION); + assertThat(vendorProperties).containsEntry(SchemaToolingSettings.HBM2DDL_AUTO, "update"); })); } @@ -406,8 +408,8 @@ void vendorPropertiesWhenDdlAutoPropertyAndHibernatePropertiesAreSet() { .withPropertyValues("spring.jpa.hibernate.ddl-auto=update", "spring.jpa.properties.hibernate.hbm2ddl.auto=create-drop") .run(vendorProperties((vendorProperties) -> { - assertThat(vendorProperties).doesNotContainKeys(AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION); - assertThat(vendorProperties).containsEntry(AvailableSettings.HBM2DDL_AUTO, "create-drop"); + assertThat(vendorProperties).doesNotContainKeys(SchemaToolingSettings.JAKARTA_HBM2DDL_DATABASE_ACTION); + assertThat(vendorProperties).containsEntry(SchemaToolingSettings.HBM2DDL_AUTO, "create-drop"); })); } @@ -415,7 +417,7 @@ void vendorPropertiesWhenDdlAutoPropertyAndHibernatePropertiesAreSet() { void vendorPropertiesWhenDdlAutoPropertyIsSetToNone() { contextRunner().withPropertyValues("spring.jpa.hibernate.ddl-auto=none") .run(vendorProperties((vendorProperties) -> assertThat(vendorProperties).doesNotContainKeys( - AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION, AvailableSettings.HBM2DDL_AUTO))); + SchemaToolingSettings.JAKARTA_HBM2DDL_DATABASE_ACTION, SchemaToolingSettings.HBM2DDL_AUTO))); } @Test @@ -423,8 +425,9 @@ void vendorPropertiesWhenJpaDdlActionIsSet() { contextRunner() .withPropertyValues("spring.jpa.properties.jakarta.persistence.schema-generation.database.action=create") .run(vendorProperties((vendorProperties) -> { - assertThat(vendorProperties).containsEntry(AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION, "create"); - assertThat(vendorProperties).doesNotContainKeys(AvailableSettings.HBM2DDL_AUTO); + assertThat(vendorProperties).containsEntry(SchemaToolingSettings.JAKARTA_HBM2DDL_DATABASE_ACTION, + "create"); + assertThat(vendorProperties).doesNotContainKeys(SchemaToolingSettings.HBM2DDL_AUTO); })); } @@ -434,8 +437,9 @@ void vendorPropertiesWhenBothDdlAutoPropertiesAreSet() { .withPropertyValues("spring.jpa.properties.jakarta.persistence.schema-generation.database.action=create", "spring.jpa.hibernate.ddl-auto=create-only") .run(vendorProperties((vendorProperties) -> { - assertThat(vendorProperties).containsEntry(AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION, "create"); - assertThat(vendorProperties).containsEntry(AvailableSettings.HBM2DDL_AUTO, "create-only"); + assertThat(vendorProperties).containsEntry(SchemaToolingSettings.JAKARTA_HBM2DDL_DATABASE_ACTION, + "create"); + assertThat(vendorProperties).containsEntry(SchemaToolingSettings.HBM2DDL_AUTO, "create-only"); })); } @@ -570,7 +574,7 @@ static class DisableBeanContainerConfiguration { @Bean HibernatePropertiesCustomizer disableBeanContainerHibernatePropertiesCustomizer() { - return (hibernateProperties) -> hibernateProperties.remove(AvailableSettings.BEAN_CONTAINER); + return (hibernateProperties) -> hibernateProperties.remove(ManagedBeanSettings.BEAN_CONTAINER); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfigurationTests.java index 9fe13d8bf07d..7641ddcdf7dc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfigurationTests.java @@ -62,7 +62,7 @@ static class CustomTransactionManagerCustomizersConfiguration { @Bean TransactionManagerCustomizers customTransactionManagerCustomizers() { - return new TransactionManagerCustomizers(Collections.emptyList()); + return TransactionManagerCustomizers.of(Collections.>emptyList()); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizersTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizersTests.java index 9f5827813760..396b00987650 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizersTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizersTests.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test; import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionManager; import org.springframework.transaction.jta.JtaTransactionManager; import static org.assertj.core.api.Assertions.assertThat; @@ -36,7 +37,7 @@ class TransactionManagerCustomizersTests { @Test void customizeWithNullCustomizersShouldDoNothing() { - new TransactionManagerCustomizers(null).customize(mock(PlatformTransactionManager.class)); + TransactionManagerCustomizers.of(null).customize(mock(TransactionManager.class)); } @Test @@ -44,15 +45,14 @@ void customizeShouldCheckGeneric() { List> list = new ArrayList<>(); list.add(new TestCustomizer<>()); list.add(new TestJtaCustomizer()); - TransactionManagerCustomizers customizers = new TransactionManagerCustomizers(list); - customizers.customize(mock(PlatformTransactionManager.class)); - customizers.customize(mock(JtaTransactionManager.class)); + TransactionManagerCustomizers customizers = TransactionManagerCustomizers.of(list); + customizers.customize((TransactionManager) mock(PlatformTransactionManager.class)); + customizers.customize((TransactionManager) mock(JtaTransactionManager.class)); assertThat(list.get(0).getCount()).isEqualTo(2); assertThat(list.get(1).getCount()).isOne(); } - static class TestCustomizer - implements PlatformTransactionManagerCustomizer { + static class TestCustomizer implements TransactionManagerCustomizer { private int count; From 6d3594db49000953570052c4fcfee23b8161d1da Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 29 Sep 2023 09:29:02 +0100 Subject: [PATCH 0512/1215] Add execution listeners to auto-configured transaction managers Closes gh-36770 --- ...ListenersTransactionManagerCustomizer.java | 45 ++++++++++++++++++ ...ManagerCustomizationAutoConfiguration.java | 7 +++ ...nersTransactionManagerCustomizerTests.java | 46 +++++++++++++++++++ ...erCustomizationAutoConfigurationTests.java | 5 +- 4 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/ExecutionListenersTransactionManagerCustomizer.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/ExecutionListenersTransactionManagerCustomizerTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/ExecutionListenersTransactionManagerCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/ExecutionListenersTransactionManagerCustomizer.java new file mode 100644 index 000000000000..18a072b0f837 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/ExecutionListenersTransactionManagerCustomizer.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2023 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.transaction; + +import java.util.List; + +import org.springframework.transaction.ConfigurableTransactionManager; +import org.springframework.transaction.TransactionExecutionListener; + +/** + * {@link TransactionManagerCustomizer} that adds {@link TransactionExecutionListener + * execution listeners} to any transaction manager that is + * {@link ConfigurableTransactionManager configurable}. + * + * @author Andy Wilkinson + */ +class ExecutionListenersTransactionManagerCustomizer + implements TransactionManagerCustomizer { + + private final List listeners; + + ExecutionListenersTransactionManagerCustomizer(List listeners) { + this.listeners = listeners; + } + + @Override + public void customize(ConfigurableTransactionManager transactionManager) { + this.listeners.forEach(transactionManager::addListener); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfiguration.java index 8106fee22427..aba33e3226c3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfiguration.java @@ -23,6 +23,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionExecutionListener; import org.springframework.transaction.TransactionManager; /** @@ -43,4 +44,10 @@ TransactionManagerCustomizers platformTransactionManagerCustomizers( return TransactionManagerCustomizers.of(customizers.orderedStream().toList()); } + @Bean + ExecutionListenersTransactionManagerCustomizer transactionExecutionListeners( + ObjectProvider listeners) { + return new ExecutionListenersTransactionManagerCustomizer(listeners.orderedStream().toList()); + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/ExecutionListenersTransactionManagerCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/ExecutionListenersTransactionManagerCustomizerTests.java new file mode 100644 index 000000000000..3722680f6d7c --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/ExecutionListenersTransactionManagerCustomizerTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2023 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.transaction; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.transaction.ConfigurableTransactionManager; +import org.springframework.transaction.TransactionExecutionListener; + +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ExecutionListenersTransactionManagerCustomizer}. + * + * @author Andy Wilkinson + */ +class ExecutionListenersTransactionManagerCustomizerTests { + + @Test + void whenTransactionManagerIsCustomizedThenExecutionListenersAreAddedToIt() { + TransactionExecutionListener listener1 = mock(TransactionExecutionListener.class); + TransactionExecutionListener listener2 = mock(TransactionExecutionListener.class); + ConfigurableTransactionManager transactionManager = mock(ConfigurableTransactionManager.class); + new ExecutionListenersTransactionManagerCustomizer(List.of(listener1, listener2)).customize(transactionManager); + then(transactionManager).should().addListener(listener1); + then(transactionManager).should().addListener(listener2); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfigurationTests.java index 7641ddcdf7dc..24bf90e0b27b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfigurationTests.java @@ -43,8 +43,9 @@ void autoConfiguresTransactionManagerCustomizers() { TransactionManagerCustomizers customizers = context.getBean(TransactionManagerCustomizers.class); assertThat(customizers).extracting("customizers") .asList() - .singleElement() - .isInstanceOf(TransactionProperties.class); + .hasSize(2) + .hasAtLeastOneElementOfType(TransactionProperties.class) + .hasAtLeastOneElementOfType(ExecutionListenersTransactionManagerCustomizer.class); }); } From 3e9a1cc1f8e577f66d00bf6b6a3fb55dd6376ed3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 29 Sep 2023 11:50:05 +0100 Subject: [PATCH 0513/1215] Add Awaitility to spring-boot-starter-test Closes gh-37195 --- .../spring-boot-docs/src/docs/asciidoc/features/testing.adoc | 1 + .../spring-boot-starters/spring-boot-starter-test/build.gradle | 1 + 2 files changed, 2 insertions(+) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc index d440e4cc5aa7..4c473a929e40 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc @@ -41,6 +41,7 @@ The `spring-boot-starter-test` "`Starter`" (in the `test` `scope`) contains the * https://site.mockito.org/[Mockito]: A Java mocking framework. * https://github.com/skyscreamer/JSONassert[JSONassert]: An assertion library for JSON. * https://github.com/jayway/JsonPath[JsonPath]: XPath for JSON. +* https://https://github.com/awaitility/awaitility[Awaitility]: A library for testing asynchronous systems. We generally find these common libraries to be useful when writing tests. If these libraries do not suit your needs, you can add additional test dependencies of your own. diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-test/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-test/build.gradle index f5a2cc091382..e98d71281766 100644 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-test/build.gradle +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-test/build.gradle @@ -12,6 +12,7 @@ dependencies { api("jakarta.xml.bind:jakarta.xml.bind-api") api("net.minidev:json-smart") api("org.assertj:assertj-core") + api("org.awaitility:awaitility") api("org.hamcrest:hamcrest") api("org.junit.jupiter:junit-jupiter") api("org.mockito:mockito-core") From ff99de49c4360b6c63e12494adf67c87fa7b601d Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Fri, 29 Sep 2023 14:12:57 -0500 Subject: [PATCH 0514/1215] Configure a RestClient.Builder with RestClientTest This commit adds support for configuring a `RestClient.Builder` and `MockRestServiceServer` support for the `RestClient` when using `@RestClientTest` sliced tests. Closes gh-37033 --- .../src/docs/asciidoc/features/testing.adoc | 10 +- .../MyRestClientServiceTests.java | 47 +++++ ...s.java => MyRestTemplateServiceTests.java} | 4 +- .../MyRestClientServiceTests.kt | 41 +++++ ...Tests.kt => MyRestTemplateServiceTests.kt} | 4 +- .../AutoConfigureMockRestServiceServer.java | 17 +- ...ockRestServiceServerAutoConfiguration.java | 63 +++++-- .../web/client/RestClientTest.java | 5 +- ....web.client.AutoConfigureWebClient.imports | 1 + .../AnotherExampleRestClientService.java | 48 ++++++ ...=> AnotherExampleRestTemplateService.java} | 6 +- ...iceServerEnabledFalseIntegrationTests.java | 3 + ...eServerWithRestClientIntegrationTests.java | 72 ++++++++ ...hRestTemplateRootUriIntegrationTests.java} | 4 +- .../web/client/ExampleRestClientService.java | 53 ++++++ ...t.java => ExampleRestTemplateService.java} | 9 +- ...ClientTestNoComponentIntegrationTests.java | 4 +- ...tClientTestRestClientIntegrationTests.java | 69 ++++++++ ...stClientTwoComponentsIntegrationTests.java | 79 +++++++++ ...ientTestRestTemplateIntegrationTests.java} | 6 +- ...emplateTwoComponentsIntegrationTests.java} | 10 +- ...thRestClientComponentIntegrationTests.java | 51 ++++++ ...estTemplateComponentIntegrationTests.java} | 11 +- ...entTestWithoutJacksonIntegrationTests.java | 6 +- .../MockServerRestClientCustomizer.java | 139 +++++++++++++++ .../MockServerRestClientCustomizerTests.java | 163 ++++++++++++++++++ 26 files changed, 868 insertions(+), 57 deletions(-) create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestClientServiceTests.java rename spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/{MyRestClientTests.java => MyRestTemplateServiceTests.java} (94%) create mode 100644 spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestClientServiceTests.kt rename spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/{MyRestClientTests.kt => MyRestTemplateServiceTests.kt} (94%) create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AnotherExampleRestClientService.java rename spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/{AnotherExampleRestClient.java => AnotherExampleRestTemplateService.java} (87%) create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureMockRestServiceServerWithRestClientIntegrationTests.java rename spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/{AutoConfigureMockRestServiceServerWithRootUriIntegrationTests.java => AutoConfigureMockRestServiceServerWithRestTemplateRootUriIntegrationTests.java} (95%) create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/ExampleRestClientService.java rename spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/{ExampleRestClient.java => ExampleRestTemplateService.java} (82%) create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestRestClientIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestRestClientTwoComponentsIntegrationTests.java rename spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/{RestClientRestIntegrationTests.java => RestClientTestRestTemplateIntegrationTests.java} (93%) rename spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/{RestClientTestTwoComponentsIntegrationTests.java => RestClientTestRestTemplateTwoComponentsIntegrationTests.java} (86%) create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestWithRestClientComponentIntegrationTests.java rename spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/{RestClientTestWithComponentIntegrationTests.java => RestClientTestWithRestTemplateComponentIntegrationTests.java} (85%) create mode 100644 spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/MockServerRestClientCustomizer.java create mode 100644 spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/MockServerRestClientCustomizerTests.java diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc index 4c473a929e40..d08ffbb58df3 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc @@ -735,17 +735,21 @@ include::code:server/MyDataLdapTests[] [[features.testing.spring-boot-applications.autoconfigured-rest-client]] ==== Auto-configured REST Clients You can use the `@RestClientTest` annotation to test REST clients. -By default, it auto-configures Jackson, GSON, and Jsonb support, configures a `RestTemplateBuilder`, and adds support for `MockRestServiceServer`. +By default, it auto-configures Jackson, GSON, and Jsonb support, configures a `RestTemplateBuilder` and a `RestClient.Builder`, and adds support for `MockRestServiceServer`. Regular `@Component` and `@ConfigurationProperties` beans are not scanned when the `@RestClientTest` annotation is used. `@EnableConfigurationProperties` can be used to include `@ConfigurationProperties` beans. TIP: A list of the auto-configuration settings that are enabled by `@RestClientTest` can be <>. -The specific beans that you want to test should be specified by using the `value` or `components` attribute of `@RestClientTest`, as shown in the following example: +The specific beans that you want to test should be specified by using the `value` or `components` attribute of `@RestClientTest`. -include::code:MyRestClientTests[] +When using a `RestTemplateBuilder` in the beans under test and `RestTemplateBuilder.rootUri(String rootUri)` has been called when building the `RestTemplate`, then the root URI should be omitted from the `MockRestServiceServer` expectations as shown in the following example: +include::code:MyRestTemplateServiceTests[] +When using a `RestClient.Builder` in the beans under test, or when using a `RestTemplateBuilder` without calling `rootUri(String rootURI)`, the full URI must be used in the `MockRestServiceServer` expectations as shown in the following example: + +include::code:MyRestClientServiceTests[] [[features.testing.spring-boot-applications.autoconfigured-spring-restdocs]] ==== Auto-configured Spring REST Docs Tests diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestClientServiceTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestClientServiceTests.java new file mode 100644 index 000000000000..8d8437fc8f5b --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestClientServiceTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2023 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.docs.features.testing.springbootapplications.autoconfiguredrestclient; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.client.MockRestServiceServer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +@RestClientTest(RemoteVehicleDetailsService.class) +class MyRestClientServiceTests { + + @Autowired + private RemoteVehicleDetailsService service; + + @Autowired + private MockRestServiceServer server; + + @Test + void getVehicleDetailsWhenResultIsSuccessShouldReturnDetails() { + this.server.expect(requestTo("https://example.com/greet/details")) + .andRespond(withSuccess("hello", MediaType.TEXT_PLAIN)); + String greeting = this.service.callRestService(); + assertThat(greeting).isEqualTo("hello"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestClientTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestTemplateServiceTests.java similarity index 94% rename from spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestClientTests.java rename to spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestTemplateServiceTests.java index 90a1a82c2e4e..fdf67b9621b3 100644 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestClientTests.java +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestTemplateServiceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -28,7 +28,7 @@ import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; @RestClientTest(RemoteVehicleDetailsService.class) -class MyRestClientTests { +class MyRestTemplateServiceTests { @Autowired private RemoteVehicleDetailsService service; diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestClientServiceTests.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestClientServiceTests.kt new file mode 100644 index 000000000000..d2264757606c --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestClientServiceTests.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2023 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.docs.features.testing.springbootapplications.autoconfiguredrestclient + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.client.RestClientTest +import org.springframework.http.MediaType +import org.springframework.test.web.client.MockRestServiceServer +import org.springframework.test.web.client.match.MockRestRequestMatchers +import org.springframework.test.web.client.response.MockRestResponseCreators + +@RestClientTest(RemoteVehicleDetailsService::class) +class MyRestClientServiceTests( + @Autowired val service: RemoteVehicleDetailsService, + @Autowired val server: MockRestServiceServer) { + + @Test + fun getVehicleDetailsWhenResultIsSuccessShouldReturnDetails(): Unit { + server.expect(MockRestRequestMatchers.requestTo("https://example.com/greet/details")) + .andRespond(MockRestResponseCreators.withSuccess("hello", MediaType.TEXT_PLAIN)) + val greeting = service.callRestService() + assertThat(greeting).isEqualTo("hello") + } + +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestClientTests.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestTemplateServiceTests.kt similarity index 94% rename from spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestClientTests.kt rename to spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestTemplateServiceTests.kt index 3345aa561703..03f1a78cc990 100644 --- a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestClientTests.kt +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredrestclient/MyRestTemplateServiceTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -26,7 +26,7 @@ import org.springframework.test.web.client.match.MockRestRequestMatchers import org.springframework.test.web.client.response.MockRestResponseCreators @RestClientTest(RemoteVehicleDetailsService::class) -class MyRestClientTests( +class MyRestTemplateServiceTests( @Autowired val service: RemoteVehicleDetailsService, @Autowired val server: MockRestServiceServer) { diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureMockRestServiceServer.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureMockRestServiceServer.java index 6e9e8def9672..43e1aac70e07 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureMockRestServiceServer.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureMockRestServiceServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. @@ -25,20 +25,26 @@ import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.test.autoconfigure.properties.PropertyMapping; +import org.springframework.boot.test.web.client.MockServerRestClientCustomizer; import org.springframework.boot.test.web.client.MockServerRestTemplateCustomizer; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestClient; /** * Annotation that can be applied to a test class to enable and configure * auto-configuration of a single {@link MockRestServiceServer}. Only useful when a single - * call is made to {@link RestTemplateBuilder}. If multiple - * {@link org.springframework.web.client.RestTemplate RestTemplates} are in use, inject + * call is made to {@link RestTemplateBuilder} or {@link RestClient.Builder}. If multiple + * {@link org.springframework.web.client.RestTemplate RestTemplates} or + * {@link org.springframework.web.client.RestClient RestClients} are in use, inject a * {@link MockServerRestTemplateCustomizer} and use * {@link MockServerRestTemplateCustomizer#getServer(org.springframework.web.client.RestTemplate) - * getServer(RestTemplate)} or bind a {@link MockRestServiceServer} directly. + * getServer(RestTemplate)}, or inject a {@link MockServerRestClientCustomizer} and use + * {@link MockServerRestClientCustomizer#getServer(org.springframework.web.client.RestClient.Builder) + * * getServer(RestClient.Builder)}, or bind a {@link MockRestServiceServer} directly. * * @author Phillip Webb + * @author Scott Frederick * @since 1.4.0 * @see MockServerRestTemplateCustomizer */ @@ -51,7 +57,8 @@ public @interface AutoConfigureMockRestServiceServer { /** - * If {@link MockServerRestTemplateCustomizer} should be enabled and + * If {@link MockServerRestTemplateCustomizer} and + * {@link MockServerRestClientCustomizer} should be enabled and * {@link MockRestServiceServer} beans should be registered. Defaults to {@code true} * @return if mock support is enabled */ diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/MockRestServiceServerAutoConfiguration.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/MockRestServiceServerAutoConfiguration.java index aa76319d00a6..6aba6245a020 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/MockRestServiceServerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/MockRestServiceServerAutoConfiguration.java @@ -19,10 +19,12 @@ import java.io.IOException; import java.lang.reflect.Constructor; import java.time.Duration; +import java.util.Collection; import java.util.Map; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.test.web.client.MockServerRestClientCustomizer; import org.springframework.boot.test.web.client.MockServerRestTemplateCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.http.client.ClientHttpRequest; @@ -33,12 +35,14 @@ import org.springframework.test.web.client.RequestMatcher; import org.springframework.test.web.client.ResponseActions; import org.springframework.util.Assert; +import org.springframework.web.client.RestClient; import org.springframework.web.client.RestTemplate; /** * Auto-configuration for {@link MockRestServiceServer} support. * * @author Phillip Webb + * @author Scott Frederick * @since 1.4.0 * @see AutoConfigureMockRestServiceServer */ @@ -52,21 +56,29 @@ public MockServerRestTemplateCustomizer mockServerRestTemplateCustomizer() { } @Bean - public MockRestServiceServer mockRestServiceServer(MockServerRestTemplateCustomizer customizer) { + public MockServerRestClientCustomizer mockServerRestClientCustomizer() { + return new MockServerRestClientCustomizer(); + } + + @Bean + public MockRestServiceServer mockRestServiceServer(MockServerRestTemplateCustomizer restTemplateCustomizer, + MockServerRestClientCustomizer restClientCustomizer) { try { - return createDeferredMockRestServiceServer(customizer); + return createDeferredMockRestServiceServer(restTemplateCustomizer, restClientCustomizer); } catch (Exception ex) { throw new IllegalStateException(ex); } } - private MockRestServiceServer createDeferredMockRestServiceServer(MockServerRestTemplateCustomizer customizer) - throws Exception { + private MockRestServiceServer createDeferredMockRestServiceServer( + MockServerRestTemplateCustomizer restTemplateCustomizer, + MockServerRestClientCustomizer restClientCustomizer) throws Exception { Constructor constructor = MockRestServiceServer.class .getDeclaredConstructor(RequestExpectationManager.class); constructor.setAccessible(true); - return constructor.newInstance(new DeferredRequestExpectationManager(customizer)); + return constructor + .newInstance(new DeferredRequestExpectationManager(restTemplateCustomizer, restClientCustomizer)); } /** @@ -77,10 +89,14 @@ private MockRestServiceServer createDeferredMockRestServiceServer(MockServerRest */ private static class DeferredRequestExpectationManager implements RequestExpectationManager { - private final MockServerRestTemplateCustomizer customizer; + private final MockServerRestTemplateCustomizer restTemplateCustomizer; + + private final MockServerRestClientCustomizer restClientCustomizer; - DeferredRequestExpectationManager(MockServerRestTemplateCustomizer customizer) { - this.customizer = customizer; + DeferredRequestExpectationManager(MockServerRestTemplateCustomizer restTemplateCustomizer, + MockServerRestClientCustomizer restClientCustomizer) { + this.restTemplateCustomizer = restTemplateCustomizer; + this.restClientCustomizer = restClientCustomizer; } @Override @@ -105,19 +121,34 @@ public void verify(Duration timeout) { @Override public void reset() { - Map expectationManagers = this.customizer.getExpectationManagers(); + resetExpectations(this.restTemplateCustomizer.getExpectationManagers().values()); + resetExpectations(this.restClientCustomizer.getExpectationManagers().values()); + } + + private void resetExpectations(Collection expectationManagers) { if (expectationManagers.size() == 1) { - getDelegate().reset(); + expectationManagers.iterator().next().reset(); } } private RequestExpectationManager getDelegate() { - Map expectationManagers = this.customizer.getExpectationManagers(); - Assert.state(!expectationManagers.isEmpty(), "Unable to use auto-configured MockRestServiceServer since " - + "MockServerRestTemplateCustomizer has not been bound to a RestTemplate"); - Assert.state(expectationManagers.size() == 1, "Unable to use auto-configured MockRestServiceServer since " - + "MockServerRestTemplateCustomizer has been bound to more than one RestTemplate"); - return expectationManagers.values().iterator().next(); + Map restTemplateExpectationManagers = this.restTemplateCustomizer + .getExpectationManagers(); + Map restClientExpectationManagers = this.restClientCustomizer + .getExpectationManagers(); + Assert.state(!(restTemplateExpectationManagers.isEmpty() && restClientExpectationManagers.isEmpty()), + "Unable to use auto-configured MockRestServiceServer since " + + "a mock server customizer has not been bound to a RestTemplate or RestClient"); + if (!restTemplateExpectationManagers.isEmpty()) { + Assert.state(restTemplateExpectationManagers.size() == 1, + "Unable to use auto-configured MockRestServiceServer since " + + "MockServerRestTemplateCustomizer has been bound to more than one RestTemplate"); + return restTemplateExpectationManagers.values().iterator().next(); + } + Assert.state(restClientExpectationManagers.size() == 1, + "Unable to use auto-configured MockRestServiceServer since " + + "MockServerRestClientCustomizer has been bound to more than one RestClient"); + return restClientExpectationManagers.values().iterator().next(); } } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTest.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTest.java index eba5ed69d003..8a93b1b42226 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTest.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. @@ -38,11 +38,12 @@ import org.springframework.test.context.BootstrapWith; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestClient; import org.springframework.web.client.RestTemplate; /** * Annotation for a Spring rest client test that focuses only on beans - * that use {@link RestTemplateBuilder}. + * that use {@link RestTemplateBuilder} or {@link RestClient.Builder}. *

    * Using this annotation will disable full auto-configuration and instead apply only * configuration relevant to rest client tests (i.e. Jackson or GSON auto-configuration diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient.imports b/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient.imports index c789b0b5c278..cad2d5fb96fe 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient.imports +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient.imports @@ -6,4 +6,5 @@ org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration +org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration \ No newline at end of file diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AnotherExampleRestClientService.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AnotherExampleRestClientService.java new file mode 100644 index 000000000000..29d459100f41 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AnotherExampleRestClientService.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2023 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.test.autoconfigure.web.client; + +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.Builder; + +/** + * A second example web client used with {@link RestClientTest @RestClientTest} tests. + * + * @author Scott Frederick + */ +@Service +public class AnotherExampleRestClientService { + + private final Builder builder; + + private final RestClient restClient; + + public AnotherExampleRestClientService(RestClient.Builder builder) { + this.builder = builder; + this.restClient = builder.baseUrl("https://example.com").build(); + } + + protected Builder getRestClientBuilder() { + return this.builder; + } + + public String test() { + return this.restClient.get().uri("/test").retrieve().toEntity(String.class).getBody(); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AnotherExampleRestClient.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AnotherExampleRestTemplateService.java similarity index 87% rename from spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AnotherExampleRestClient.java rename to spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AnotherExampleRestTemplateService.java index e7452ca904e4..a781d7cc80cc 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AnotherExampleRestClient.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AnotherExampleRestTemplateService.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. @@ -26,11 +26,11 @@ * @author Phillip Webb */ @Service -public class AnotherExampleRestClient { +public class AnotherExampleRestTemplateService { private final RestTemplate restTemplate; - public AnotherExampleRestClient(RestTemplateBuilder builder) { + public AnotherExampleRestTemplateService(RestTemplateBuilder builder) { this.restTemplate = builder.rootUri("https://example.com").build(); } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureMockRestServiceServerEnabledFalseIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureMockRestServiceServerEnabledFalseIntegrationTests.java index 68c31ee5bd0e..fc55c4ad9974 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureMockRestServiceServerEnabledFalseIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureMockRestServiceServerEnabledFalseIntegrationTests.java @@ -20,6 +20,7 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.web.client.MockServerRestClientCustomizer; import org.springframework.boot.test.web.client.MockServerRestTemplateCustomizer; import org.springframework.context.ApplicationContext; @@ -43,6 +44,8 @@ class AutoConfigureMockRestServiceServerEnabledFalseIntegrationTests { void mockServerRestTemplateCustomizerShouldNotBeRegistered() { assertThatExceptionOfType(NoSuchBeanDefinitionException.class) .isThrownBy(() -> this.applicationContext.getBean(MockServerRestTemplateCustomizer.class)); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> this.applicationContext.getBean(MockServerRestClientCustomizer.class)); } } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureMockRestServiceServerWithRestClientIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureMockRestServiceServerWithRestClientIntegrationTests.java new file mode 100644 index 000000000000..7e7b5adcbd46 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureMockRestServiceServerWithRestClientIntegrationTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2023 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.test.autoconfigure.web.client; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.Builder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * Tests for + * {@link AutoConfigureMockRestServiceServer @AutoConfigureMockRestServiceServer} with a + * {@link RestClient} configured with a base URL. + * + * @author Scott Frederick + */ +@SpringBootTest +@AutoConfigureMockRestServiceServer +class AutoConfigureMockRestServiceServerWithRestClientIntegrationTests { + + @Autowired + private RestClient restClient; + + @Autowired + private MockRestServiceServer server; + + @Test + void mockServerExpectationsAreMatched() { + this.server.expect(requestTo("/rest/test")).andRespond(withSuccess("hello", MediaType.TEXT_HTML)); + ResponseEntity entity = this.restClient.get().uri("/test").retrieve().toEntity(String.class); + assertThat(entity.getBody()).isEqualTo("hello"); + } + + @EnableAutoConfiguration(exclude = CassandraAutoConfiguration.class) + @Configuration(proxyBeanMethods = false) + static class RootUriConfiguration { + + @Bean + RestClient restClient(Builder restClientBuilder) { + return restClientBuilder.baseUrl("/rest").build(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureMockRestServiceServerWithRootUriIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureMockRestServiceServerWithRestTemplateRootUriIntegrationTests.java similarity index 95% rename from spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureMockRestServiceServerWithRootUriIntegrationTests.java rename to spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureMockRestServiceServerWithRestTemplateRootUriIntegrationTests.java index 2ac9b41f4144..fabdbf9602ad 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureMockRestServiceServerWithRootUriIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/AutoConfigureMockRestServiceServerWithRestTemplateRootUriIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -44,7 +44,7 @@ */ @SpringBootTest @AutoConfigureMockRestServiceServer -class AutoConfigureMockRestServiceServerWithRootUriIntegrationTests { +class AutoConfigureMockRestServiceServerWithRestTemplateRootUriIntegrationTests { @Autowired private RestTemplate restTemplate; diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/ExampleRestClientService.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/ExampleRestClientService.java new file mode 100644 index 000000000000..f4e4c922a860 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/ExampleRestClientService.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2023 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.test.autoconfigure.web.client; + +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.Builder; + +/** + * Example web client using {@code RestClient} with {@link RestClientTest @RestClientTest} + * tests. + * + * @author Scott Frederick + */ +@Service +public class ExampleRestClientService { + + private final Builder builder; + + private final RestClient restClient; + + public ExampleRestClientService(RestClient.Builder builder) { + this.builder = builder; + this.restClient = builder.baseUrl("https://example.com").build(); + } + + protected Builder getRestClientBuilder() { + return this.builder; + } + + public String test() { + return this.restClient.get().uri("/test").retrieve().toEntity(String.class).getBody(); + } + + public void testPostWithBody(String body) { + this.restClient.post().uri("/test").body(body).retrieve().toBodilessEntity(); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/ExampleRestClient.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/ExampleRestTemplateService.java similarity index 82% rename from spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/ExampleRestClient.java rename to spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/ExampleRestTemplateService.java index 06b923f7acbf..95f55211753c 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/ExampleRestClient.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/ExampleRestTemplateService.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -21,16 +21,17 @@ import org.springframework.web.client.RestTemplate; /** - * Example web client used with {@link RestClientTest @RestClientTest} tests. + * Example web client using {@code RestTemplate} with + * {@link RestClientTest @RestClientTest} tests. * * @author Phillip Webb */ @Service -public class ExampleRestClient { +public class ExampleRestTemplateService { private final RestTemplate restTemplate; - public ExampleRestClient(RestTemplateBuilder builder) { + public ExampleRestTemplateService(RestTemplateBuilder builder) { this.restTemplate = builder.rootUri("https://example.com").build(); } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestNoComponentIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestNoComponentIntegrationTests.java index 6ed3e63e46d5..3da4654a6b8d 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestNoComponentIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestNoComponentIntegrationTests.java @@ -50,7 +50,7 @@ class RestClientTestNoComponentIntegrationTests { @Test void exampleRestClientIsNotInjected() { assertThatExceptionOfType(NoSuchBeanDefinitionException.class) - .isThrownBy(() -> this.applicationContext.getBean(ExampleRestClient.class)); + .isThrownBy(() -> this.applicationContext.getBean(ExampleRestTemplateService.class)); } @Test @@ -61,7 +61,7 @@ void examplePropertiesIsNotInjected() { @Test void manuallyCreateBean() { - ExampleRestClient client = new ExampleRestClient(this.restTemplateBuilder); + ExampleRestTemplateService client = new ExampleRestTemplateService(this.restTemplateBuilder); this.server.expect(requestTo("/test")).andRespond(withSuccess("hello", MediaType.TEXT_HTML)); assertThat(client.test()).isEqualTo("hello"); } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestRestClientIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestRestClientIntegrationTests.java new file mode 100644 index 000000000000..098f6456a9b5 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestRestClientIntegrationTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2023 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.test.autoconfigure.web.client; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.content; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * Tests for {@link RestClientTest @RestClientTest} with a {@link RestClient}. + * + * @author Scott Frederick + */ +@RestClientTest(ExampleRestClientService.class) +class RestClientTestRestClientIntegrationTests { + + @Autowired + private MockRestServiceServer server; + + @Autowired + private ExampleRestClientService client; + + @Test + void mockServerCall1() { + this.server.expect(requestTo(uri("/test"))).andRespond(withSuccess("1", MediaType.TEXT_HTML)); + assertThat(this.client.test()).isEqualTo("1"); + } + + @Test + void mockServerCall2() { + this.server.expect(requestTo(uri("/test"))).andRespond(withSuccess("2", MediaType.TEXT_HTML)); + assertThat(this.client.test()).isEqualTo("2"); + } + + @Test + void mockServerCallWithContent() { + this.server.expect(requestTo(uri("/test"))) + .andExpect(content().string("test")) + .andRespond(withSuccess("1", MediaType.TEXT_HTML)); + this.client.testPostWithBody("test"); + } + + private static String uri(String path) { + return "https://example.com" + path; + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestRestClientTwoComponentsIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestRestClientTwoComponentsIntegrationTests.java new file mode 100644 index 000000000000..15695607fe99 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestRestClientTwoComponentsIntegrationTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2023 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.test.autoconfigure.web.client; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.web.client.MockServerRestClientCustomizer; +import org.springframework.http.MediaType; +import org.springframework.test.web.client.MockRestServiceServer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * Tests for {@link RestClientTest @RestClientTest} with two {@code RestClient} clients. + * + * @author Phillip Webb + * @author Scott Frederick + */ +@RestClientTest({ ExampleRestClientService.class, AnotherExampleRestClientService.class }) +class RestClientTestRestClientTwoComponentsIntegrationTests { + + @Autowired + private ExampleRestClientService client1; + + @Autowired + private AnotherExampleRestClientService client2; + + @Autowired + private MockServerRestClientCustomizer customizer; + + @Autowired + private MockRestServiceServer server; + + @Test + void serverShouldNotWork() { + assertThatIllegalStateException().isThrownBy( + () -> this.server.expect(requestTo(uri("/test"))).andRespond(withSuccess("hello", MediaType.TEXT_HTML))) + .withMessageContaining("Unable to use auto-configured"); + } + + @Test + void client1RestCallViaCustomizer() { + this.customizer.getServer(this.client1.getRestClientBuilder()) + .expect(requestTo(uri("/test"))) + .andRespond(withSuccess("hello", MediaType.TEXT_HTML)); + assertThat(this.client1.test()).isEqualTo("hello"); + } + + @Test + void client2RestCallViaCustomizer() { + this.customizer.getServer(this.client2.getRestClientBuilder()) + .expect(requestTo(uri("/test"))) + .andRespond(withSuccess("there", MediaType.TEXT_HTML)); + assertThat(this.client2.test()).isEqualTo("there"); + } + + private static String uri(String path) { + return "https://example.com" + path; + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientRestIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestRestTemplateIntegrationTests.java similarity index 93% rename from spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientRestIntegrationTests.java rename to spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestRestTemplateIntegrationTests.java index 3966b197e28c..7f92f7550070 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientRestIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestRestTemplateIntegrationTests.java @@ -32,14 +32,14 @@ * * @author Phillip Webb */ -@RestClientTest(ExampleRestClient.class) -class RestClientRestIntegrationTests { +@RestClientTest(ExampleRestTemplateService.class) +class RestClientTestRestTemplateIntegrationTests { @Autowired private MockRestServiceServer server; @Autowired - private ExampleRestClient client; + private ExampleRestTemplateService client; @Test void mockServerCall1() { diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestTwoComponentsIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestRestTemplateTwoComponentsIntegrationTests.java similarity index 86% rename from spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestTwoComponentsIntegrationTests.java rename to spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestRestTemplateTwoComponentsIntegrationTests.java index 08e00159c61a..c106df3776fc 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestTwoComponentsIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestRestTemplateTwoComponentsIntegrationTests.java @@ -29,18 +29,18 @@ import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; /** - * Tests for {@link RestClientTest @RestClientTest} with two clients. + * Tests for {@link RestClientTest @RestClientTest} with two {@code RestTemplate} clients. * * @author Phillip Webb */ -@RestClientTest({ ExampleRestClient.class, AnotherExampleRestClient.class }) -class RestClientTestTwoComponentsIntegrationTests { +@RestClientTest({ ExampleRestTemplateService.class, AnotherExampleRestTemplateService.class }) +class RestClientTestRestTemplateTwoComponentsIntegrationTests { @Autowired - private ExampleRestClient client1; + private ExampleRestTemplateService client1; @Autowired - private AnotherExampleRestClient client2; + private AnotherExampleRestTemplateService client2; @Autowired private MockServerRestTemplateCustomizer customizer; diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestWithRestClientComponentIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestWithRestClientComponentIntegrationTests.java new file mode 100644 index 000000000000..954ecfbb1bb5 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestWithRestClientComponentIntegrationTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2023 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.test.autoconfigure.web.client; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.client.MockRestServiceServer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * Tests for {@link RestClientTest @RestClientTest} with a single client using + * {@code RestClient}. + * + * @author Phillip Webb + * @author Scott Frederick + */ +@RestClientTest(ExampleRestClientService.class) +class RestClientTestWithRestClientComponentIntegrationTests { + + @Autowired + private MockRestServiceServer server; + + @Autowired + private ExampleRestClientService client; + + @Test + void mockServerCall() { + this.server.expect(requestTo("https://example.com/test")).andRespond(withSuccess("hello", MediaType.TEXT_HTML)); + assertThat(this.client.test()).isEqualTo("hello"); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestWithComponentIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestWithRestTemplateComponentIntegrationTests.java similarity index 85% rename from spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestWithComponentIntegrationTests.java rename to spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestWithRestTemplateComponentIntegrationTests.java index 09ba31463e93..f495765f1823 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestWithComponentIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestWithRestTemplateComponentIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. @@ -27,18 +27,19 @@ import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; /** - * Tests for {@link RestClientTest @RestClientTest} with a single client. + * Tests for {@link RestClientTest @RestClientTest} with a single client using + * {@code RestTemplate}. * * @author Phillip Webb */ -@RestClientTest(ExampleRestClient.class) -class RestClientTestWithComponentIntegrationTests { +@RestClientTest(ExampleRestTemplateService.class) +class RestClientTestWithRestTemplateComponentIntegrationTests { @Autowired private MockRestServiceServer server; @Autowired - private ExampleRestClient client; + private ExampleRestTemplateService client; @Test void mockServerCall() { diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestWithoutJacksonIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestWithoutJacksonIntegrationTests.java index b6aa993467b5..250f50f5d183 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestWithoutJacksonIntegrationTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestWithoutJacksonIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. @@ -34,14 +34,14 @@ * @author Andy Wilkinson */ @ClassPathExclusions("jackson-*.jar") -@RestClientTest(ExampleRestClient.class) +@RestClientTest(ExampleRestTemplateService.class) class RestClientTestWithoutJacksonIntegrationTests { @Autowired private MockRestServiceServer server; @Autowired - private ExampleRestClient client; + private ExampleRestTemplateService client; @Test void restClientTestCanBeUsedWhenJacksonIsNotOnTheClassPath() { diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/MockServerRestClientCustomizer.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/MockServerRestClientCustomizer.java new file mode 100644 index 000000000000..29b8345140b8 --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/MockServerRestClientCustomizer.java @@ -0,0 +1,139 @@ +/* + * Copyright 2012-2023 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.test.web.client; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +import org.springframework.beans.BeanUtils; +import org.springframework.boot.web.client.RestClientCustomizer; +import org.springframework.http.client.BufferingClientHttpRequestFactory; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.test.web.client.MockRestServiceServer.MockRestServiceServerBuilder; +import org.springframework.test.web.client.RequestExpectationManager; +import org.springframework.test.web.client.SimpleRequestExpectationManager; +import org.springframework.util.Assert; +import org.springframework.web.client.RestClient; + +/** + * {@link RestClientCustomizer} that can be applied to {@link RestClient.Builder} + * instances to add {@link MockRestServiceServer} support. + *

    + * Typically applied to an existing builder before it is used, for example: + *

    + * MockServerRestClientCustomizer customizer = new MockServerRestClientCustomizer();
    + * RestClient.Builder builder = RestClient.builder();
    + * customizer.customize(builder);
    + * MyBean bean = new MyBean(client.build());
    + * customizer.getServer().expect(requestTo("/hello")).andRespond(withSuccess());
    + * bean.makeRestCall();
    + * 
    + *

    + * If the customizer is only used once, the {@link #getServer()} method can be used to + * obtain the mock server. If the customizer has been used more than once the + * {@link #getServer(RestClient.Builder)} or {@link #getServers()} method must be used to + * access the related server. + * + * @author Scott Frederick + * @since 3.2.0 + * @see #getServer() + * @see #getServer(RestClient.Builder) + */ +public class MockServerRestClientCustomizer implements RestClientCustomizer { + + private final Map expectationManagers = new ConcurrentHashMap<>(); + + private final Map servers = new ConcurrentHashMap<>(); + + private final Supplier expectationManagerSupplier; + + private boolean bufferContent = false; + + public MockServerRestClientCustomizer() { + this(SimpleRequestExpectationManager::new); + } + + /** + * Crate a new {@link MockServerRestClientCustomizer} instance. + * @param expectationManager the expectation manager class to use + */ + public MockServerRestClientCustomizer(Class expectationManager) { + this(() -> BeanUtils.instantiateClass(expectationManager)); + Assert.notNull(expectationManager, "ExpectationManager must not be null"); + } + + /** + * Crate a new {@link MockServerRestClientCustomizer} instance. + * @param expectationManagerSupplier a supplier that provides the + * {@link RequestExpectationManager} to use + * @since 3.0.0 + */ + public MockServerRestClientCustomizer(Supplier expectationManagerSupplier) { + Assert.notNull(expectationManagerSupplier, "ExpectationManagerSupplier must not be null"); + this.expectationManagerSupplier = expectationManagerSupplier; + } + + /** + * Set if the {@link BufferingClientHttpRequestFactory} wrapper should be used to + * buffer the input and output streams, and for example, allow multiple reads of the + * response body. + * @param bufferContent if request and response content should be buffered + * @since 3.1.0 + */ + public void setBufferContent(boolean bufferContent) { + this.bufferContent = bufferContent; + } + + @Override + public void customize(RestClient.Builder restClientBuilder) { + RequestExpectationManager expectationManager = createExpectationManager(); + MockRestServiceServerBuilder serverBuilder = MockRestServiceServer.bindTo(restClientBuilder); + if (this.bufferContent) { + serverBuilder.bufferContent(); + } + MockRestServiceServer server = serverBuilder.build(expectationManager); + this.expectationManagers.put(restClientBuilder, expectationManager); + this.servers.put(restClientBuilder, server); + } + + protected RequestExpectationManager createExpectationManager() { + return this.expectationManagerSupplier.get(); + } + + public MockRestServiceServer getServer() { + Assert.state(!this.servers.isEmpty(), "Unable to return a single MockRestServiceServer since " + + "MockServerRestClientCustomizer has not been bound to a RestClient"); + Assert.state(this.servers.size() == 1, "Unable to return a single MockRestServiceServer since " + + "MockServerRestClientCustomizer has been bound to more than one RestClient"); + return this.servers.values().iterator().next(); + } + + public Map getExpectationManagers() { + return this.expectationManagers; + } + + public MockRestServiceServer getServer(RestClient.Builder restClientBuilder) { + return this.servers.get(restClientBuilder); + } + + public Map getServers() { + return Collections.unmodifiableMap(this.servers); + } + +} diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/MockServerRestClientCustomizerTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/MockServerRestClientCustomizerTests.java new file mode 100644 index 000000000000..0dffb76514a8 --- /dev/null +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/web/client/MockServerRestClientCustomizerTests.java @@ -0,0 +1,163 @@ +/* + * Copyright 2012-2023 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.test.web.client; + +import java.util.function.Supplier; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.test.web.client.RequestExpectationManager; +import org.springframework.test.web.client.SimpleRequestExpectationManager; +import org.springframework.test.web.client.UnorderedRequestExpectationManager; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.Builder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * Tests for {@link MockServerRestClientCustomizer}. + * + * @author Scott Frederick + */ +class MockServerRestClientCustomizerTests { + + private MockServerRestClientCustomizer customizer; + + @BeforeEach + void setup() { + this.customizer = new MockServerRestClientCustomizer(); + } + + @Test + void createShouldUseSimpleRequestExpectationManager() { + MockServerRestClientCustomizer customizer = new MockServerRestClientCustomizer(); + customizer.customize(RestClient.builder()); + assertThat(customizer.getServer()).extracting("expectationManager") + .isInstanceOf(SimpleRequestExpectationManager.class); + } + + @Test + void createWhenExpectationManagerClassIsNullShouldThrowException() { + Class expectationManager = null; + assertThatIllegalArgumentException().isThrownBy(() -> new MockServerRestClientCustomizer(expectationManager)) + .withMessageContaining("ExpectationManager must not be null"); + } + + @Test + void createWhenExpectationManagerSupplierIsNullShouldThrowException() { + Supplier expectationManagerSupplier = null; + assertThatIllegalArgumentException() + .isThrownBy(() -> new MockServerRestClientCustomizer(expectationManagerSupplier)) + .withMessageContaining("ExpectationManagerSupplier must not be null"); + } + + @Test + void createShouldUseExpectationManagerClass() { + MockServerRestClientCustomizer customizer = new MockServerRestClientCustomizer( + UnorderedRequestExpectationManager.class); + customizer.customize(RestClient.builder()); + assertThat(customizer.getServer()).extracting("expectationManager") + .isInstanceOf(UnorderedRequestExpectationManager.class); + } + + @Test + void createShouldUseSupplier() { + MockServerRestClientCustomizer customizer = new MockServerRestClientCustomizer( + UnorderedRequestExpectationManager::new); + customizer.customize(RestClient.builder()); + assertThat(customizer.getServer()).extracting("expectationManager") + .isInstanceOf(UnorderedRequestExpectationManager.class); + } + + @Test + void customizeShouldBindServer() { + Builder builder = RestClient.builder(); + this.customizer.customize(builder); + this.customizer.getServer().expect(requestTo("/test")).andRespond(withSuccess()); + builder.build().get().uri("/test").retrieve().toEntity(String.class); + this.customizer.getServer().verify(); + } + + @Test + void getServerWhenNoServersAreBoundShouldThrowException() { + assertThatIllegalStateException().isThrownBy(this.customizer::getServer) + .withMessageContaining("Unable to return a single MockRestServiceServer since " + + "MockServerRestClientCustomizer has not been bound to a RestClient"); + } + + @Test + void getServerWhenMultipleServersAreBoundShouldThrowException() { + this.customizer.customize(RestClient.builder()); + this.customizer.customize(RestClient.builder()); + assertThatIllegalStateException().isThrownBy(this.customizer::getServer) + .withMessageContaining("Unable to return a single MockRestServiceServer since " + + "MockServerRestClientCustomizer has been bound to more than one RestClient"); + } + + @Test + void getServerWhenSingleServerIsBoundShouldReturnServer() { + Builder builder = RestClient.builder(); + this.customizer.customize(builder); + assertThat(this.customizer.getServer()).isEqualTo(this.customizer.getServer(builder)); + } + + @Test + void getServerWhenRestClientBuilderIsFoundShouldReturnServer() { + Builder builder1 = RestClient.builder(); + Builder builder2 = RestClient.builder(); + this.customizer.customize(builder1); + this.customizer.customize(builder2); + assertThat(this.customizer.getServer(builder1)).isNotNull(); + assertThat(this.customizer.getServer(builder2)).isNotNull().isNotSameAs(this.customizer.getServer(builder1)); + } + + @Test + void getServerWhenRestClientBuilderIsNotFoundShouldReturnNull() { + Builder builder1 = RestClient.builder(); + Builder builder2 = RestClient.builder(); + this.customizer.customize(builder1); + assertThat(this.customizer.getServer(builder1)).isNotNull(); + assertThat(this.customizer.getServer(builder2)).isNull(); + } + + @Test + void getServersShouldReturnServers() { + Builder builder1 = RestClient.builder(); + Builder builder2 = RestClient.builder(); + this.customizer.customize(builder1); + this.customizer.customize(builder2); + assertThat(this.customizer.getServers()).containsOnlyKeys(builder1, builder2); + } + + @Test + void getExpectationManagersShouldReturnExpectationManagers() { + Builder builder1 = RestClient.builder(); + Builder builder2 = RestClient.builder(); + this.customizer.customize(builder1); + this.customizer.customize(builder2); + RequestExpectationManager manager1 = this.customizer.getExpectationManagers().get(builder1); + RequestExpectationManager manager2 = this.customizer.getExpectationManagers().get(builder2); + assertThat(this.customizer.getServer(builder1)).extracting("expectationManager").isEqualTo(manager1); + assertThat(this.customizer.getServer(builder2)).extracting("expectationManager").isEqualTo(manager2); + } + +} From 4493958f1328ba660a4d57fe01b23784c0686dc5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Sat, 30 Sep 2023 09:04:04 +0100 Subject: [PATCH 0515/1215] Improve conditions for enabling WebFlux security This commit correct a mistake where AuthenticationManager was used instead of ReactiveAuthenticationManager. It also expands the conditions so that WebFlux security will be enabled when the user has defined their own SecurityWebFilterChain. In such a situation no other security-related beans may be needed to use WebFlux security as things may have been configured directly using the DSL. Closes gh-37504 --- .../ReactiveSecurityAutoConfiguration.java | 18 ++++++++++++------ ...ReactiveSecurityAutoConfigurationTests.java | 17 ++++++++++++++++- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfiguration.java index b4d4394bfe4a..5bcb151be46b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfiguration.java @@ -29,9 +29,10 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; -import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.WebFilterChainProxy; import org.springframework.web.reactive.config.WebFluxConfigurer; @@ -54,20 +55,20 @@ public class ReactiveSecurityAutoConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(WebFilterChainProxy.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) - @Conditional(ReactiveAuthenticationManagerCondition.class) + @Conditional(EnableWebFluxSecurityCondition.class) @EnableWebFluxSecurity static class EnableWebFluxSecurityConfiguration { } - static final class ReactiveAuthenticationManagerCondition extends AnyNestedCondition { + static final class EnableWebFluxSecurityCondition extends AnyNestedCondition { - ReactiveAuthenticationManagerCondition() { + EnableWebFluxSecurityCondition() { super(ConfigurationPhase.REGISTER_BEAN); } - @ConditionalOnBean(AuthenticationManager.class) - static final class ConditionalOnAuthenticationManagerBean { + @ConditionalOnBean(ReactiveAuthenticationManager.class) + static final class ConditionalOnReactiveAuthenticationManagerBean { } @@ -76,6 +77,11 @@ static final class ConditionalOnReactiveUserDetailsService { } + @ConditionalOnBean(SecurityWebFilterChain.class) + static final class ConditionalOnSecurityWebFilterChain { + + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java index b046e89d970c..dd5dd07ef32d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java @@ -25,9 +25,11 @@ import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; import org.springframework.security.core.userdetails.User; +import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.WebFilterChainProxy; import org.springframework.web.reactive.config.WebFluxConfigurer; @@ -57,11 +59,24 @@ void backsOffWhenReactiveAuthenticationManagerNotPresent() { } @Test - void enablesWebFluxSecurity() { + void enablesWebFluxSecurityWhenUserDetailsServiceIsPresent() { this.contextRunner.withUserConfiguration(UserDetailsServiceConfiguration.class) .run((context) -> assertThat(context).getBean(WebFilterChainProxy.class).isNotNull()); } + @Test + void enablesWebFluxSecurityWhenReactiveAuthenticationManagerIsPresent() { + this.contextRunner + .withBean(ReactiveAuthenticationManager.class, () -> mock(ReactiveAuthenticationManager.class)) + .run((context) -> assertThat(context).getBean(WebFilterChainProxy.class).isNotNull()); + } + + @Test + void enablesWebFluxSecurityWhenSecurityWebFilterChainIsPresent() { + this.contextRunner.withBean(SecurityWebFilterChain.class, () -> mock(SecurityWebFilterChain.class)) + .run((context) -> assertThat(context).getBean(WebFilterChainProxy.class).isNotNull()); + } + @Test void autoConfigurationIsConditionalOnClass() { this.contextRunner From a454712dea35d5528f25491fd94f2865b93f3633 Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Thu, 28 Sep 2023 21:09:18 -0700 Subject: [PATCH 0516/1215] Add auto-configuration for CountedAspect and TimedAspect See gh-37626 --- .../MetricsAspectsAutoConfiguration.java | 60 +++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../MetricsAspectsAutoConfigurationTests.java | 121 ++++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAspectsAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAspectsAutoConfigurationTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAspectsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAspectsAutoConfiguration.java new file mode 100644 index 000000000000..dbeeb8b27d6e --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAspectsAutoConfiguration.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.metrics; + +import io.micrometer.core.aop.CountedAspect; +import io.micrometer.core.aop.MeterTagAnnotationHandler; +import io.micrometer.core.aop.TimedAspect; +import io.micrometer.core.instrument.MeterRegistry; +import org.aspectj.weaver.Advice; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Micrometer-based metrics + * aspects. + * + * @author Jonatan Ivanov + * @since 3.2.0 + */ +@AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class }) +@ConditionalOnClass({ MeterRegistry.class, Advice.class }) +@ConditionalOnBean(MeterRegistry.class) +public class MetricsAspectsAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + CountedAspect countedAspect(MeterRegistry registry) { + return new CountedAspect(registry); + } + + @Bean + @ConditionalOnMissingBean + TimedAspect timedAspect(MeterRegistry registry, + ObjectProvider meterTagAnnotationHandler) { + TimedAspect timedAspect = new TimedAspect(registry); + meterTagAnnotationHandler.ifAvailable(timedAspect::setMeterTagAnnotationHandler); + return timedAspect; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 9ce3a9092f86..b1e23fd863fe 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -43,6 +43,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.JvmMetricsAutoConfigurati org.springframework.boot.actuate.autoconfigure.metrics.KafkaMetricsAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.Log4J2MetricsAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.LogbackMetricsAutoConfiguration +org.springframework.boot.actuate.autoconfigure.metrics.MetricsAspectsAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.MetricsEndpointAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.SystemMetricsAutoConfiguration diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAspectsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAspectsAutoConfigurationTests.java new file mode 100644 index 000000000000..215eba7b481b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAspectsAutoConfigurationTests.java @@ -0,0 +1,121 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.metrics; + +import io.micrometer.core.aop.CountedAspect; +import io.micrometer.core.aop.MeterTagAnnotationHandler; +import io.micrometer.core.aop.TimedAspect; +import io.micrometer.core.instrument.MeterRegistry; +import org.aspectj.weaver.Advice; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MetricsAspectsAutoConfiguration}. + * + * @author Jonatan Ivanov + */ +class MetricsAspectsAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(MetricsAspectsAutoConfiguration.class)); + + @Test + void shouldConfigureAspects() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(CountedAspect.class); + assertThat(context).hasSingleBean(TimedAspect.class); + }); + } + + @Test + void shouldConfigureMeterTagAnnotationHandler() { + this.contextRunner.withUserConfiguration(MeterTagAnnotationHandlerConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(CountedAspect.class); + assertThat(ReflectionTestUtils.getField(context.getBean(TimedAspect.class), "meterTagAnnotationHandler")) + .isSameAs(context.getBean(MeterTagAnnotationHandler.class)); + }); + } + + @Test + void shouldNotConfigureAspectsIfMicrometerIsMissing() { + this.contextRunner.withClassLoader(new FilteredClassLoader(MeterRegistry.class)).run((context) -> { + assertThat(context).doesNotHaveBean(CountedAspect.class); + assertThat(context).doesNotHaveBean(TimedAspect.class); + }); + } + + @Test + void shouldNotConfigureAspectsIfAspectjIsMissing() { + this.contextRunner.withClassLoader(new FilteredClassLoader(Advice.class)).run((context) -> { + assertThat(context).doesNotHaveBean(CountedAspect.class); + assertThat(context).doesNotHaveBean(TimedAspect.class); + }); + } + + @Test + void shouldNotConfigureAspectsIfMeterRegistryBeanIsMissing() { + new ApplicationContextRunner().run((context) -> { + assertThat(context).doesNotHaveBean(MeterRegistry.class); + assertThat(context).doesNotHaveBean(CountedAspect.class); + assertThat(context).doesNotHaveBean(TimedAspect.class); + }); + } + + @Test + void shouldBackOffIfAspectBeansExist() { + this.contextRunner.withUserConfiguration(CustomAspectsConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(CountedAspect.class).hasBean("customCountedAspect"); + assertThat(context).hasSingleBean(TimedAspect.class).hasBean("customTimedAspect"); + }); + } + + @Configuration(proxyBeanMethods = false) + static class CustomAspectsConfiguration { + + @Bean + CountedAspect customCountedAspect(MeterRegistry registry) { + return new CountedAspect(registry); + } + + @Bean + TimedAspect customTimedAspect(MeterRegistry registry) { + return new TimedAspect(registry); + } + + } + + @Configuration(proxyBeanMethods = false) + static class MeterTagAnnotationHandlerConfiguration { + + @Bean + MeterTagAnnotationHandler meterTagAnnotationHandler() { + return new MeterTagAnnotationHandler(null, null); + } + + } + +} From 932355adbfd75254c5369331f66be89ed8178139 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 2 Oct 2023 21:18:26 -0700 Subject: [PATCH 0517/1215] Polish adoc formatting --- .../spring-boot-docs/src/docs/asciidoc/features/testing.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc index d08ffbb58df3..1015b354bfe6 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc @@ -751,6 +751,8 @@ When using a `RestClient.Builder` in the beans under test, or when using a `Rest include::code:MyRestClientServiceTests[] + + [[features.testing.spring-boot-applications.autoconfigured-spring-restdocs]] ==== Auto-configured Spring REST Docs Tests You can use the `@AutoConfigureRestDocs` annotation to use {spring-restdocs}[Spring REST Docs] in your tests with Mock MVC, REST Assured, or WebTestClient. From 780f75d5a2f83ebe279b0b6a32a2fe40a0b3a686 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 28 Sep 2023 13:45:45 -0700 Subject: [PATCH 0518/1215] Polish --- .../spring-boot-server-tests-app/build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/spring-boot-server-tests-app/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/spring-boot-server-tests-app/build.gradle index b8ff1ca48d1a..bd73d368e598 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/spring-boot-server-tests-app/build.gradle +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/spring-boot-server-tests-app/build.gradle @@ -12,13 +12,13 @@ apply plugin: "io.spring.dependency-management" repositories { maven { url "file:${rootDir}/../test-repository"} mavenCentral() - maven { + maven { url "https://repo.spring.io/milestone" content { excludeGroup "org.springframework.boot" } } - maven { + maven { url "https://repo.spring.io/snapshot" content { excludeGroup "org.springframework.boot" @@ -41,7 +41,7 @@ configurations { } } -tasks.register("resourcesJar", Jar) { jar -> +tasks.register("resourcesJar", Jar) { jar -> def nested = project.resources.text.fromString("nested") from(nested) { into "META-INF/resources/" @@ -76,7 +76,7 @@ def boolean isWindows() { } ["jetty", "tomcat", "undertow"].each { webServer -> - def configurer = { task -> + def configurer = { task -> task.dependsOn resourcesJar task.mainClass = "com.example.ResourceHandlingApplication" task.classpath = configurations.getByName(webServer) From 3d6859e80f5b66613db1f33a6751854de7e49b21 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 28 Sep 2023 13:29:30 -0700 Subject: [PATCH 0519/1215] Use the term "uber jar" in reference documentation and javadoc Update reference documentation and javadoc to use the term "uber jar" rather than "fat jar". Closes gh-37653 --- .../springframework/boot/devtools/restart/Restarter.java | 6 +++--- .../src/docs/asciidoc/container-images/dockerfiles.adoc | 2 +- .../docs/asciidoc/container-images/efficient-images.adoc | 6 +++--- .../src/docs/asciidoc/executable-jar/property-launcher.adoc | 2 +- .../asciidoc/features/developing-auto-configuration.adoc | 2 +- .../docs/asciidoc/getting-started/first-application.adoc | 2 +- .../src/docs/asciidoc/reacting.adoc | 2 +- .../java/org/springframework/boot/loader/tools/Library.java | 4 ++-- .../springframework/boot/loader/tools/StandardLayers.java | 2 +- .../boot/loader/jarmode/JarModeLauncher.java | 2 +- .../org/springframework/boot/maven/ArtifactsLibraries.java | 2 +- .../java/org/springframework/boot/maven/RepackageMojo.java | 2 +- .../springframework/boot/loader/LoaderIntegrationTests.java | 2 +- 13 files changed, 18 insertions(+), 18 deletions(-) diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java index 410b269d1d66..b02a92fb3156 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/Restarter.java @@ -68,9 +68,9 @@ * {@link #initialize(String[])} directly if your SpringApplication arguments are not * identical to your main method arguments. *

    - * By default, applications running in an IDE (i.e. those not packaged as "fat jars") will - * automatically detect URLs that can change. It's also possible to manually configure - * URLs or class file updates for remote restart scenarios. + * By default, applications running in an IDE (i.e. those not packaged as "uber jars") + * will automatically detect URLs that can change. It's also possible to manually + * configure URLs or class file updates for remote restart scenarios. * * @author Phillip Webb * @author Andy Wilkinson diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/container-images/dockerfiles.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/container-images/dockerfiles.adoc index 1c3d77cff254..a3374debfaac 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/container-images/dockerfiles.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/container-images/dockerfiles.adoc @@ -1,6 +1,6 @@ [[container-images.dockerfiles]] == Dockerfiles -While it is possible to convert a Spring Boot fat jar into a docker image with just a few lines in the Dockerfile, we will use the <> to create an optimized docker image. +While it is possible to convert a Spring Boot uber jar into a docker image with just a few lines in the Dockerfile, we will use the <> to create an optimized docker image. When you create a jar containing the layers index file, the `spring-boot-jarmode-layertools` jar will be added as a dependency to your jar. With this jar on the classpath, you can launch your application in a special mode which allows the bootstrap code to run something entirely different from your application, for example, something that extracts the layers. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/container-images/efficient-images.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/container-images/efficient-images.adoc index ad70d6a75eb5..b7bfbdf9f144 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/container-images/efficient-images.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/container-images/efficient-images.adoc @@ -1,8 +1,8 @@ [[container-images.efficient-images]] == Efficient Container Images -It is easily possible to package a Spring Boot fat jar as a docker image. -However, there are various downsides to copying and running the fat jar as is in the docker image. -There’s always a certain amount of overhead when running a fat jar without unpacking it, and in a containerized environment this can be noticeable. +It is easily possible to package a Spring Boot uber jar as a docker image. +However, there are various downsides to copying and running the uber jar as is in the docker image. +There’s always a certain amount of overhead when running a uber jar without unpacking it, and in a containerized environment this can be noticeable. The other issue is that putting your application's code and all its dependencies in one layer in the Docker image is sub-optimal. Since you probably recompile your code more often than you upgrade the version of Spring Boot you use, it’s often better to separate things a bit more. If you put jar files in the layer before your application classes, Docker often only needs to change the very bottom layer and can pick others up from its cache. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/property-launcher.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/property-launcher.adoc index 675a2bc27801..ba6ae905b834 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/property-launcher.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/property-launcher.adoc @@ -64,7 +64,7 @@ When specified as environment variables or manifest entries, the following names | `LOADER_SYSTEM` |=== -TIP: Build plugins automatically move the `Main-Class` attribute to `Start-Class` when the fat jar is built. +TIP: Build plugins automatically move the `Main-Class` attribute to `Start-Class` when the uber jar is built. If you use that, specify the name of the class to launch by using the `Main-Class` attribute and leaving out `Start-Class`. The following rules apply to working with `PropertiesLauncher`: diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/developing-auto-configuration.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/developing-auto-configuration.adoc index 70f020dc5a90..a98d92eecfec 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/developing-auto-configuration.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/developing-auto-configuration.adoc @@ -272,7 +272,7 @@ When building with Maven, it is recommended to add the following dependency in a ---- -If you have defined auto-configurations directly in your application, make sure to configure the `spring-boot-maven-plugin` to prevent the `repackage` goal from adding the dependency into the fat jar: +If you have defined auto-configurations directly in your application, make sure to configure the `spring-boot-maven-plugin` to prevent the `repackage` goal from adding the dependency into the uber jar: [source,xml,indent=0,subs="verbatim"] ---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/first-application.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/first-application.adoc index 6abd497b49b7..b0daeba458ce 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/first-application.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/getting-started/first-application.adoc @@ -380,7 +380,7 @@ To gracefully exit the application, press `ctrl-c`. [[getting-started.first-application.executable-jar]] === Creating an Executable Jar We finish our example by creating a completely self-contained executable jar file that we could run in production. -Executable jars (sometimes called "`fat jars`") are archives containing your compiled classes along with all of the jar dependencies that your code needs to run. +Executable jars (sometimes called "`uber jars`" or "`fat jars`") are archives containing your compiled classes along with all of the jar dependencies that your code needs to run. .Executable jars and Java **** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc index 7da583578b38..31d1f2c1b32b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc @@ -9,7 +9,7 @@ This section describes those changes. == Reacting to the Java Plugin When Gradle's {java-plugin}[`java` plugin] is applied to a project, the Spring Boot plugin: -1. Creates a {boot-jar-javadoc}[`BootJar`] task named `bootJar` that will create an executable, fat jar for the project. +1. Creates a {boot-jar-javadoc}[`BootJar`] task named `bootJar` that will create an executable, uber jar for the project. The jar will contain everything on the runtime classpath of the main source set; classes are packaged in `BOOT-INF/classes` and jars are packaged in `BOOT-INF/lib` 2. Configures the `assemble` task to depend on the `bootJar` task. 3. Configures the `jar` task to use `plain` as the convention for its archive classifier. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java index f2fe532223c5..654666ed3066 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Library.java @@ -64,7 +64,7 @@ public Library(File file, LibraryScope scope) { * @param unpackRequired if the library needs to be unpacked before it can be used * @param local if the library is local (part of the same build) to the application * that is being packaged - * @param included if the library is included in the fat jar + * @param included if the library is included in the uber jar * @since 2.4.8 */ public Library(String name, File file, LibraryScope scope, LibraryCoordinates coordinates, boolean unpackRequired, @@ -142,7 +142,7 @@ public boolean isLocal() { } /** - * Return if the library is included in the fat jar. + * Return if the library is included in the uber jar. * @return if the library is included */ public boolean isIncluded() { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/StandardLayers.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/StandardLayers.java index 544c213f3ad9..22b8a0c02c78 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/StandardLayers.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/StandardLayers.java @@ -27,7 +27,7 @@ *

      *
    1. "dependencies" - For non snapshot dependencies
    2. *
    3. "spring-boot-loader" - For classes from {@code spring-boot-loader} used to launch a - * fat jar
    4. + * uber jar *
    5. "snapshot-dependencies" - For snapshot dependencies
    6. *
    7. "application" - For application classes and resources
    8. *
    diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java index 42a89a50a35b..442488934103 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java @@ -22,7 +22,7 @@ import org.springframework.util.ClassUtils; /** - * Delegate class used to launch the fat jar in a specific mode. + * Delegate class used to launch the uber jar in a specific mode. * * @author Phillip Webb * @since 2.3.0 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java index 372f74a1b566..dfeca203d18c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/ArtifactsLibraries.java @@ -85,7 +85,7 @@ public ArtifactsLibraries(Set artifacts, Collection loca /** * Creates a new {@code ArtifactsLibraries} from the given {@code artifacts}. * @param artifacts all artifacts that can be represented as libraries - * @param includedArtifacts the actual artifacts to include in the fat jar + * @param includedArtifacts the actual artifacts to include in the uber jar * @param localProjects projects for which {@link Library#isLocal() local} libraries * should be created * @param unpacks artifacts that should be unpacked on launch diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java index 1e3df71dd495..5ca2ecac5a90 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java @@ -106,7 +106,7 @@ public class RepackageMojo extends AbstractPackagerMojo { private boolean attach = true; /** - * A list of the libraries that must be unpacked from fat jars in order to run. + * A list of the libraries that must be unpacked from uber jars in order to run. * Specify each library as a {@code } with a {@code } and a * {@code } and they will be unpacked at runtime. * @since 1.1.0 diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java index 6e5168574e3e..9acac3f61da1 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java @@ -39,7 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests loader that supports fat jars. + * Integration tests loader that supports uber jars. * * @author Phillip Webb * @author Moritz Halbritter From c22548982abf0b9c5c02406fe789b0423fcfa02d Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 18 Sep 2023 12:39:28 -0700 Subject: [PATCH 0520/1215] Relocate launcher classes Create alternative launcher classes under the package `org.springframework.boot.loader.launch` and use them in favor of the previous location. This update is designed to improve compatibility with future changes in the loader. Closes gh-37667 --- .../container-images/dockerfiles.adoc | 2 +- .../container-images/efficient-images.adoc | 4 +-- .../docs/asciidoc/deployment/efficient.adoc | 2 +- .../asciidoc/executable-jar/launching.adoc | 2 +- .../asciidoc/features/spring-application.adoc | 2 +- .../src/docs/asciidoc/howto/build.adoc | 2 +- .../org/springframework/boot/ant/antlib.xml | 2 +- .../spring-boot-cli/build.gradle | 2 +- .../src/main/content/bin/spring.bat | 2 +- .../src/main/executablecontent/bin/spring | 2 +- .../cli/command/shell/ForkProcessCommand.java | 4 +-- .../boot-war-properties-launcher.gradle | 2 +- .../boot-war-properties-launcher.gradle.kts | 2 +- .../tasks/bundling/BootArchiveSupport.java | 6 ++-- .../boot/gradle/tasks/bundling/BootJar.java | 2 +- .../boot/gradle/tasks/bundling/BootWar.java | 2 +- .../docs/PackagingDocumentationTests.java | 2 +- .../bundling/AbstractBootArchiveTests.java | 6 ++-- .../gradle/tasks/bundling/BootJarTests.java | 2 +- .../gradle/tasks/bundling/BootWarTests.java | 2 +- ...nTests-explodedApplicationClasspath.gradle | 2 +- .../boot/loader/tools/Layouts.java | 8 ++--- .../loader/tools/AbstractPackagerTests.java | 8 ++--- .../boot/loader/tools/RepackagerTests.java | 4 +-- .../boot/loader/launch/JarLauncher.java | 34 +++++++++++++++++++ .../loader/launch/PropertiesLauncher.java | 34 +++++++++++++++++++ .../boot/loader/launch/WarLauncher.java | 34 +++++++++++++++++++ .../boot/loader/launch/package-info.java | 23 +++++++++++++ .../boot/maven/JarIntegrationTests.java | 16 ++++----- .../boot/maven/WarIntegrationTests.java | 4 +-- .../boot/image/paketo/PaketoBuilderTests.java | 10 +++--- .../embedded/ExplodedApplicationLauncher.java | 6 ++-- .../spring-boot-smoke-test-ant/build.xml | 2 +- 33 files changed, 183 insertions(+), 54 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/package-info.java diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/container-images/dockerfiles.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/container-images/dockerfiles.adoc index a3374debfaac..1956cc4a3188 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/container-images/dockerfiles.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/container-images/dockerfiles.adoc @@ -44,7 +44,7 @@ COPY --from=builder application/dependencies/ ./ COPY --from=builder application/spring-boot-loader/ ./ COPY --from=builder application/snapshot-dependencies/ ./ COPY --from=builder application/application/ ./ -ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"] +ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"] ---- Assuming the above `Dockerfile` is in the current directory, your docker image can be built with `docker build .`, or optionally specifying the path to your application jar, as shown in the following example: diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/container-images/efficient-images.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/container-images/efficient-images.adoc index b7bfbdf9f144..d06327bb05f3 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/container-images/efficient-images.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/container-images/efficient-images.adoc @@ -28,8 +28,8 @@ The following shows an example of a `layers.idx` file: - BOOT-INF/lib/library1.jar - BOOT-INF/lib/library2.jar - "spring-boot-loader": - - org/springframework/boot/loader/JarLauncher.class - - org/springframework/boot/loader/jar/JarEntry.class + - org/springframework/boot/loader/launch/JarLauncher.class + - ... - "snapshot-dependencies": - BOOT-INF/lib/library3-SNAPSHOT.jar - "application": diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc index 9064f844c7f8..5b8db54d370d 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc @@ -13,7 +13,7 @@ One way to run an unpacked archive is by starting the appropriate launcher, as f [source,shell,indent=0,subs="verbatim"] ---- $ jar -xf myapp.jar - $ java org.springframework.boot.loader.JarLauncher + $ java org.springframework.boot.loader.launch.JarLauncher ---- This is actually slightly faster on startup (depending on the size of the jar) than running from an unexploded archive. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/launching.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/launching.adoc index a672c6963495..481145b60a6d 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/launching.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/launching.adoc @@ -22,7 +22,7 @@ The following example shows a typical `MANIFEST.MF` for an executable jar file: [indent=0] ---- - Main-Class: org.springframework.boot.loader.JarLauncher + Main-Class: org.springframework.boot.loader.launch.JarLauncher Start-Class: com.mycompany.project.MyApplication ---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc index 9aae568be3db..5255d703817f 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc @@ -132,7 +132,7 @@ The printed banner is registered as a singleton bean under the following name: ` The `${application.version}` and `${application.formatted-version}` properties are only available if you are using Spring Boot launchers. The values will not be resolved if you are running an unpacked jar and starting it with `java -cp `. -This is why we recommend that you always launch unpacked jars using `java org.springframework.boot.loader.JarLauncher`. +This is why we recommend that you always launch unpacked jars using `java org.springframework.boot.loader.launch.JarLauncher`. This will initialize the `application.*` banner variables before building the classpath and launching your app. ==== diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/build.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/build.adoc index 97c24447a62d..cfae774a9500 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/build.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/build.adoc @@ -290,7 +290,7 @@ The following example shows how to build an executable archive with Ant: - + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/main/resources/org/springframework/boot/ant/antlib.xml b/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/main/resources/org/springframework/boot/ant/antlib.xml index bf2f7307866d..3a0d4902d9a1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/main/resources/org/springframework/boot/ant/antlib.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/main/resources/org/springframework/boot/ant/antlib.xml @@ -61,7 +61,7 @@ + value="org.springframework.boot.loader.launch.JarLauncher" /> diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cli/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-cli/build.gradle index 35714e676f53..db81bc3e806e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-cli/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-cli/build.gradle @@ -66,7 +66,7 @@ task fullJar(type: Jar) { } manifest { attributes( - "Main-Class": "org.springframework.boot.loader.JarLauncher", + "Main-Class": "org.springframework.boot.loader.launch.JarLauncher", "Start-Class": "org.springframework.boot.cli.SpringCli" ) } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/content/bin/spring.bat b/spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/content/bin/spring.bat index c9c0081c06f7..3bec92853213 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/content/bin/spring.bat +++ b/spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/content/bin/spring.bat @@ -59,7 +59,7 @@ set CMD_LINE_ARGS=%$ @rem Setup the command line set CLASSPATH=%SPRING_HOME%\lib\* -"%JAVA_EXE%" %JAVA_OPTS% -cp "%CLASSPATH%" org.springframework.boot.loader.JarLauncher %CMD_LINE_ARGS% +"%JAVA_EXE%" %JAVA_OPTS% -cp "%CLASSPATH%" org.springframework.boot.loader.launch.JarLauncher %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/executablecontent/bin/spring b/spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/executablecontent/bin/spring index 0e025b27d6f7..dda4e9b2819b 100755 --- a/spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/executablecontent/bin/spring +++ b/spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/executablecontent/bin/spring @@ -115,4 +115,4 @@ if $cygwin; then fi IFS=" " read -r -a javaOpts <<< "$JAVA_OPTS" -exec "${JAVA_HOME}/bin/java" "${javaOpts[@]}" -cp "$CLASSPATH" org.springframework.boot.loader.JarLauncher "$@" +exec "${JAVA_HOME}/bin/java" "${javaOpts[@]}" -cp "$CLASSPATH" org.springframework.boot.loader.launch.JarLauncher "$@" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/shell/ForkProcessCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/shell/ForkProcessCommand.java index a3c282faf429..eb2336d75ca8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/shell/ForkProcessCommand.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/shell/ForkProcessCommand.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -33,7 +33,7 @@ */ class ForkProcessCommand extends RunProcessCommand { - private static final String MAIN_CLASS = "org.springframework.boot.loader.JarLauncher"; + private static final String MAIN_CLASS = "org.springframework.boot.loader.launch.JarLauncher"; private final Command command; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-war-properties-launcher.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-war-properties-launcher.gradle index 2872469f60fb..6a1897ae3d3b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-war-properties-launcher.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-war-properties-launcher.gradle @@ -10,7 +10,7 @@ tasks.named("bootWar") { // tag::properties-launcher[] tasks.named("bootWar") { manifest { - attributes 'Main-Class': 'org.springframework.boot.loader.PropertiesLauncher' + attributes 'Main-Class': 'org.springframework.boot.loader.launch.PropertiesLauncher' } } // end::properties-launcher[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-war-properties-launcher.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-war-properties-launcher.gradle.kts index 19d723b795fa..f5284eb8f259 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-war-properties-launcher.gradle.kts +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-war-properties-launcher.gradle.kts @@ -12,7 +12,7 @@ tasks.named("bootWar") { // tag::properties-launcher[] tasks.named("bootWar") { manifest { - attributes("Main-Class" to "org.springframework.boot.loader.PropertiesLauncher") + attributes("Main-Class" to "org.springframework.boot.loader.launch.PropertiesLauncher") } } // end::properties-launcher[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java index 7a39e10bfb4f..921e9f3d4856 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java @@ -61,9 +61,9 @@ class BootArchiveSupport { static { Set defaultLauncherClasses = new HashSet<>(); - defaultLauncherClasses.add("org.springframework.boot.loader.JarLauncher"); - defaultLauncherClasses.add("org.springframework.boot.loader.PropertiesLauncher"); - defaultLauncherClasses.add("org.springframework.boot.loader.WarLauncher"); + defaultLauncherClasses.add("org.springframework.boot.loader.launch.JarLauncher"); + defaultLauncherClasses.add("org.springframework.boot.loader.launch.PropertiesLauncher"); + defaultLauncherClasses.add("org.springframework.boot.loader.launch.WarLauncher"); DEFAULT_LAUNCHER_CLASSES = Collections.unmodifiableSet(defaultLauncherClasses); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java index 2b2d1cfa2e55..5cf51bb85075 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java @@ -49,7 +49,7 @@ @DisableCachingByDefault(because = "Not worth caching") public abstract class BootJar extends Jar implements BootArchive { - private static final String LAUNCHER = "org.springframework.boot.loader.JarLauncher"; + private static final String LAUNCHER = "org.springframework.boot.loader.launch.JarLauncher"; private static final String CLASSES_DIRECTORY = "BOOT-INF/classes/"; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java index 47ce5f0c5410..d697f00a1e54 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java @@ -48,7 +48,7 @@ @DisableCachingByDefault(because = "Not worth caching") public abstract class BootWar extends War implements BootArchive { - private static final String LAUNCHER = "org.springframework.boot.loader.WarLauncher"; + private static final String LAUNCHER = "org.springframework.boot.loader.launch.WarLauncher"; private static final String CLASSES_DIRECTORY = "WEB-INF/classes/"; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java index 1b1a6531682c..187c42fb6902 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java @@ -166,7 +166,7 @@ void bootWarPropertiesLauncher() throws IOException { assertThat(file).isFile(); try (JarFile jar = new JarFile(file)) { assertThat(jar.getManifest().getMainAttributes().getValue("Main-Class")) - .isEqualTo("org.springframework.boot.loader.PropertiesLauncher"); + .isEqualTo("org.springframework.boot.loader.launch.PropertiesLauncher"); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java index f4eceac89a8f..17623f9ed415 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java @@ -270,7 +270,9 @@ void loaderIsWrittenToTheRootOfTheJarAfterManifest() throws IOException { void loaderIsWrittenToTheRootOfTheJarWhenUsingThePropertiesLauncher() throws IOException { this.task.getMainClass().set("com.example.Main"); executeTask(); - this.task.getManifest().getAttributes().put("Main-Class", "org.springframework.boot.loader.PropertiesLauncher"); + this.task.getManifest() + .getAttributes() + .put("Main-Class", "org.springframework.boot.loader.launch.PropertiesLauncher"); try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { assertThat(jarFile.getEntry("org/springframework/boot/loader/LaunchedURLClassLoader.class")).isNotNull(); assertThat(jarFile.getEntry("org/springframework/boot/loader/")).isNotNull(); @@ -362,7 +364,7 @@ void customMainClassInTheManifestIsHonored() throws IOException { assertThat(jarFile.getManifest().getMainAttributes().getValue("Main-Class")) .isEqualTo("com.example.CustomLauncher"); assertThat(jarFile.getManifest().getMainAttributes().getValue("Start-Class")).isEqualTo("com.example.Main"); - assertThat(jarFile.getEntry("org/springframework/boot/loader/LaunchedURLClassLoader.class")).isNull(); + assertThat(jarFile.getEntry("org/springframework/boot/loader/launch/LaunchedClassLoader.class")).isNull(); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java index 5355e1ba79d8..2cbe89cf5712 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java @@ -41,7 +41,7 @@ class BootJarTests extends AbstractBootArchiveTests { BootJarTests() { - super(BootJar.class, "org.springframework.boot.loader.JarLauncher", "BOOT-INF/lib/", "BOOT-INF/classes/", + super(BootJar.class, "org.springframework.boot.loader.launch.JarLauncher", "BOOT-INF/lib/", "BOOT-INF/classes/", "BOOT-INF/"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootWarTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootWarTests.java index e53080ca779f..8728298b4936 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootWarTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootWarTests.java @@ -39,7 +39,7 @@ class BootWarTests extends AbstractBootArchiveTests { BootWarTests() { - super(BootWar.class, "org.springframework.boot.loader.WarLauncher", "WEB-INF/lib/", "WEB-INF/classes/", + super(BootWar.class, "org.springframework.boot.loader.launch.WarLauncher", "WEB-INF/lib/", "WEB-INF/classes/", "WEB-INF/"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-explodedApplicationClasspath.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-explodedApplicationClasspath.gradle index d4fb21f8c9f0..c0139a8a971d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-explodedApplicationClasspath.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-explodedApplicationClasspath.gradle @@ -21,5 +21,5 @@ task explode(type: Sync) { task launch(type: JavaExec) { classpath = files(explode) - mainClass = 'org.springframework.boot.loader.JarLauncher' + mainClass = 'org.springframework.boot.loader.launch.JarLauncher' } \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java index 61586d3d1c51..82c73ef85987 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -66,7 +66,7 @@ public static class Jar implements RepackagingLayout { @Override public String getLauncherClassName() { - return "org.springframework.boot.loader.JarLauncher"; + return "org.springframework.boot.loader.launch.JarLauncher"; } @Override @@ -108,7 +108,7 @@ public static class Expanded extends Jar { @Override public String getLauncherClassName() { - return "org.springframework.boot.loader.PropertiesLauncher"; + return "org.springframework.boot.loader.launch.PropertiesLauncher"; } } @@ -148,7 +148,7 @@ public static class War implements Layout { @Override public String getLauncherClassName() { - return "org.springframework.boot.loader.WarLauncher"; + return "org.springframework.boot.loader.launch.WarLauncher"; } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java index 3429e4af90ea..c4986aba5685 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java @@ -105,7 +105,7 @@ void specificMainClass() throws Exception { execute(packager, NO_LIBRARIES); Manifest actualManifest = getPackagedManifest(); assertThat(actualManifest.getMainAttributes().getValue("Main-Class")) - .isEqualTo("org.springframework.boot.loader.JarLauncher"); + .isEqualTo("org.springframework.boot.loader.launch.JarLauncher"); assertThat(actualManifest.getMainAttributes().getValue("Start-Class")).isEqualTo("a.b.C"); assertThat(hasPackagedLauncherClasses()).isTrue(); } @@ -121,7 +121,7 @@ void mainClassFromManifest() throws Exception { execute(packager, NO_LIBRARIES); Manifest actualManifest = getPackagedManifest(); assertThat(actualManifest.getMainAttributes().getValue("Main-Class")) - .isEqualTo("org.springframework.boot.loader.JarLauncher"); + .isEqualTo("org.springframework.boot.loader.launch.JarLauncher"); assertThat(actualManifest.getMainAttributes().getValue("Start-Class")).isEqualTo("a.b.C"); assertThat(hasPackagedLauncherClasses()).isTrue(); } @@ -133,7 +133,7 @@ void mainClassFound() throws Exception { execute(packager, NO_LIBRARIES); Manifest actualManifest = getPackagedManifest(); assertThat(actualManifest.getMainAttributes().getValue("Main-Class")) - .isEqualTo("org.springframework.boot.loader.JarLauncher"); + .isEqualTo("org.springframework.boot.loader.launch.JarLauncher"); assertThat(actualManifest.getMainAttributes().getValue("Start-Class")).isEqualTo("a.b.C"); assertThat(hasPackagedLauncherClasses()).isTrue(); } @@ -684,7 +684,7 @@ protected Collection getPackagedEntryNames() throws IOException { protected boolean hasPackagedLauncherClasses() throws IOException { return hasPackagedEntry("org/springframework/boot/") - && hasPackagedEntry("org/springframework/boot/loader/JarLauncher.class"); + && hasPackagedEntry("org/springframework/boot/loader/launch/JarLauncher.class"); } private boolean hasPackagedEntry(String name) throws IOException { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java index a4a648c34fb9..f1dd7d583d25 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java @@ -79,7 +79,7 @@ void jarIsOnlyRepackagedOnce() throws Exception { repackager.repackage(NO_LIBRARIES); Manifest actualManifest = getPackagedManifest(); assertThat(actualManifest.getMainAttributes().getValue("Main-Class")) - .isEqualTo("org.springframework.boot.loader.JarLauncher"); + .isEqualTo("org.springframework.boot.loader.launch.JarLauncher"); assertThat(actualManifest.getMainAttributes().getValue("Start-Class")).isEqualTo("a.b.C"); assertThat(hasPackagedLauncherClasses()).isTrue(); } @@ -220,7 +220,7 @@ void repackagingDeeplyNestedPackageIsNotProhibitivelySlow() throws IOException { private boolean hasLauncherClasses(File file) throws IOException { return hasEntry(file, "org/springframework/boot/") - && hasEntry(file, "org/springframework/boot/loader/JarLauncher.class"); + && hasEntry(file, "org/springframework/boot/loader/launch/JarLauncher.class"); } private boolean hasEntry(File file, String name) throws IOException { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java new file mode 100644 index 000000000000..5beb8d109640 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2023 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.loader.launch; + +/** + * Repackaged {@link org.springframework.boot.loader.JarLauncher}. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public final class JarLauncher { + + private JarLauncher() { + } + + public static void main(String[] args) throws Exception { + org.springframework.boot.loader.JarLauncher.main(args); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java new file mode 100644 index 000000000000..d80fb0bb7105 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2023 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.loader.launch; + +/** + * Repackaged {@link org.springframework.boot.loader.PropertiesLauncher}. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public final class PropertiesLauncher { + + private PropertiesLauncher() { + } + + public static void main(String[] args) throws Exception { + org.springframework.boot.loader.PropertiesLauncher.main(args); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java new file mode 100644 index 000000000000..9392d3bf2b45 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2023 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.loader.launch; + +/** + * Repackaged {@link org.springframework.boot.loader.WarLauncher}. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public final class WarLauncher { + + private WarLauncher() { + } + + public static void main(String[] args) throws Exception { + org.springframework.boot.loader.WarLauncher.main(args); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/package-info.java new file mode 100644 index 000000000000..7968d509a2bb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Repackaged launcher classes. + * + * @see org.springframework.boot.loader.launch.JarLauncher + * @see org.springframework.boot.loader.launch.WarLauncher + */ +package org.springframework.boot.loader.launch; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java index 07eeaaa70bc7..1d37949cfaa9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java @@ -57,7 +57,7 @@ void whenJarIsRepackagedInPlaceOnlyRepackagedJarIsInstalled(MavenBuild mavenBuil File repackaged = new File(project, "target/jar-0.0.1.BUILD-SNAPSHOT.jar"); assertThat(launchScript(repackaged)).isEmpty(); assertThat(jar(repackaged)).manifest((manifest) -> { - manifest.hasMainClass("org.springframework.boot.loader.JarLauncher"); + manifest.hasMainClass("org.springframework.boot.loader.launch.JarLauncher"); manifest.hasStartClass("some.random.Main"); manifest.hasAttribute("Not-Used", "Foo"); }) @@ -66,7 +66,7 @@ void whenJarIsRepackagedInPlaceOnlyRepackagedJarIsInstalled(MavenBuild mavenBuil .hasEntryWithNameStartingWith("BOOT-INF/lib/spring-jcl") .hasEntryWithNameStartingWith("BOOT-INF/lib/jakarta.servlet-api-6") .hasEntryWithName("BOOT-INF/classes/org/test/SampleApplication.class") - .hasEntryWithName("org/springframework/boot/loader/JarLauncher.class"); + .hasEntryWithName("org/springframework/boot/loader/launch/JarLauncher.class"); assertThat(buildLog(project)) .contains("Replacing main artifact " + repackaged + " with repackaged archive,") .contains("The original artifact has been renamed to " + original) @@ -273,9 +273,9 @@ void whenAProjectIsBuiltWithALayoutPropertyTheSpecifiedLayoutIsUsed(MavenBuild m .goals("package", "-Dspring-boot.repackage.layout=ZIP") .execute((project) -> { File main = new File(project, "target/jar-with-layout-property-0.0.1.BUILD-SNAPSHOT.jar"); - assertThat(jar(main)) - .manifest((manifest) -> manifest.hasMainClass("org.springframework.boot.loader.PropertiesLauncher") - .hasStartClass("org.test.SampleApplication")); + assertThat(jar(main)).manifest( + (manifest) -> manifest.hasMainClass("org.springframework.boot.loader.launch.PropertiesLauncher") + .hasStartClass("org.test.SampleApplication")); assertThat(buildLog(project)).contains("Layout: ZIP"); }); } @@ -284,9 +284,9 @@ void whenAProjectIsBuiltWithALayoutPropertyTheSpecifiedLayoutIsUsed(MavenBuild m void whenALayoutIsConfiguredTheSpecifiedLayoutIsUsed(MavenBuild mavenBuild) { mavenBuild.project("jar-with-zip-layout").execute((project) -> { File main = new File(project, "target/jar-with-zip-layout-0.0.1.BUILD-SNAPSHOT.jar"); - assertThat(jar(main)) - .manifest((manifest) -> manifest.hasMainClass("org.springframework.boot.loader.PropertiesLauncher") - .hasStartClass("org.test.SampleApplication")); + assertThat(jar(main)).manifest( + (manifest) -> manifest.hasMainClass("org.springframework.boot.loader.launch.PropertiesLauncher") + .hasStartClass("org.test.SampleApplication")); assertThat(buildLog(project)).contains("Layout: ZIP"); }); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java index 0a8894cb6bfc..e7cebf3c576b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/WarIntegrationTests.java @@ -57,10 +57,10 @@ void warRepackaging(MavenBuild mavenBuild) { .hasEntryWithNameStartingWith("WEB-INF/lib/spring-core") .hasEntryWithNameStartingWith("WEB-INF/lib/spring-jcl") .hasEntryWithNameStartingWith("WEB-INF/lib-provided/jakarta.servlet-api-6") - .hasEntryWithName("org/springframework/boot/loader/WarLauncher.class") + .hasEntryWithName("org/springframework/boot/loader/launch/WarLauncher.class") .hasEntryWithName("WEB-INF/classes/org/test/SampleApplication.class") .hasEntryWithName("index.html") - .manifest((manifest) -> manifest.hasMainClass("org.springframework.boot.loader.WarLauncher") + .manifest((manifest) -> manifest.hasMainClass("org.springframework.boot.loader.launch.WarLauncher") .hasStartClass("org.test.SampleApplication") .hasAttribute("Not-Used", "Foo"))); } diff --git a/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java b/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java index ab376dd3ee6a..76e642d3f22f 100644 --- a/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java +++ b/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java @@ -93,9 +93,10 @@ void executableJarApp() throws Exception { .contains("paketo-buildpacks/ca-certificates", "paketo-buildpacks/bellsoft-liberica", "paketo-buildpacks/executable-jar", "paketo-buildpacks/dist-zip", "paketo-buildpacks/spring-boot"); - metadata.processOfType("web").containsExactly("java", "org.springframework.boot.loader.JarLauncher"); + metadata.processOfType("web") + .containsExactly("java", "org.springframework.boot.loader.launch.launch.JarLauncher"); metadata.processOfType("executable-jar") - .containsExactly("java", "org.springframework.boot.loader.JarLauncher"); + .containsExactly("java", "org.springframework.boot.loader.launch.launch.JarLauncher"); }); assertImageHasJvmSbomLayer(imageReference, config); assertImageHasDependenciesSbomLayer(imageReference, config, "executable-jar"); @@ -238,9 +239,10 @@ void executableWarApp() throws Exception { .contains("paketo-buildpacks/ca-certificates", "paketo-buildpacks/bellsoft-liberica", "paketo-buildpacks/executable-jar", "paketo-buildpacks/dist-zip", "paketo-buildpacks/spring-boot"); - metadata.processOfType("web").containsExactly("java", "org.springframework.boot.loader.WarLauncher"); + metadata.processOfType("web") + .containsExactly("java", "org.springframework.boot.loader.launch.WarLauncher"); metadata.processOfType("executable-jar") - .containsExactly("java", "org.springframework.boot.loader.WarLauncher"); + .containsExactly("java", "org.springframework.boot.loader.launch.WarLauncher"); }); assertImageHasJvmSbomLayer(imageReference, config); assertImageHasDependenciesSbomLayer(imageReference, config, "executable-jar"); diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/intTest/java/org/springframework/boot/context/embedded/ExplodedApplicationLauncher.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/intTest/java/org/springframework/boot/context/embedded/ExplodedApplicationLauncher.java index dc8fb37a0cb9..b066ac9be081 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/intTest/java/org/springframework/boot/context/embedded/ExplodedApplicationLauncher.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/intTest/java/org/springframework/boot/context/embedded/ExplodedApplicationLauncher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -55,8 +55,8 @@ protected String getDescription(String packaging) { @Override protected List getArguments(File archive, File serverPortFile) { - String mainClass = (archive.getName().endsWith(".war") ? "org.springframework.boot.loader.WarLauncher" - : "org.springframework.boot.loader.JarLauncher"); + String mainClass = (archive.getName().endsWith(".war") ? "org.springframework.boot.loader.launch.WarLauncher" + : "org.springframework.boot.loader.launch.JarLauncher"); try { explodeArchive(archive); return Arrays.asList("-cp", this.exploded.getAbsolutePath(), mainClass, serverPortFile.getAbsolutePath()); diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/build.xml b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/build.xml index 418a7501f05f..091e4aa11678 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/build.xml +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/build.xml @@ -67,7 +67,7 @@ - + From aeb6537f57d00cdf47b948067c8a3421a71e72d8 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 18 Sep 2023 13:41:31 -0700 Subject: [PATCH 0521/1215] Rename spring-boot-loader to spring-boot-loader-classic Rename the `spring-boot-loader` module to `spring-boot-loader-classic` so that we can introduce an alternative loader implementation. See gh-37669 --- eclipse/spring-boot-project.setup | 2 +- settings.gradle | 4 ++-- .../spring-boot-dependencies/build.gradle | 2 +- .../spring-boot-antlib/build.gradle | 2 +- .../org/springframework/boot/ant/antlib.xml | 4 ++-- .../spring-boot-tools/spring-boot-cli/build.gradle | 2 +- .../gradle/tasks/bundling/LoaderZipEntries.java | 2 +- .../spring-boot-jarmode-layertools/build.gradle | 2 +- .../build.gradle | 2 +- .../boot/loader/ClassPathIndexFile.java | 2 +- .../boot/loader/ExecutableArchiveLauncher.java | 2 +- .../springframework/boot/loader/JarLauncher.java | 2 +- .../boot/loader/LaunchedURLClassLoader.java | 2 +- .../org/springframework/boot/loader/Launcher.java | 2 +- .../boot/loader/MainMethodRunner.java | 2 +- .../boot/loader/PropertiesLauncher.java | 0 .../springframework/boot/loader/WarLauncher.java | 2 +- .../boot/loader/archive/Archive.java | 2 +- .../boot/loader/archive/ExplodedArchive.java | 2 +- .../boot/loader/archive/JarFileArchive.java | 2 +- .../boot/loader/archive/package-info.java | 2 +- .../boot/loader/data/RandomAccessData.java | 2 +- .../boot/loader/data/RandomAccessDataFile.java | 0 .../boot/loader/data/package-info.java | 2 +- .../boot/loader/jar/AbstractJarFile.java | 2 +- .../springframework/boot/loader/jar/AsciiBytes.java | 2 +- .../org/springframework/boot/loader/jar/Bytes.java | 2 +- .../boot/loader/jar/CentralDirectoryEndRecord.java | 2 +- .../boot/loader/jar/CentralDirectoryFileHeader.java | 0 .../boot/loader/jar/CentralDirectoryParser.java | 2 +- .../boot/loader/jar/CentralDirectoryVisitor.java | 2 +- .../springframework/boot/loader/jar/FileHeader.java | 2 +- .../springframework/boot/loader/jar/Handler.java | 2 +- .../springframework/boot/loader/jar/JarEntry.java | 2 +- .../boot/loader/jar/JarEntryCertification.java | 2 +- .../boot/loader/jar/JarEntryFilter.java | 2 +- .../springframework/boot/loader/jar/JarFile.java | 2 +- .../boot/loader/jar/JarFileEntries.java | 0 .../boot/loader/jar/JarFileWrapper.java | 2 +- .../boot/loader/jar/JarURLConnection.java | 2 +- .../boot/loader/jar/StringSequence.java | 2 +- .../boot/loader/jar/ZipInflaterInputStream.java | 2 +- .../boot/loader/jar/package-info.java | 2 +- .../boot/loader/jarmode/JarMode.java | 2 +- .../boot/loader/jarmode/JarModeLauncher.java | 2 +- .../boot/loader/jarmode/TestJarMode.java | 2 +- .../boot/loader/jarmode/package-info.java | 2 +- .../boot/loader/launch/JarLauncher.java | 0 .../boot/loader/launch/PropertiesLauncher.java | 0 .../boot/loader/launch/WarLauncher.java | 0 .../boot/loader/launch/package-info.java | 0 .../springframework/boot/loader/package-info.java | 2 +- .../boot/loader/util/SystemPropertyUtils.java | 2 +- .../boot/loader/util/package-info.java | 2 +- .../AbstractExecutableArchiveLauncherTests.java | 2 +- .../boot/loader/ClassPathIndexFileTests.java | 0 .../boot/loader/JarLauncherTests.java | 2 +- .../boot/loader/LaunchedURLClassLoaderTests.java | 0 .../boot/loader/PropertiesLauncherTests.java | 0 .../springframework/boot/loader/TestJarCreator.java | 2 +- .../boot/loader/WarLauncherTests.java | 2 +- .../boot/loader/archive/ExplodedArchiveTests.java | 0 .../boot/loader/archive/JarFileArchiveTests.java | 0 .../boot/loader/data/RandomAccessDataFileTests.java | 0 .../boot/loader/jar/AsciiBytesTests.java | 0 .../loader/jar/CentralDirectoryParserTests.java | 0 .../boot/loader/jar/HandlerTests.java | 0 .../boot/loader/jar/JarFileTests.java | 0 .../boot/loader/jar/JarFileWrapperTests.java | 0 .../boot/loader/jar/JarURLConnectionTests.java | 0 .../boot/loader/jar/JarUrlProtocolHandler.java | 0 .../boot/loader/jar/StringSequenceTests.java | 0 .../boot/loader/jarmode/LauncherJarModeTests.java | 2 +- .../boot/loader/util/SystemPropertyUtilsTests.java | 2 +- .../BOOT-INF/classes/application.properties | 0 .../test/resources/BOOT-INF/classes/bar.properties | 0 .../test/resources/BOOT-INF/classes/foo.properties | 0 .../resources/BOOT-INF/classes/loader.properties | 0 .../src/test/resources/META-INF/spring.factories | 0 .../src/test/resources/bar.properties | 0 .../test/resources/explodedsample/ExampleClass.txt | 0 .../src/test/resources/home/loader.properties | 0 .../src/test/resources/jars/app.jar | Bin .../src/test/resources/more-jars/app.jar | Bin .../src/test/resources/nested-jars/app.jar | Bin .../test/resources/nested-jars/nested-jar-app.jar | Bin .../boot/loader/classpath-index-file.idx | 0 .../resources/placeholders/META-INF/MANIFEST.MF | 0 .../test/resources/placeholders/loader.properties | 0 .../src/test/resources/root/META-INF/MANIFEST.MF | 0 .../resources/root/META-INF/spring/application.xml | 0 .../spring-boot-loader-tools/build.gradle | 8 ++++---- .../boot/loader/tools/AbstractJarWriter.java | 2 +- .../build.gradle | 8 ++++---- .../build.gradle | 0 .../settings.gradle | 0 .../boot/loaderapp/LoaderTestApplication.java | 0 .../boot/loader/LoaderIntegrationTests.java | 2 +- .../intTest/resources/conf/oracle-jdk-17/Dockerfile | 0 .../resources/conf/oracle-jdk-17/Dockerfile-aarch64 | 0 .../resources/conf/oracle-jdk-17/README.adoc | 0 .../src/intTest/resources/logback.xml | 0 .../spring-boot-smoke-test-ant/build.gradle | 2 +- .../spring-boot-smoke-test-ant/build.xml | 2 +- .../spring-boot-smoke-test-ant/ivy.xml | 2 +- 105 files changed, 67 insertions(+), 67 deletions(-) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/build.gradle (94%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java (98%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/JarLauncher.java (97%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/Launcher.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/MainMethodRunner.java (96%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/WarLauncher.java (96%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/archive/Archive.java (98%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/archive/package-info.java (93%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java (97%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/data/package-info.java (93%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java (97%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/Bytes.java (94%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java (98%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java (94%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/FileHeader.java (96%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/Handler.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/JarEntry.java (98%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java (97%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java (95%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/JarFile.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java (98%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/StringSequence.java (98%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java (97%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jar/package-info.java (92%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java (95%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java (96%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java (94%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/jarmode/package-info.java (93%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/launch/package-info.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/package-info.java (95%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/main/java/org/springframework/boot/loader/util/package-info.java (92%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java (98%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/JarLauncherTests.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/TestJarCreator.java (99%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/WarLauncherTests.java (98%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/jar/JarUrlProtocolHandler.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java (97%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java (96%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/BOOT-INF/classes/application.properties (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/BOOT-INF/classes/bar.properties (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/BOOT-INF/classes/foo.properties (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/BOOT-INF/classes/loader.properties (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/META-INF/spring.factories (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/bar.properties (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/explodedsample/ExampleClass.txt (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/home/loader.properties (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/jars/app.jar (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/more-jars/app.jar (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/nested-jars/app.jar (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/nested-jars/nested-jar-app.jar (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/placeholders/META-INF/MANIFEST.MF (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/placeholders/loader.properties (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/root/META-INF/MANIFEST.MF (100%) rename spring-boot-project/spring-boot-tools/{spring-boot-loader => spring-boot-loader-classic}/src/test/resources/root/META-INF/spring/application.xml (100%) rename spring-boot-tests/spring-boot-integration-tests/{spring-boot-loader-tests => spring-boot-loader-classic-tests}/build.gradle (82%) rename spring-boot-tests/spring-boot-integration-tests/{spring-boot-loader-tests/spring-boot-loader-tests-app => spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app}/build.gradle (100%) rename spring-boot-tests/spring-boot-integration-tests/{spring-boot-loader-tests/spring-boot-loader-tests-app => spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app}/settings.gradle (100%) rename spring-boot-tests/spring-boot-integration-tests/{spring-boot-loader-tests/spring-boot-loader-tests-app => spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app}/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java (100%) rename spring-boot-tests/spring-boot-integration-tests/{spring-boot-loader-tests => spring-boot-loader-classic-tests}/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java (99%) rename spring-boot-tests/spring-boot-integration-tests/{spring-boot-loader-tests => spring-boot-loader-classic-tests}/src/intTest/resources/conf/oracle-jdk-17/Dockerfile (100%) rename spring-boot-tests/spring-boot-integration-tests/{spring-boot-loader-tests => spring-boot-loader-classic-tests}/src/intTest/resources/conf/oracle-jdk-17/Dockerfile-aarch64 (100%) rename spring-boot-tests/spring-boot-integration-tests/{spring-boot-loader-tests => spring-boot-loader-classic-tests}/src/intTest/resources/conf/oracle-jdk-17/README.adoc (100%) rename spring-boot-tests/spring-boot-integration-tests/{spring-boot-loader-tests => spring-boot-loader-classic-tests}/src/intTest/resources/logback.xml (100%) diff --git a/eclipse/spring-boot-project.setup b/eclipse/spring-boot-project.setup index 90ea0c29a199..0e060372d33f 100644 --- a/eclipse/spring-boot-project.setup +++ b/eclipse/spring-boot-project.setup @@ -136,7 +136,7 @@ name="spring-boot-tools"> + pattern="spring-boot-(tools|antlib|configuration-.*|loader|loader-classic|.*-tools|.*-layertools|.*-plugin|autoconfigure-processor|buildpack.*)"/> diff --git a/settings.gradle b/settings.gradle index 48b547ef1c4e..d5d771e915d9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -58,7 +58,7 @@ include "spring-boot-project:spring-boot-tools:spring-boot-configuration-process include "spring-boot-project:spring-boot-tools:spring-boot-gradle-plugin" include "spring-boot-project:spring-boot-tools:spring-boot-gradle-test-support" include "spring-boot-project:spring-boot-tools:spring-boot-jarmode-layertools" -include "spring-boot-project:spring-boot-tools:spring-boot-loader" +include "spring-boot-project:spring-boot-tools:spring-boot-loader-classic" include "spring-boot-project:spring-boot-tools:spring-boot-loader-tools" include "spring-boot-project:spring-boot-tools:spring-boot-maven-plugin" include "spring-boot-project:spring-boot-tools:spring-boot-properties-migrator" @@ -75,7 +75,7 @@ include "spring-boot-project:spring-boot-testcontainers" include "spring-boot-project:spring-boot-test-autoconfigure" include "spring-boot-tests:spring-boot-integration-tests:spring-boot-configuration-processor-tests" include "spring-boot-tests:spring-boot-integration-tests:spring-boot-launch-script-tests" -include "spring-boot-tests:spring-boot-integration-tests:spring-boot-loader-tests" +include "spring-boot-tests:spring-boot-integration-tests:spring-boot-loader-classic-tests" include "spring-boot-tests:spring-boot-integration-tests:spring-boot-server-tests" include "spring-boot-system-tests:spring-boot-deployment-tests" include "spring-boot-system-tests:spring-boot-image-tests" diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e07a36ed83be..c1ffd7a6615c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1380,7 +1380,7 @@ bom { "spring-boot-devtools", "spring-boot-docker-compose", "spring-boot-jarmode-layertools", - "spring-boot-loader", + "spring-boot-loader-classic", "spring-boot-loader-tools", "spring-boot-properties-migrator", "spring-boot-starter", diff --git a/spring-boot-project/spring-boot-tools/spring-boot-antlib/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-antlib/build.gradle index e00c34aeaf8e..750604b01944 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-antlib/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-antlib/build.gradle @@ -19,7 +19,7 @@ dependencies { antUnit "org.apache.ant:ant-antunit:1.3" antIvy "org.apache.ivy:ivy:2.5.0" - compileOnly(project(":spring-boot-project:spring-boot-tools:spring-boot-loader")) + compileOnly(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-classic")) compileOnly("org.apache.ant:ant:${antVersion}") implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-tools")) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/main/resources/org/springframework/boot/ant/antlib.xml b/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/main/resources/org/springframework/boot/ant/antlib.xml index 3a0d4902d9a1..980049c0cd2d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/main/resources/org/springframework/boot/ant/antlib.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-antlib/src/main/resources/org/springframework/boot/ant/antlib.xml @@ -42,7 +42,7 @@ Extracting spring-boot-loader to ${destdir}/dependency - @@ -58,7 +58,7 @@ - + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cli/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-cli/build.gradle index db81bc3e806e..bf42c6816213 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-cli/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-cli/build.gradle @@ -35,7 +35,7 @@ dependencies { intTestImplementation("org.junit.jupiter:junit-jupiter") intTestImplementation("org.springframework:spring-core") - loader(project(":spring-boot-project:spring-boot-tools:spring-boot-loader")) + loader(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-classic")) testImplementation(project(":spring-boot-project:spring-boot")) testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java index dd4d50894ff4..7606a3d66a2b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java @@ -54,7 +54,7 @@ class LoaderZipEntries { WrittenEntries writeTo(ZipArchiveOutputStream out) throws IOException { WrittenEntries written = new WrittenEntries(); try (ZipInputStream loaderJar = new ZipInputStream( - getClass().getResourceAsStream("/META-INF/loader/spring-boot-loader.jar"))) { + getClass().getResourceAsStream("/META-INF/loader/spring-boot-loader-classic.jar"))) { java.util.zip.ZipEntry entry = loaderJar.getNextEntry(); while (entry != null) { if (entry.isDirectory() && !entry.getName().equals("META-INF/")) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/build.gradle index 1f78242394e5..96d503924997 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/build.gradle @@ -7,7 +7,7 @@ plugins { description = "Spring Boot Layers Tools" dependencies { - implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-loader")) + implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-classic")) implementation("org.springframework:spring-core") testImplementation("org.assertj:assertj-core") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/build.gradle similarity index 94% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/build.gradle rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/build.gradle index 845fde0b6107..17d2a7b519a5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/build.gradle @@ -4,7 +4,7 @@ plugins { id "org.springframework.boot.deployed" } -description = "Spring Boot Loader" +description = "Spring Boot Classic Loader" dependencies { compileOnly("org.springframework:spring-core") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java similarity index 98% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java index c08407941b3a..5ad01e507127 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java index 91b84b1140de..d2ceaf61c565 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/JarLauncher.java similarity index 97% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/JarLauncher.java index 2c86b3d41f43..5061573e2460 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/JarLauncher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java index 8c7cf98ae130..7e3e2fa22392 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/Launcher.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/Launcher.java index f83f685d24f7..2f4cac944408 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/Launcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/MainMethodRunner.java similarity index 96% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/MainMethodRunner.java index 9b7a551a8b6d..12355a2bef46 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/MainMethodRunner.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/WarLauncher.java similarity index 96% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/WarLauncher.java index 81e0a744144c..482832c1f722 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/WarLauncher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/Archive.java similarity index 98% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/Archive.java index a99c1c2c229b..c1f2bbb2f75b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/Archive.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java index 08734078520c..f8cd52dc16f1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java index b30c8bb37a52..91e7bc53a486 100755 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/package-info.java similarity index 93% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/package-info.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/package-info.java index ebaca84bb95d..27ce99b006f0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/archive/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java similarity index 97% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java index ec1aa5e4a1e8..e96d5ea81a05 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/data/package-info.java similarity index 93% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/package-info.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/data/package-info.java index 8f456bd685dc..34bf2ead4378 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/data/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java similarity index 97% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java index 88726e373754..6a98ef682189 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java index 4b6e2678b3ec..cfe121b68996 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/Bytes.java similarity index 94% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/Bytes.java index 7f53bac6297f..d46a22555dcb 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/Bytes.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java index b971b590abd1..61db0b73f422 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java similarity index 98% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java index 71a767853561..eff96a56e2cc 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java similarity index 94% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java index d160cbf84772..22e04b329c30 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/FileHeader.java similarity index 96% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/FileHeader.java index 4b8de5008cf4..7e4134fe5649 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/FileHeader.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/Handler.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/Handler.java index 67bf8048f046..932dea654867 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/Handler.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarEntry.java similarity index 98% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarEntry.java index 5b8f3bedb20b..8f54dc3070df 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarEntry.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java similarity index 97% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java index cbf66412e215..ffd629e09428 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java similarity index 95% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java index 98ed4b905e5c..6804f0ba37f9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarFile.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarFile.java index 502c450fa738..6e548048dbf0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarFile.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java similarity index 98% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java index 0a3bf030a5e0..b65358947ad1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java index c9286b3e8b58..859ae88ab000 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/StringSequence.java similarity index 98% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/StringSequence.java index 9e6af077ed99..12850a4ebe3e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/StringSequence.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java similarity index 97% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java index 87587bed3ff1..67624460ccd7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/package-info.java similarity index 92% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/package-info.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/package-info.java index e232261ff47e..638afe45f497 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jar/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java similarity index 95% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java index c711e206f5da..162e4a6a7396 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java similarity index 96% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java index 442488934103..600266a241be 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java similarity index 94% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java index 6a6e83ff23c4..2e17175690a5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/package-info.java similarity index 93% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/package-info.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/package-info.java index 315cb5696b83..2f3b5a74e8fd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/launch/package-info.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/package-info.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/launch/package-info.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/package-info.java similarity index 95% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/package-info.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/package-info.java index c2114c2d83bb..4b32f644f542 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java index b6f0e3a3a7fb..df00705e9eec 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/util/package-info.java similarity index 92% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/package-info.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/util/package-info.java index af0aa2d1a7dc..d3d7eef2d9db 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/util/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java similarity index 98% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java index 48d7340ee384..60e3cb2765eb 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/JarLauncherTests.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/JarLauncherTests.java index fa713034304a..afa32a7c4f18 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/JarLauncherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/TestJarCreator.java similarity index 99% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/TestJarCreator.java index 100e2c757e37..c5c5fd3b95c9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/TestJarCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/WarLauncherTests.java similarity index 98% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/WarLauncherTests.java index aef78cfa53d9..fbab8d36ed0a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/WarLauncherTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarUrlProtocolHandler.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarUrlProtocolHandler.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarUrlProtocolHandler.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarUrlProtocolHandler.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java similarity index 97% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java index 93179bad6fe2..dec587e18bb2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java similarity index 96% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java index 0697b77b7bba..802a762e79dd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/application.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/BOOT-INF/classes/application.properties similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/application.properties rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/BOOT-INF/classes/application.properties diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/bar.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/BOOT-INF/classes/bar.properties similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/bar.properties rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/BOOT-INF/classes/bar.properties diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/foo.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/BOOT-INF/classes/foo.properties similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/foo.properties rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/BOOT-INF/classes/foo.properties diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/loader.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/BOOT-INF/classes/loader.properties similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/loader.properties rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/BOOT-INF/classes/loader.properties diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/META-INF/spring.factories similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/META-INF/spring.factories rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/META-INF/spring.factories diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/bar.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/bar.properties similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/bar.properties rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/bar.properties diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/explodedsample/ExampleClass.txt b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/explodedsample/ExampleClass.txt similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/explodedsample/ExampleClass.txt rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/explodedsample/ExampleClass.txt diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/home/loader.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/home/loader.properties similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/home/loader.properties rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/home/loader.properties diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/jars/app.jar b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/jars/app.jar similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/jars/app.jar rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/jars/app.jar diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/more-jars/app.jar b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/more-jars/app.jar similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/more-jars/app.jar rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/more-jars/app.jar diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/app.jar b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/nested-jars/app.jar similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/app.jar rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/nested-jars/app.jar diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/nested-jar-app.jar b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/nested-jars/nested-jar-app.jar similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/nested-jar-app.jar rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/nested-jars/nested-jar-app.jar diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/META-INF/MANIFEST.MF b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/placeholders/META-INF/MANIFEST.MF similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/META-INF/MANIFEST.MF rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/placeholders/META-INF/MANIFEST.MF diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/loader.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/placeholders/loader.properties similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/loader.properties rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/placeholders/loader.properties diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/MANIFEST.MF b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/root/META-INF/MANIFEST.MF similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/MANIFEST.MF rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/root/META-INF/MANIFEST.MF diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/spring/application.xml b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/root/META-INF/spring/application.xml similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/spring/application.xml rename to spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/resources/root/META-INF/spring/application.xml diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle index 755f1cc7bc73..80311be0acb7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle @@ -36,7 +36,7 @@ dependencies { compileOnly("ch.qos.logback:logback-classic") - loader(project(":spring-boot-project:spring-boot-tools:spring-boot-loader")) + loader(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-classic")) jarmode(project(":spring-boot-project:spring-boot-tools:spring-boot-jarmode-layertools")) @@ -57,7 +57,7 @@ task reproducibleLoaderJar(type: Jar) { } reproducibleFileOrder = true preserveFileTimestamps = false - archiveFileName = "spring-boot-loader.jar" + archiveFileName = "spring-boot-loader-classic.jar" destinationDirectory = file("${generatedResources}/META-INF/loader") } @@ -78,6 +78,6 @@ sourceSets { compileJava { if ((!project.hasProperty("toolchainVersion")) && JavaVersion.current() == JavaVersion.VERSION_1_8) { - options.compilerArgs += ['-Xlint:-sunapi', '-XDenableSunApiLintControl'] - } + options.compilerArgs += ['-Xlint:-sunapi', '-XDenableSunApiLintControl'] + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java index ffbdf5ec73b9..23282dbea729 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java @@ -51,7 +51,7 @@ */ public abstract class AbstractJarWriter implements LoaderClassesWriter { - private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader.jar"; + private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader-classic.jar"; private static final int BUFFER_SIZE = 32 * 1024; diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/build.gradle similarity index 82% rename from spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/build.gradle rename to spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/build.gradle index 7c4095f73b1a..d05a3d6c9e09 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/build.gradle +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/build.gradle @@ -4,7 +4,7 @@ plugins { id "org.springframework.boot.integration-test" } -description = "Spring Boot Loader Integration Tests" +description = "Spring Boot Classic Loader Integration Tests" configurations { app @@ -28,13 +28,13 @@ task syncMavenRepository(type: Sync) { } task syncAppSource(type: org.springframework.boot.build.SyncAppSource) { - sourceDirectory = file("spring-boot-loader-tests-app") - destinationDirectory = file("${buildDir}/spring-boot-loader-tests-app") + sourceDirectory = file("spring-boot-loader-classic-tests-app") + destinationDirectory = file("${buildDir}/spring-boot-loader-classic-tests-app") } task buildApp(type: GradleBuild) { dependsOn syncAppSource, syncMavenRepository - dir = "${buildDir}/spring-boot-loader-tests-app" + dir = "${buildDir}/spring-boot-loader-classic-tests-app" startParameter.buildCacheEnabled = false tasks = ["build"] } diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/build.gradle similarity index 100% rename from spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/build.gradle rename to spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/build.gradle diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/settings.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/settings.gradle similarity index 100% rename from spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/settings.gradle rename to spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/settings.gradle diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java similarity index 100% rename from spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java rename to spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java similarity index 99% rename from spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java rename to spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java index 9acac3f61da1..b11478b61c7f 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java @@ -71,7 +71,7 @@ private GenericContainer createContainer(JavaRuntime javaRuntime) { } private File findApplication() { - String name = String.format("build/%1$s/build/libs/%1$s.jar", "spring-boot-loader-tests-app"); + String name = String.format("build/%1$s/build/libs/%1$s.jar", "spring-boot-loader-classic-tests-app"); File jar = new File(name); Assert.state(jar.isFile(), () -> "Could not find " + name + ". Have you built it?"); return jar; diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile similarity index 100% rename from spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile rename to spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile-aarch64 b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile-aarch64 similarity index 100% rename from spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile-aarch64 rename to spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile-aarch64 diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/README.adoc b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/src/intTest/resources/conf/oracle-jdk-17/README.adoc similarity index 100% rename from spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/README.adoc rename to spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/src/intTest/resources/conf/oracle-jdk-17/README.adoc diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/logback.xml b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/src/intTest/resources/logback.xml similarity index 100% rename from spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/logback.xml rename to spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/src/intTest/resources/logback.xml diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/build.gradle index 7ff1740185fb..7f90497a4ff2 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/build.gradle @@ -30,7 +30,7 @@ dependencies { antDependencies "org.apache.ant:ant-launcher:1.10.7" antDependencies "org.apache.ant:ant:1.10.7" - testRepository(project(path: ":spring-boot-project:spring-boot-tools:spring-boot-loader", configuration: "mavenRepository")) + testRepository(project(path: ":spring-boot-project:spring-boot-tools:spring-boot-loader-classic", configuration: "mavenRepository")) testRepository(project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter", configuration: "mavenRepository")) testImplementation(project(path: ":spring-boot-project:spring-boot-tools:spring-boot-loader-tools")) diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/build.xml b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/build.xml index 091e4aa11678..a03067231cef 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/build.xml +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/build.xml @@ -65,7 +65,7 @@ - + diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/ivy.xml b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/ivy.xml index 192d5281fcda..2ecb5cc31a2b 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/ivy.xml +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-ant/ivy.xml @@ -7,6 +7,6 @@ - + From a89057b7c7ccece78b16152dfc80b75b17476b35 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 18 Sep 2023 13:53:58 -0700 Subject: [PATCH 0522/1215] Reintroduce spring-boot-loader modules Restore the `spring-boot-loader` with the previous loader code so that we can develop it further. See gh-37669 --- settings.gradle | 2 + .../spring-boot-dependencies/build.gradle | 1 + .../spring-boot-loader/build.gradle | 23 + .../boot/loader/ClassPathIndexFile.java | 123 +++ .../loader/ExecutableArchiveLauncher.java | 207 +++++ .../boot/loader/JarLauncher.java | 68 ++ .../boot/loader/LaunchedURLClassLoader.java | 366 +++++++++ .../springframework/boot/loader/Launcher.java | 159 ++++ .../boot/loader/MainMethodRunner.java | 52 ++ .../boot/loader/PropertiesLauncher.java | 726 +++++++++++++++++ .../boot/loader/WarLauncher.java | 62 ++ .../boot/loader/archive/Archive.java | 115 +++ .../boot/loader/archive/ExplodedArchive.java | 342 ++++++++ .../boot/loader/archive/JarFileArchive.java | 310 ++++++++ .../boot/loader/archive/package-info.java | 23 + .../boot/loader/data/RandomAccessData.java | 74 ++ .../loader/data/RandomAccessDataFile.java | 262 +++++++ .../boot/loader/data/package-info.java | 22 + .../boot/loader/jar/AbstractJarFile.java | 78 ++ .../boot/loader/jar/AsciiBytes.java | 255 ++++++ .../boot/loader/jar/Bytes.java | 37 + .../loader/jar/CentralDirectoryEndRecord.java | 258 ++++++ .../jar/CentralDirectoryFileHeader.java | 222 ++++++ .../loader/jar/CentralDirectoryParser.java | 101 +++ .../loader/jar/CentralDirectoryVisitor.java | 34 + .../boot/loader/jar/FileHeader.java | 64 ++ .../boot/loader/jar/Handler.java | 466 +++++++++++ .../boot/loader/jar/JarEntry.java | 120 +++ .../loader/jar/JarEntryCertification.java | 58 ++ .../boot/loader/jar/JarEntryFilter.java | 35 + .../boot/loader/jar/JarFile.java | 475 +++++++++++ .../boot/loader/jar/JarFileEntries.java | 491 ++++++++++++ .../boot/loader/jar/JarFileWrapper.java | 126 +++ .../boot/loader/jar/JarURLConnection.java | 393 ++++++++++ .../boot/loader/jar/StringSequence.java | 157 ++++ .../loader/jar/ZipInflaterInputStream.java | 88 +++ .../boot/loader/jar/package-info.java | 20 + .../boot/loader/jarmode/JarMode.java | 42 + .../boot/loader/jarmode/JarModeLauncher.java | 53 ++ .../boot/loader/jarmode/TestJarMode.java | 38 + .../boot/loader/jarmode/package-info.java | 22 + .../boot/loader/launch/JarLauncher.java | 34 + .../loader/launch/PropertiesLauncher.java | 34 + .../boot/loader/launch/WarLauncher.java | 34 + .../boot/loader/launch/package-info.java | 23 + .../boot/loader/package-info.java | 26 + .../boot/loader/util/SystemPropertyUtils.java | 232 ++++++ .../boot/loader/util/package-info.java | 20 + ...bstractExecutableArchiveLauncherTests.java | 149 ++++ .../boot/loader/ClassPathIndexFileTests.java | 109 +++ .../boot/loader/JarLauncherTests.java | 154 ++++ .../loader/LaunchedURLClassLoaderTests.java | 111 +++ .../boot/loader/PropertiesLauncherTests.java | 433 +++++++++++ .../boot/loader/TestJarCreator.java | 151 ++++ .../boot/loader/WarLauncherTests.java | 121 +++ .../loader/archive/ExplodedArchiveTests.java | 189 +++++ .../loader/archive/JarFileArchiveTests.java | 207 +++++ .../data/RandomAccessDataFileTests.java | 300 +++++++ .../boot/loader/jar/AsciiBytesTests.java | 196 +++++ .../jar/CentralDirectoryParserTests.java | 139 ++++ .../boot/loader/jar/HandlerTests.java | 210 +++++ .../boot/loader/jar/JarFileTests.java | 736 ++++++++++++++++++ .../boot/loader/jar/JarFileWrapperTests.java | 281 +++++++ .../loader/jar/JarURLConnectionTests.java | 246 ++++++ .../loader/jar/JarUrlProtocolHandler.java | 57 ++ .../boot/loader/jar/StringSequenceTests.java | 220 ++++++ .../loader/jarmode/LauncherJarModeTests.java | 86 ++ .../loader/util/SystemPropertyUtilsTests.java | 62 ++ .../BOOT-INF/classes/application.properties | 1 + .../resources/BOOT-INF/classes/bar.properties | 1 + .../resources/BOOT-INF/classes/foo.properties | 3 + .../BOOT-INF/classes/loader.properties | 1 + .../test/resources/META-INF/spring.factories | 3 + .../src/test/resources/bar.properties | 1 + .../resources/explodedsample/ExampleClass.txt | 26 + .../src/test/resources/home/loader.properties | 1 + .../src/test/resources/jars/app.jar | Bin 0 -> 2213 bytes .../src/test/resources/more-jars/app.jar | Bin 0 -> 1150 bytes .../src/test/resources/nested-jars/app.jar | Bin 0 -> 3313 bytes .../resources/nested-jars/nested-jar-app.jar | Bin 0 -> 1408 bytes .../boot/loader/classpath-index-file.idx | 5 + .../placeholders/META-INF/MANIFEST.MF | 2 + .../resources/placeholders/loader.properties | 1 + .../test/resources/root/META-INF/MANIFEST.MF | 1 + .../root/META-INF/spring/application.xml | 6 + .../spring-boot-loader-tests/build.gradle | 44 ++ .../spring-boot-loader-tests-app/build.gradle | 18 + .../settings.gradle | 15 + .../boot/loaderapp/LoaderTestApplication.java | 59 ++ .../boot/loader/LoaderIntegrationTests.java | 139 ++++ .../resources/conf/oracle-jdk-17/Dockerfile | 8 + .../conf/oracle-jdk-17/Dockerfile-aarch64 | 8 + .../resources/conf/oracle-jdk-17/README.adoc | 5 + .../src/intTest/resources/logback.xml | 4 + 94 files changed, 11482 insertions(+) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/build.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java create mode 100755 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java create mode 100755 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java create mode 100755 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java create mode 100755 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarUrlProtocolHandler.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/application.properties create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/bar.properties create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/foo.properties create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/loader.properties create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/META-INF/spring.factories create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/bar.properties create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/explodedsample/ExampleClass.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/home/loader.properties create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/jars/app.jar create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/more-jars/app.jar create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/app.jar create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/nested-jar-app.jar create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/META-INF/MANIFEST.MF create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/loader.properties create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/MANIFEST.MF create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/spring/application.xml create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/build.gradle create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/build.gradle create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/settings.gradle create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile-aarch64 create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/README.adoc create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/logback.xml diff --git a/settings.gradle b/settings.gradle index d5d771e915d9..9dc4ab6ee40d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -58,6 +58,7 @@ include "spring-boot-project:spring-boot-tools:spring-boot-configuration-process include "spring-boot-project:spring-boot-tools:spring-boot-gradle-plugin" include "spring-boot-project:spring-boot-tools:spring-boot-gradle-test-support" include "spring-boot-project:spring-boot-tools:spring-boot-jarmode-layertools" +include "spring-boot-project:spring-boot-tools:spring-boot-loader" include "spring-boot-project:spring-boot-tools:spring-boot-loader-classic" include "spring-boot-project:spring-boot-tools:spring-boot-loader-tools" include "spring-boot-project:spring-boot-tools:spring-boot-maven-plugin" @@ -75,6 +76,7 @@ include "spring-boot-project:spring-boot-testcontainers" include "spring-boot-project:spring-boot-test-autoconfigure" include "spring-boot-tests:spring-boot-integration-tests:spring-boot-configuration-processor-tests" include "spring-boot-tests:spring-boot-integration-tests:spring-boot-launch-script-tests" +include "spring-boot-tests:spring-boot-integration-tests:spring-boot-loader-tests" include "spring-boot-tests:spring-boot-integration-tests:spring-boot-loader-classic-tests" include "spring-boot-tests:spring-boot-integration-tests:spring-boot-server-tests" include "spring-boot-system-tests:spring-boot-deployment-tests" diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c1ffd7a6615c..3f7bac1a79b3 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1380,6 +1380,7 @@ bom { "spring-boot-devtools", "spring-boot-docker-compose", "spring-boot-jarmode-layertools", + "spring-boot-loader", "spring-boot-loader-classic", "spring-boot-loader-tools", "spring-boot-properties-migrator", diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-loader/build.gradle new file mode 100644 index 000000000000..845fde0b6107 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/build.gradle @@ -0,0 +1,23 @@ +plugins { + id "java-library" + id "org.springframework.boot.conventions" + id "org.springframework.boot.deployed" +} + +description = "Spring Boot Loader" + +dependencies { + compileOnly("org.springframework:spring-core") + + testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + testImplementation("org.assertj:assertj-core") + testImplementation("org.awaitility:awaitility") + testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.mockito:mockito-core") + testImplementation("org.springframework:spring-test") + testImplementation("org.springframework:spring-core-test") + + testRuntimeOnly("ch.qos.logback:logback-classic") + testRuntimeOnly("org.bouncycastle:bcprov-jdk18on:1.71") + testRuntimeOnly("org.springframework:spring-webmvc") +} \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java new file mode 100644 index 000000000000..5ad01e507127 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java @@ -0,0 +1,123 @@ +/* + * Copyright 2012-2023 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.loader; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A class path index file that provides ordering information for JARs. + * + * @author Madhura Bhave + * @author Phillip Webb + */ +final class ClassPathIndexFile { + + private final File root; + + private final List lines; + + private ClassPathIndexFile(File root, List lines) { + this.root = root; + this.lines = lines.stream().map(this::extractName).toList(); + } + + private String extractName(String line) { + if (line.startsWith("- \"") && line.endsWith("\"")) { + return line.substring(3, line.length() - 1); + } + throw new IllegalStateException("Malformed classpath index line [" + line + "]"); + } + + int size() { + return this.lines.size(); + } + + boolean containsEntry(String name) { + if (name == null || name.isEmpty()) { + return false; + } + return this.lines.contains(name); + } + + List getUrls() { + return this.lines.stream().map(this::asUrl).toList(); + } + + private URL asUrl(String line) { + try { + return new File(this.root, line).toURI().toURL(); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + + static ClassPathIndexFile loadIfPossible(URL root, String location) throws IOException { + return loadIfPossible(asFile(root), location); + } + + private static ClassPathIndexFile loadIfPossible(File root, String location) throws IOException { + return loadIfPossible(root, new File(root, location)); + } + + private static ClassPathIndexFile loadIfPossible(File root, File indexFile) throws IOException { + if (indexFile.exists() && indexFile.isFile()) { + try (InputStream inputStream = new FileInputStream(indexFile)) { + return new ClassPathIndexFile(root, loadLines(inputStream)); + } + } + return null; + } + + private static List loadLines(InputStream inputStream) throws IOException { + List lines = new ArrayList<>(); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + String line = reader.readLine(); + while (line != null) { + if (!line.trim().isEmpty()) { + lines.add(line); + } + line = reader.readLine(); + } + return Collections.unmodifiableList(lines); + } + + private static File asFile(URL url) { + if (!"file".equals(url.getProtocol())) { + throw new IllegalArgumentException("URL does not reference a file"); + } + try { + return new File(url.toURI()); + } + catch (URISyntaxException ex) { + return new File(url.getPath()); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java new file mode 100644 index 000000000000..d2ceaf61c565 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java @@ -0,0 +1,207 @@ +/* + * Copyright 2012-2023 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.loader; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.archive.ExplodedArchive; + +/** + * Base class for executable archive {@link Launcher}s. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Madhura Bhave + * @author Scott Frederick + * @since 1.0.0 + */ +public abstract class ExecutableArchiveLauncher extends Launcher { + + private static final String START_CLASS_ATTRIBUTE = "Start-Class"; + + protected static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index"; + + protected static final String DEFAULT_CLASSPATH_INDEX_FILE_NAME = "classpath.idx"; + + private final Archive archive; + + private final ClassPathIndexFile classPathIndex; + + public ExecutableArchiveLauncher() { + try { + this.archive = createArchive(); + this.classPathIndex = getClassPathIndex(this.archive); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + protected ExecutableArchiveLauncher(Archive archive) { + try { + this.archive = archive; + this.classPathIndex = getClassPathIndex(this.archive); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException { + // Only needed for exploded archives, regular ones already have a defined order + if (archive instanceof ExplodedArchive) { + String location = getClassPathIndexFileLocation(archive); + return ClassPathIndexFile.loadIfPossible(archive.getUrl(), location); + } + return null; + } + + private String getClassPathIndexFileLocation(Archive archive) throws IOException { + Manifest manifest = archive.getManifest(); + Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null; + String location = (attributes != null) ? attributes.getValue(BOOT_CLASSPATH_INDEX_ATTRIBUTE) : null; + return (location != null) ? location : getArchiveEntryPathPrefix() + DEFAULT_CLASSPATH_INDEX_FILE_NAME; + } + + @Override + protected String getMainClass() throws Exception { + Manifest manifest = this.archive.getManifest(); + String mainClass = null; + if (manifest != null) { + mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE); + } + if (mainClass == null) { + throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this); + } + return mainClass; + } + + @Override + protected ClassLoader createClassLoader(Iterator archives) throws Exception { + List urls = new ArrayList<>(guessClassPathSize()); + while (archives.hasNext()) { + urls.add(archives.next().getUrl()); + } + if (this.classPathIndex != null) { + urls.addAll(this.classPathIndex.getUrls()); + } + return createClassLoader(urls.toArray(new URL[0])); + } + + private int guessClassPathSize() { + if (this.classPathIndex != null) { + return this.classPathIndex.size() + 10; + } + return 50; + } + + @Override + protected Iterator getClassPathArchivesIterator() throws Exception { + Archive.EntryFilter searchFilter = this::isSearchCandidate; + Iterator archives = this.archive.getNestedArchives(searchFilter, + (entry) -> isNestedArchive(entry) && !isEntryIndexed(entry)); + if (isPostProcessingClassPathArchives()) { + archives = applyClassPathArchivePostProcessing(archives); + } + return archives; + } + + private boolean isEntryIndexed(Archive.Entry entry) { + if (this.classPathIndex != null) { + return this.classPathIndex.containsEntry(entry.getName()); + } + return false; + } + + private Iterator applyClassPathArchivePostProcessing(Iterator archives) throws Exception { + List list = new ArrayList<>(); + while (archives.hasNext()) { + list.add(archives.next()); + } + postProcessClassPathArchives(list); + return list.iterator(); + } + + /** + * Determine if the specified entry is a candidate for further searching. + * @param entry the entry to check + * @return {@code true} if the entry is a candidate for further searching + * @since 2.3.0 + */ + protected boolean isSearchCandidate(Archive.Entry entry) { + if (getArchiveEntryPathPrefix() == null) { + return true; + } + return entry.getName().startsWith(getArchiveEntryPathPrefix()); + } + + /** + * Determine if the specified entry is a nested item that should be added to the + * classpath. + * @param entry the entry to check + * @return {@code true} if the entry is a nested item (jar or directory) + */ + protected abstract boolean isNestedArchive(Archive.Entry entry); + + /** + * Return if post-processing needs to be applied to the archives. For back + * compatibility this method returns {@code true}, but subclasses that don't override + * {@link #postProcessClassPathArchives(List)} should provide an implementation that + * returns {@code false}. + * @return if the {@link #postProcessClassPathArchives(List)} method is implemented + * @since 2.3.0 + */ + protected boolean isPostProcessingClassPathArchives() { + return true; + } + + /** + * Called to post-process archive entries before they are used. Implementations can + * add and remove entries. + * @param archives the archives + * @throws Exception if the post-processing fails + * @see #isPostProcessingClassPathArchives() + */ + protected void postProcessClassPathArchives(List archives) throws Exception { + } + + /** + * Return the path prefix for entries in the archive. + * @return the path prefix + */ + protected String getArchiveEntryPathPrefix() { + return null; + } + + @Override + protected boolean isExploded() { + return this.archive.isExploded(); + } + + @Override + protected final Archive getArchive() { + return this.archive; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java new file mode 100644 index 000000000000..5061573e2460 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2023 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.loader; + +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.archive.Archive.EntryFilter; + +/** + * {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are + * included inside a {@code /BOOT-INF/lib} directory and that application classes are + * included inside a {@code /BOOT-INF/classes} directory. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Madhura Bhave + * @author Scott Frederick + * @since 1.0.0 + */ +public class JarLauncher extends ExecutableArchiveLauncher { + + static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> { + if (entry.isDirectory()) { + return entry.getName().equals("BOOT-INF/classes/"); + } + return entry.getName().startsWith("BOOT-INF/lib/"); + }; + + public JarLauncher() { + } + + protected JarLauncher(Archive archive) { + super(archive); + } + + @Override + protected boolean isPostProcessingClassPathArchives() { + return false; + } + + @Override + protected boolean isNestedArchive(Archive.Entry entry) { + return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry); + } + + @Override + protected String getArchiveEntryPathPrefix() { + return "BOOT-INF/"; + } + + public static void main(String[] args) throws Exception { + new JarLauncher().launch(args); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java new file mode 100644 index 000000000000..7e3e2fa22392 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java @@ -0,0 +1,366 @@ +/* + * Copyright 2012-2023 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.loader; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLClassLoader; +import java.net.URLConnection; +import java.util.Enumeration; +import java.util.function.Supplier; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.jar.Handler; + +/** + * {@link ClassLoader} used by the {@link Launcher}. + * + * @author Phillip Webb + * @author Dave Syer + * @author Andy Wilkinson + * @since 1.0.0 + */ +public class LaunchedURLClassLoader extends URLClassLoader { + + private static final int BUFFER_SIZE = 4096; + + static { + ClassLoader.registerAsParallelCapable(); + } + + private final boolean exploded; + + private final Archive rootArchive; + + private final Object packageLock = new Object(); + + private volatile DefinePackageCallType definePackageCallType; + + /** + * Create a new {@link LaunchedURLClassLoader} instance. + * @param urls the URLs from which to load classes and resources + * @param parent the parent class loader for delegation + */ + public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) { + this(false, urls, parent); + } + + /** + * Create a new {@link LaunchedURLClassLoader} instance. + * @param exploded if the underlying archive is exploded + * @param urls the URLs from which to load classes and resources + * @param parent the parent class loader for delegation + */ + public LaunchedURLClassLoader(boolean exploded, URL[] urls, ClassLoader parent) { + this(exploded, null, urls, parent); + } + + /** + * Create a new {@link LaunchedURLClassLoader} instance. + * @param exploded if the underlying archive is exploded + * @param rootArchive the root archive or {@code null} + * @param urls the URLs from which to load classes and resources + * @param parent the parent class loader for delegation + * @since 2.3.1 + */ + public LaunchedURLClassLoader(boolean exploded, Archive rootArchive, URL[] urls, ClassLoader parent) { + super(urls, parent); + this.exploded = exploded; + this.rootArchive = rootArchive; + } + + @Override + public URL findResource(String name) { + if (this.exploded) { + return super.findResource(name); + } + Handler.setUseFastConnectionExceptions(true); + try { + return super.findResource(name); + } + finally { + Handler.setUseFastConnectionExceptions(false); + } + } + + @Override + public Enumeration findResources(String name) throws IOException { + if (this.exploded) { + return super.findResources(name); + } + Handler.setUseFastConnectionExceptions(true); + try { + return new UseFastConnectionExceptionsEnumeration(super.findResources(name)); + } + finally { + Handler.setUseFastConnectionExceptions(false); + } + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.startsWith("org.springframework.boot.loader.jarmode.")) { + try { + Class result = loadClassInLaunchedClassLoader(name); + if (resolve) { + resolveClass(result); + } + return result; + } + catch (ClassNotFoundException ex) { + } + } + if (this.exploded) { + return super.loadClass(name, resolve); + } + Handler.setUseFastConnectionExceptions(true); + try { + try { + definePackageIfNecessary(name); + } + catch (IllegalArgumentException ex) { + // Tolerate race condition due to being parallel capable + if (getDefinedPackage(name) == null) { + // This should never happen as the IllegalArgumentException indicates + // that the package has already been defined and, therefore, + // getDefinedPackage(name) should not return null. + throw new AssertionError("Package " + name + " has already been defined but it could not be found"); + } + } + return super.loadClass(name, resolve); + } + finally { + Handler.setUseFastConnectionExceptions(false); + } + } + + private Class loadClassInLaunchedClassLoader(String name) throws ClassNotFoundException { + String internalName = name.replace('.', '/') + ".class"; + InputStream inputStream = getParent().getResourceAsStream(internalName); + if (inputStream == null) { + throw new ClassNotFoundException(name); + } + try { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[BUFFER_SIZE]; + int bytesRead = -1; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + inputStream.close(); + byte[] bytes = outputStream.toByteArray(); + Class definedClass = defineClass(name, bytes, 0, bytes.length); + definePackageIfNecessary(name); + return definedClass; + } + finally { + inputStream.close(); + } + } + catch (IOException ex) { + throw new ClassNotFoundException("Cannot load resource for class [" + name + "]", ex); + } + } + + /** + * Define a package before a {@code findClass} call is made. This is necessary to + * ensure that the appropriate manifest for nested JARs is associated with the + * package. + * @param className the class name being found + */ + private void definePackageIfNecessary(String className) { + int lastDot = className.lastIndexOf('.'); + if (lastDot >= 0) { + String packageName = className.substring(0, lastDot); + if (getDefinedPackage(packageName) == null) { + try { + definePackage(className, packageName); + } + catch (IllegalArgumentException ex) { + // Tolerate race condition due to being parallel capable + if (getDefinedPackage(packageName) == null) { + // This should never happen as the IllegalArgumentException + // indicates that the package has already been defined and, + // therefore, getDefinedPackage(name) should not have returned + // null. + throw new AssertionError( + "Package " + packageName + " has already been defined but it could not be found"); + } + } + } + } + } + + private void definePackage(String className, String packageName) { + String packageEntryName = packageName.replace('.', '/') + "/"; + String classEntryName = className.replace('.', '/') + ".class"; + for (URL url : getURLs()) { + try { + URLConnection connection = url.openConnection(); + if (connection instanceof JarURLConnection jarURLConnection) { + JarFile jarFile = jarURLConnection.getJarFile(); + if (jarFile.getEntry(classEntryName) != null && jarFile.getEntry(packageEntryName) != null + && jarFile.getManifest() != null) { + definePackage(packageName, jarFile.getManifest(), url); + return; + } + } + } + catch (IOException ex) { + // Ignore + } + } + } + + @Override + protected Package definePackage(String name, Manifest man, URL url) throws IllegalArgumentException { + if (!this.exploded) { + return super.definePackage(name, man, url); + } + synchronized (this.packageLock) { + return doDefinePackage(DefinePackageCallType.MANIFEST, () -> super.definePackage(name, man, url)); + } + } + + @Override + protected Package definePackage(String name, String specTitle, String specVersion, String specVendor, + String implTitle, String implVersion, String implVendor, URL sealBase) throws IllegalArgumentException { + if (!this.exploded) { + return super.definePackage(name, specTitle, specVersion, specVendor, implTitle, implVersion, implVendor, + sealBase); + } + synchronized (this.packageLock) { + if (this.definePackageCallType == null) { + // We're not part of a call chain which means that the URLClassLoader + // is trying to define a package for our exploded JAR. We use the + // manifest version to ensure package attributes are set + Manifest manifest = getManifest(this.rootArchive); + if (manifest != null) { + return definePackage(name, manifest, sealBase); + } + } + return doDefinePackage(DefinePackageCallType.ATTRIBUTES, () -> super.definePackage(name, specTitle, + specVersion, specVendor, implTitle, implVersion, implVendor, sealBase)); + } + } + + private Manifest getManifest(Archive archive) { + try { + return (archive != null) ? archive.getManifest() : null; + } + catch (IOException ex) { + return null; + } + } + + private T doDefinePackage(DefinePackageCallType type, Supplier call) { + DefinePackageCallType existingType = this.definePackageCallType; + try { + this.definePackageCallType = type; + return call.get(); + } + finally { + this.definePackageCallType = existingType; + } + } + + /** + * Clear URL caches. + */ + public void clearCache() { + if (this.exploded) { + return; + } + for (URL url : getURLs()) { + try { + URLConnection connection = url.openConnection(); + if (connection instanceof JarURLConnection) { + clearCache(connection); + } + } + catch (IOException ex) { + // Ignore + } + } + + } + + private void clearCache(URLConnection connection) throws IOException { + Object jarFile = ((JarURLConnection) connection).getJarFile(); + if (jarFile instanceof org.springframework.boot.loader.jar.JarFile) { + ((org.springframework.boot.loader.jar.JarFile) jarFile).clearCache(); + } + } + + private static class UseFastConnectionExceptionsEnumeration implements Enumeration { + + private final Enumeration delegate; + + UseFastConnectionExceptionsEnumeration(Enumeration delegate) { + this.delegate = delegate; + } + + @Override + public boolean hasMoreElements() { + Handler.setUseFastConnectionExceptions(true); + try { + return this.delegate.hasMoreElements(); + } + finally { + Handler.setUseFastConnectionExceptions(false); + } + + } + + @Override + public URL nextElement() { + Handler.setUseFastConnectionExceptions(true); + try { + return this.delegate.nextElement(); + } + finally { + Handler.setUseFastConnectionExceptions(false); + } + } + + } + + /** + * The different types of call made to define a package. We track these for exploded + * jars so that we can detect packages that should have manifest attributes applied. + */ + private enum DefinePackageCallType { + + /** + * A define package call from a resource that has a manifest. + */ + MANIFEST, + + /** + * A define package call with a direct set of attributes. + */ + ATTRIBUTES + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java new file mode 100644 index 000000000000..2f4cac944408 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java @@ -0,0 +1,159 @@ +/* + * Copyright 2012-2023 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.loader; + +import java.io.File; +import java.net.URI; +import java.net.URL; +import java.security.CodeSource; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.archive.ExplodedArchive; +import org.springframework.boot.loader.archive.JarFileArchive; +import org.springframework.boot.loader.jar.JarFile; + +/** + * Base class for launchers that can start an application with a fully configured + * classpath backed by one or more {@link Archive}s. + * + * @author Phillip Webb + * @author Dave Syer + * @since 1.0.0 + */ +public abstract class Launcher { + + private static final String JAR_MODE_LAUNCHER = "org.springframework.boot.loader.jarmode.JarModeLauncher"; + + /** + * Launch the application. This method is the initial entry point that should be + * called by a subclass {@code public static void main(String[] args)} method. + * @param args the incoming arguments + * @throws Exception if the application fails to launch + */ + protected void launch(String[] args) throws Exception { + if (!isExploded()) { + JarFile.registerUrlProtocolHandler(); + } + ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator()); + String jarMode = System.getProperty("jarmode"); + String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass(); + launch(args, launchClass, classLoader); + } + + /** + * Create a classloader for the specified archives. + * @param archives the archives + * @return the classloader + * @throws Exception if the classloader cannot be created + * @since 2.3.0 + */ + protected ClassLoader createClassLoader(Iterator archives) throws Exception { + List urls = new ArrayList<>(50); + while (archives.hasNext()) { + urls.add(archives.next().getUrl()); + } + return createClassLoader(urls.toArray(new URL[0])); + } + + /** + * Create a classloader for the specified URLs. + * @param urls the URLs + * @return the classloader + * @throws Exception if the classloader cannot be created + */ + protected ClassLoader createClassLoader(URL[] urls) throws Exception { + return new LaunchedURLClassLoader(isExploded(), getArchive(), urls, getClass().getClassLoader()); + } + + /** + * Launch the application given the archive file and a fully configured classloader. + * @param args the incoming arguments + * @param launchClass the launch class to run + * @param classLoader the classloader + * @throws Exception if the launch fails + */ + protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception { + Thread.currentThread().setContextClassLoader(classLoader); + createMainMethodRunner(launchClass, args, classLoader).run(); + } + + /** + * Create the {@code MainMethodRunner} used to launch the application. + * @param mainClass the main class + * @param args the incoming arguments + * @param classLoader the classloader + * @return the main method runner + */ + protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) { + return new MainMethodRunner(mainClass, args); + } + + /** + * Returns the main class that should be launched. + * @return the name of the main class + * @throws Exception if the main class cannot be obtained + */ + protected abstract String getMainClass() throws Exception; + + /** + * Returns the archives that will be used to construct the class path. + * @return the class path archives + * @throws Exception if the class path archives cannot be obtained + * @since 2.3.0 + */ + protected abstract Iterator getClassPathArchivesIterator() throws Exception; + + protected final Archive createArchive() throws Exception { + ProtectionDomain protectionDomain = getClass().getProtectionDomain(); + CodeSource codeSource = protectionDomain.getCodeSource(); + URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null; + String path = (location != null) ? location.getSchemeSpecificPart() : null; + if (path == null) { + throw new IllegalStateException("Unable to determine code source archive"); + } + File root = new File(path); + if (!root.exists()) { + throw new IllegalStateException("Unable to determine code source archive from " + root); + } + return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root)); + } + + /** + * Returns if the launcher is running in an exploded mode. If this method returns + * {@code true} then only regular JARs are supported and the additional URL and + * ClassLoader support infrastructure can be optimized. + * @return if the jar is exploded. + * @since 2.3.0 + */ + protected boolean isExploded() { + return false; + } + + /** + * Return the root archive. + * @return the root archive + * @since 2.3.1 + */ + protected Archive getArchive() { + return null; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java new file mode 100644 index 000000000000..12355a2bef46 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2023 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.loader; + +import java.lang.reflect.Method; + +/** + * Utility class that is used by {@link Launcher}s to call a main method. The class + * containing the main method is loaded using the thread context class loader. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @since 1.0.0 + */ +public class MainMethodRunner { + + private final String mainClassName; + + private final String[] args; + + /** + * Create a new {@link MainMethodRunner} instance. + * @param mainClass the main class + * @param args incoming arguments + */ + public MainMethodRunner(String mainClass, String[] args) { + this.mainClassName = mainClass; + this.args = (args != null) ? args.clone() : null; + } + + public void run() throws Exception { + Class mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader()); + Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); + mainMethod.setAccessible(true); + mainMethod.invoke(null, new Object[] { this.args }); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java new file mode 100755 index 000000000000..3703ac136705 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java @@ -0,0 +1,726 @@ +/* + * Copyright 2012-2023 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.loader; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Constructor; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.Set; +import java.util.jar.Manifest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.archive.Archive.Entry; +import org.springframework.boot.loader.archive.Archive.EntryFilter; +import org.springframework.boot.loader.archive.ExplodedArchive; +import org.springframework.boot.loader.archive.JarFileArchive; +import org.springframework.boot.loader.util.SystemPropertyUtils; + +/** + * {@link Launcher} for archives with user-configured classpath and main class through a + * properties file. This model is often more flexible and more amenable to creating + * well-behaved OS-level services than a model based on executable jars. + *

    + * Looks in various places for a properties file to extract loader settings, defaulting to + * {@code loader.properties} either on the current classpath or in the current working + * directory. The name of the properties file can be changed by setting a System property + * {@code loader.config.name} (e.g. {@code -Dloader.config.name=foo} will look for + * {@code foo.properties}. If that file doesn't exist then tries + * {@code loader.config.location} (with allowed prefixes {@code classpath:} and + * {@code file:} or any valid URL). Once that file is located turns it into Properties and + * extracts optional values (which can also be provided overridden as System properties in + * case the file doesn't exist): + *

      + *
    • {@code loader.path}: a comma-separated list of directories (containing file + * resources and/or nested archives in *.jar or *.zip or archives) or archives to append + * to the classpath. {@code BOOT-INF/classes,BOOT-INF/lib} in the application archive are + * always used
    • + *
    • {@code loader.main}: the main method to delegate execution to once the class loader + * is set up. No default, but will fall back to looking for a {@code Start-Class} in a + * {@code MANIFEST.MF}, if there is one in ${loader.home}/META-INF.
    • + *
    + * + * @author Dave Syer + * @author Janne Valkealahti + * @author Andy Wilkinson + * @since 1.0.0 + */ +public class PropertiesLauncher extends Launcher { + + private static final Class[] PARENT_ONLY_PARAMS = new Class[] { ClassLoader.class }; + + private static final Class[] URLS_AND_PARENT_PARAMS = new Class[] { URL[].class, ClassLoader.class }; + + private static final Class[] NO_PARAMS = new Class[] {}; + + private static final URL[] NO_URLS = new URL[0]; + + private static final String DEBUG = "loader.debug"; + + /** + * Properties key for main class. As a manifest entry can also be specified as + * {@code Start-Class}. + */ + public static final String MAIN = "loader.main"; + + /** + * Properties key for classpath entries (directories possibly containing jars or + * jars). Multiple entries can be specified using a comma-separated list. {@code + * BOOT-INF/classes,BOOT-INF/lib} in the application archive are always used. + */ + public static final String PATH = "loader.path"; + + /** + * Properties key for home directory. This is the location of external configuration + * if not on classpath, and also the base path for any relative paths in the + * {@link #PATH loader path}. Defaults to current working directory ( + * ${user.dir}). + */ + public static final String HOME = "loader.home"; + + /** + * Properties key for default command line arguments. These arguments (if present) are + * prepended to the main method arguments before launching. + */ + public static final String ARGS = "loader.args"; + + /** + * Properties key for name of external configuration file (excluding suffix). Defaults + * to "application". Ignored if {@link #CONFIG_LOCATION loader config location} is + * provided instead. + */ + public static final String CONFIG_NAME = "loader.config.name"; + + /** + * Properties key for config file location (including optional classpath:, file: or + * URL prefix). + */ + public static final String CONFIG_LOCATION = "loader.config.location"; + + /** + * Properties key for boolean flag (default false) which, if set, will cause the + * external configuration properties to be copied to System properties (assuming that + * is allowed by Java security). + */ + public static final String SET_SYSTEM_PROPERTIES = "loader.system"; + + private static final Pattern WORD_SEPARATOR = Pattern.compile("\\W+"); + + private static final String NESTED_ARCHIVE_SEPARATOR = "!" + File.separator; + + private final File home; + + private List paths = new ArrayList<>(); + + private final Properties properties = new Properties(); + + private final Archive parent; + + private volatile ClassPathArchives classPathArchives; + + public PropertiesLauncher() { + try { + this.home = getHomeDirectory(); + initializeProperties(); + initializePaths(); + this.parent = createArchive(); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + protected File getHomeDirectory() { + try { + return new File(getPropertyWithDefault(HOME, "${user.dir}")); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + private void initializeProperties() throws Exception { + List configs = new ArrayList<>(); + if (getProperty(CONFIG_LOCATION) != null) { + configs.add(getProperty(CONFIG_LOCATION)); + } + else { + String[] names = getPropertyWithDefault(CONFIG_NAME, "loader").split(","); + for (String name : names) { + configs.add("file:" + getHomeDirectory() + "/" + name + ".properties"); + configs.add("classpath:" + name + ".properties"); + configs.add("classpath:BOOT-INF/classes/" + name + ".properties"); + } + } + for (String config : configs) { + try (InputStream resource = getResource(config)) { + if (resource != null) { + debug("Found: " + config); + loadResource(resource); + // Load the first one we find + return; + } + else { + debug("Not found: " + config); + } + } + } + } + + private void loadResource(InputStream resource) throws Exception { + this.properties.load(resource); + for (Object key : Collections.list(this.properties.propertyNames())) { + String text = this.properties.getProperty((String) key); + String value = SystemPropertyUtils.resolvePlaceholders(this.properties, text); + if (value != null) { + this.properties.put(key, value); + } + } + if ("true".equals(getProperty(SET_SYSTEM_PROPERTIES))) { + debug("Adding resolved properties to System properties"); + for (Object key : Collections.list(this.properties.propertyNames())) { + String value = this.properties.getProperty((String) key); + System.setProperty((String) key, value); + } + } + } + + private InputStream getResource(String config) throws Exception { + if (config.startsWith("classpath:")) { + return getClasspathResource(config.substring("classpath:".length())); + } + config = handleUrl(config); + if (isUrl(config)) { + return getURLResource(config); + } + return getFileResource(config); + } + + private String handleUrl(String path) throws UnsupportedEncodingException { + if (path.startsWith("jar:file:") || path.startsWith("file:")) { + path = URLDecoder.decode(path, "UTF-8"); + if (path.startsWith("file:")) { + path = path.substring("file:".length()); + if (path.startsWith("//")) { + path = path.substring(2); + } + } + } + return path; + } + + private boolean isUrl(String config) { + return config.contains("://"); + } + + private InputStream getClasspathResource(String config) { + while (config.startsWith("/")) { + config = config.substring(1); + } + config = "/" + config; + debug("Trying classpath: " + config); + return getClass().getResourceAsStream(config); + } + + private InputStream getFileResource(String config) throws Exception { + File file = new File(config); + debug("Trying file: " + config); + if (file.canRead()) { + return new FileInputStream(file); + } + return null; + } + + private InputStream getURLResource(String config) throws Exception { + URL url = new URL(config); + if (exists(url)) { + URLConnection con = url.openConnection(); + try { + return con.getInputStream(); + } + catch (IOException ex) { + // Close the HTTP connection (if applicable). + if (con instanceof HttpURLConnection httpURLConnection) { + httpURLConnection.disconnect(); + } + throw ex; + } + } + return null; + } + + private boolean exists(URL url) throws IOException { + // Try a URL connection content-length header... + URLConnection connection = url.openConnection(); + try { + connection.setUseCaches(connection.getClass().getSimpleName().startsWith("JNLP")); + if (connection instanceof HttpURLConnection httpConnection) { + httpConnection.setRequestMethod("HEAD"); + int responseCode = httpConnection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { + return true; + } + else if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) { + return false; + } + } + return (connection.getContentLength() >= 0); + } + finally { + if (connection instanceof HttpURLConnection httpURLConnection) { + httpURLConnection.disconnect(); + } + } + } + + private void initializePaths() throws Exception { + String path = getProperty(PATH); + if (path != null) { + this.paths = parsePathsProperty(path); + } + debug("Nested archive paths: " + this.paths); + } + + private List parsePathsProperty(String commaSeparatedPaths) { + List paths = new ArrayList<>(); + for (String path : commaSeparatedPaths.split(",")) { + path = cleanupPath(path); + // "" means the user wants root of archive but not current directory + path = (path == null || path.isEmpty()) ? "/" : path; + paths.add(path); + } + if (paths.isEmpty()) { + paths.add("lib"); + } + return paths; + } + + protected String[] getArgs(String... args) throws Exception { + String loaderArgs = getProperty(ARGS); + if (loaderArgs != null) { + String[] defaultArgs = loaderArgs.split("\\s+"); + String[] additionalArgs = args; + args = new String[defaultArgs.length + additionalArgs.length]; + System.arraycopy(defaultArgs, 0, args, 0, defaultArgs.length); + System.arraycopy(additionalArgs, 0, args, defaultArgs.length, additionalArgs.length); + } + return args; + } + + @Override + protected String getMainClass() throws Exception { + String mainClass = getProperty(MAIN, "Start-Class"); + if (mainClass == null) { + throw new IllegalStateException("No '" + MAIN + "' or 'Start-Class' specified"); + } + return mainClass; + } + + @Override + protected ClassLoader createClassLoader(Iterator archives) throws Exception { + String customLoaderClassName = getProperty("loader.classLoader"); + if (customLoaderClassName == null) { + return super.createClassLoader(archives); + } + Set urls = new LinkedHashSet<>(); + while (archives.hasNext()) { + urls.add(archives.next().getUrl()); + } + ClassLoader loader = new LaunchedURLClassLoader(urls.toArray(NO_URLS), getClass().getClassLoader()); + debug("Classpath for custom loader: " + urls); + loader = wrapWithCustomClassLoader(loader, customLoaderClassName); + debug("Using custom class loader: " + customLoaderClassName); + return loader; + } + + @SuppressWarnings("unchecked") + private ClassLoader wrapWithCustomClassLoader(ClassLoader parent, String className) throws Exception { + Class type = (Class) Class.forName(className, true, parent); + ClassLoader classLoader = newClassLoader(type, PARENT_ONLY_PARAMS, parent); + if (classLoader == null) { + classLoader = newClassLoader(type, URLS_AND_PARENT_PARAMS, NO_URLS, parent); + } + if (classLoader == null) { + classLoader = newClassLoader(type, NO_PARAMS); + } + if (classLoader == null) { + throw new IllegalArgumentException("Unable to create class loader for " + className); + } + return classLoader; + } + + private ClassLoader newClassLoader(Class loaderClass, Class[] parameterTypes, Object... initargs) + throws Exception { + try { + Constructor constructor = loaderClass.getDeclaredConstructor(parameterTypes); + constructor.setAccessible(true); + return constructor.newInstance(initargs); + } + catch (NoSuchMethodException ex) { + return null; + } + } + + private String getProperty(String propertyKey) throws Exception { + return getProperty(propertyKey, null, null); + } + + private String getProperty(String propertyKey, String manifestKey) throws Exception { + return getProperty(propertyKey, manifestKey, null); + } + + private String getPropertyWithDefault(String propertyKey, String defaultValue) throws Exception { + return getProperty(propertyKey, null, defaultValue); + } + + private String getProperty(String propertyKey, String manifestKey, String defaultValue) throws Exception { + if (manifestKey == null) { + manifestKey = propertyKey.replace('.', '-'); + manifestKey = toCamelCase(manifestKey); + } + String property = SystemPropertyUtils.getProperty(propertyKey); + if (property != null) { + String value = SystemPropertyUtils.resolvePlaceholders(this.properties, property); + debug("Property '" + propertyKey + "' from environment: " + value); + return value; + } + if (this.properties.containsKey(propertyKey)) { + String value = SystemPropertyUtils.resolvePlaceholders(this.properties, + this.properties.getProperty(propertyKey)); + debug("Property '" + propertyKey + "' from properties: " + value); + return value; + } + try { + if (this.home != null) { + // Prefer home dir for MANIFEST if there is one + try (ExplodedArchive archive = new ExplodedArchive(this.home, false)) { + Manifest manifest = archive.getManifest(); + if (manifest != null) { + String value = manifest.getMainAttributes().getValue(manifestKey); + if (value != null) { + debug("Property '" + manifestKey + "' from home directory manifest: " + value); + return SystemPropertyUtils.resolvePlaceholders(this.properties, value); + } + } + } + } + } + catch (IllegalStateException ex) { + // Ignore + } + // Otherwise try the parent archive + Manifest manifest = createArchive().getManifest(); + if (manifest != null) { + String value = manifest.getMainAttributes().getValue(manifestKey); + if (value != null) { + debug("Property '" + manifestKey + "' from archive manifest: " + value); + return SystemPropertyUtils.resolvePlaceholders(this.properties, value); + } + } + return (defaultValue != null) ? SystemPropertyUtils.resolvePlaceholders(this.properties, defaultValue) + : defaultValue; + } + + @Override + protected Iterator getClassPathArchivesIterator() throws Exception { + ClassPathArchives classPathArchives = this.classPathArchives; + if (classPathArchives == null) { + classPathArchives = new ClassPathArchives(); + this.classPathArchives = classPathArchives; + } + return classPathArchives.iterator(); + } + + public static void main(String[] args) throws Exception { + PropertiesLauncher launcher = new PropertiesLauncher(); + args = launcher.getArgs(args); + launcher.launch(args); + } + + public static String toCamelCase(CharSequence string) { + if (string == null) { + return null; + } + StringBuilder builder = new StringBuilder(); + Matcher matcher = WORD_SEPARATOR.matcher(string); + int pos = 0; + while (matcher.find()) { + builder.append(capitalize(string.subSequence(pos, matcher.end()).toString())); + pos = matcher.end(); + } + builder.append(capitalize(string.subSequence(pos, string.length()).toString())); + return builder.toString(); + } + + private static String capitalize(String str) { + return Character.toUpperCase(str.charAt(0)) + str.substring(1); + } + + private void debug(String message) { + if (Boolean.getBoolean(DEBUG)) { + System.out.println(message); + } + } + + private String cleanupPath(String path) { + path = path.trim(); + // No need for current dir path + if (path.startsWith("./")) { + path = path.substring(2); + } + String lowerCasePath = path.toLowerCase(Locale.ENGLISH); + if (lowerCasePath.endsWith(".jar") || lowerCasePath.endsWith(".zip")) { + return path; + } + if (path.endsWith("/*")) { + path = path.substring(0, path.length() - 1); + } + else { + // It's a directory + if (!path.endsWith("/") && !path.equals(".")) { + path = path + "/"; + } + } + return path; + } + + void close() throws Exception { + if (this.classPathArchives != null) { + this.classPathArchives.close(); + } + if (this.parent != null) { + this.parent.close(); + } + } + + /** + * An iterable collection of the classpath archives. + */ + private class ClassPathArchives implements Iterable { + + private final List classPathArchives; + + private final List jarFileArchives = new ArrayList<>(); + + ClassPathArchives() throws Exception { + this.classPathArchives = new ArrayList<>(); + for (String path : PropertiesLauncher.this.paths) { + for (Archive archive : getClassPathArchives(path)) { + addClassPathArchive(archive); + } + } + addNestedEntries(); + } + + private void addClassPathArchive(Archive archive) throws IOException { + if (!(archive instanceof ExplodedArchive)) { + this.classPathArchives.add(archive); + return; + } + this.classPathArchives.add(archive); + this.classPathArchives.addAll(asList(archive.getNestedArchives(null, new ArchiveEntryFilter()))); + } + + private List getClassPathArchives(String path) throws Exception { + String root = cleanupPath(handleUrl(path)); + List lib = new ArrayList<>(); + File file = new File(root); + if (!"/".equals(root)) { + if (!isAbsolutePath(root)) { + file = new File(PropertiesLauncher.this.home, root); + } + if (file.isDirectory()) { + debug("Adding classpath entries from " + file); + Archive archive = new ExplodedArchive(file, false); + lib.add(archive); + } + } + Archive archive = getArchive(file); + if (archive != null) { + debug("Adding classpath entries from archive " + archive.getUrl() + root); + lib.add(archive); + } + List nestedArchives = getNestedArchives(root); + if (nestedArchives != null) { + debug("Adding classpath entries from nested " + root); + lib.addAll(nestedArchives); + } + return lib; + } + + private boolean isAbsolutePath(String root) { + // Windows contains ":" others start with "/" + return root.contains(":") || root.startsWith("/"); + } + + private Archive getArchive(File file) throws IOException { + if (isNestedArchivePath(file)) { + return null; + } + String name = file.getName().toLowerCase(Locale.ENGLISH); + if (name.endsWith(".jar") || name.endsWith(".zip")) { + return getJarFileArchive(file); + } + return null; + } + + private boolean isNestedArchivePath(File file) { + return file.getPath().contains(NESTED_ARCHIVE_SEPARATOR); + } + + private List getNestedArchives(String path) throws Exception { + Archive parent = PropertiesLauncher.this.parent; + String root = path; + if (!root.equals("/") && root.startsWith("/") + || parent.getUrl().toURI().equals(PropertiesLauncher.this.home.toURI())) { + // If home dir is same as parent archive, no need to add it twice. + return null; + } + int index = root.indexOf('!'); + if (index != -1) { + File file = new File(PropertiesLauncher.this.home, root.substring(0, index)); + if (root.startsWith("jar:file:")) { + file = new File(root.substring("jar:file:".length(), index)); + } + parent = getJarFileArchive(file); + root = root.substring(index + 1); + while (root.startsWith("/")) { + root = root.substring(1); + } + } + if (root.endsWith(".jar")) { + File file = new File(PropertiesLauncher.this.home, root); + if (file.exists()) { + parent = getJarFileArchive(file); + root = ""; + } + } + if (root.equals("/") || root.equals("./") || root.equals(".")) { + // The prefix for nested jars is actually empty if it's at the root + root = ""; + } + EntryFilter filter = new PrefixMatchingArchiveFilter(root); + List archives = asList(parent.getNestedArchives(null, filter)); + if ((root == null || root.isEmpty() || ".".equals(root)) && !path.endsWith(".jar") + && parent != PropertiesLauncher.this.parent) { + // You can't find the root with an entry filter so it has to be added + // explicitly. But don't add the root of the parent archive. + archives.add(parent); + } + return archives; + } + + private void addNestedEntries() { + // The parent archive might have "BOOT-INF/lib/" and "BOOT-INF/classes/" + // directories, meaning we are running from an executable JAR. We add nested + // entries from there with low priority (i.e. at end). + try { + Iterator archives = PropertiesLauncher.this.parent.getNestedArchives(null, + JarLauncher.NESTED_ARCHIVE_ENTRY_FILTER); + while (archives.hasNext()) { + this.classPathArchives.add(archives.next()); + } + } + catch (IOException ex) { + // Ignore + } + } + + private List asList(Iterator iterator) { + List list = new ArrayList<>(); + while (iterator.hasNext()) { + list.add(iterator.next()); + } + return list; + } + + private JarFileArchive getJarFileArchive(File file) throws IOException { + JarFileArchive archive = new JarFileArchive(file); + this.jarFileArchives.add(archive); + return archive; + } + + @Override + public Iterator iterator() { + return this.classPathArchives.iterator(); + } + + void close() throws IOException { + for (JarFileArchive archive : this.jarFileArchives) { + archive.close(); + } + } + + } + + /** + * Convenience class for finding nested archives that have a prefix in their file path + * (e.g. "lib/"). + */ + private static final class PrefixMatchingArchiveFilter implements EntryFilter { + + private final String prefix; + + private final ArchiveEntryFilter filter = new ArchiveEntryFilter(); + + private PrefixMatchingArchiveFilter(String prefix) { + this.prefix = prefix; + } + + @Override + public boolean matches(Entry entry) { + if (entry.isDirectory()) { + return entry.getName().equals(this.prefix); + } + return entry.getName().startsWith(this.prefix) && this.filter.matches(entry); + } + + } + + /** + * Convenience class for finding nested archives (archive entries that can be + * classpath entries). + */ + private static final class ArchiveEntryFilter implements EntryFilter { + + private static final String DOT_JAR = ".jar"; + + private static final String DOT_ZIP = ".zip"; + + @Override + public boolean matches(Entry entry) { + return entry.getName().endsWith(DOT_JAR) || entry.getName().endsWith(DOT_ZIP); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java new file mode 100644 index 000000000000..482832c1f722 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2023 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.loader; + +import org.springframework.boot.loader.archive.Archive; + +/** + * {@link Launcher} for WAR based archives. This launcher for standard WAR archives. + * Supports dependencies in {@code WEB-INF/lib} as well as {@code WEB-INF/lib-provided}, + * classes are loaded from {@code WEB-INF/classes}. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Scott Frederick + * @since 1.0.0 + */ +public class WarLauncher extends ExecutableArchiveLauncher { + + public WarLauncher() { + } + + protected WarLauncher(Archive archive) { + super(archive); + } + + @Override + protected boolean isPostProcessingClassPathArchives() { + return false; + } + + @Override + public boolean isNestedArchive(Archive.Entry entry) { + if (entry.isDirectory()) { + return entry.getName().equals("WEB-INF/classes/"); + } + return entry.getName().startsWith("WEB-INF/lib/") || entry.getName().startsWith("WEB-INF/lib-provided/"); + } + + @Override + protected String getArchiveEntryPathPrefix() { + return "WEB-INF/"; + } + + public static void main(String[] args) throws Exception { + new WarLauncher().launch(args); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java new file mode 100644 index 000000000000..c1f2bbb2f75b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-2023 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.loader.archive; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Iterator; +import java.util.jar.Manifest; + +import org.springframework.boot.loader.Launcher; + +/** + * An archive that can be launched by the {@link Launcher}. + * + * @author Phillip Webb + * @since 1.0.0 + * @see JarFileArchive + */ +public interface Archive extends Iterable, AutoCloseable { + + /** + * Returns a URL that can be used to load the archive. + * @return the archive URL + * @throws MalformedURLException if the URL is malformed + */ + URL getUrl() throws MalformedURLException; + + /** + * Returns the manifest of the archive. + * @return the manifest + * @throws IOException if the manifest cannot be read + */ + Manifest getManifest() throws IOException; + + /** + * Returns nested {@link Archive}s for entries that match the specified filters. + * @param searchFilter filter used to limit when additional sub-entry searching is + * required or {@code null} if all entries should be considered. + * @param includeFilter filter used to determine which entries should be included in + * the result or {@code null} if all entries should be included + * @return the nested archives + * @throws IOException on IO error + * @since 2.3.0 + */ + Iterator getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) throws IOException; + + /** + * Return if the archive is exploded (already unpacked). + * @return if the archive is exploded + * @since 2.3.0 + */ + default boolean isExploded() { + return false; + } + + /** + * Closes the {@code Archive}, releasing any open resources. + * @throws Exception if an error occurs during close processing + * @since 2.2.0 + */ + @Override + default void close() throws Exception { + + } + + /** + * Represents a single entry in the archive. + */ + interface Entry { + + /** + * Returns {@code true} if the entry represents a directory. + * @return if the entry is a directory + */ + boolean isDirectory(); + + /** + * Returns the name of the entry. + * @return the name of the entry + */ + String getName(); + + } + + /** + * Strategy interface to filter {@link Entry Entries}. + */ + @FunctionalInterface + interface EntryFilter { + + /** + * Apply the jar entry filter. + * @param entry the entry to filter + * @return {@code true} if the filter matches + */ + boolean matches(Entry entry); + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java new file mode 100644 index 000000000000..f8cd52dc16f1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java @@ -0,0 +1,342 @@ +/* + * Copyright 2012-2023 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.loader.archive; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Deque; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.jar.Manifest; + +/** + * {@link Archive} implementation backed by an exploded archive directory. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Madhura Bhave + * @since 1.0.0 + */ +public class ExplodedArchive implements Archive { + + private static final Set SKIPPED_NAMES = new HashSet<>(Arrays.asList(".", "..")); + + private final File root; + + private final boolean recursive; + + private final File manifestFile; + + private Manifest manifest; + + /** + * Create a new {@link ExplodedArchive} instance. + * @param root the root directory + */ + public ExplodedArchive(File root) { + this(root, true); + } + + /** + * Create a new {@link ExplodedArchive} instance. + * @param root the root directory + * @param recursive if recursive searching should be used to locate the manifest. + * Defaults to {@code true}, directories with a large tree might want to set this to + * {@code false}. + */ + public ExplodedArchive(File root, boolean recursive) { + if (!root.exists() || !root.isDirectory()) { + throw new IllegalArgumentException("Invalid source directory " + root); + } + this.root = root; + this.recursive = recursive; + this.manifestFile = getManifestFile(root); + } + + private File getManifestFile(File root) { + File metaInf = new File(root, "META-INF"); + return new File(metaInf, "MANIFEST.MF"); + } + + @Override + public URL getUrl() throws MalformedURLException { + return this.root.toURI().toURL(); + } + + @Override + public Manifest getManifest() throws IOException { + if (this.manifest == null && this.manifestFile.exists()) { + try (FileInputStream inputStream = new FileInputStream(this.manifestFile)) { + this.manifest = new Manifest(inputStream); + } + } + return this.manifest; + } + + @Override + public Iterator getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) throws IOException { + return new ArchiveIterator(this.root, this.recursive, searchFilter, includeFilter); + } + + @Override + @Deprecated(since = "2.3.10", forRemoval = false) + public Iterator iterator() { + return new EntryIterator(this.root, this.recursive, null, null); + } + + protected Archive getNestedArchive(Entry entry) { + File file = ((FileEntry) entry).getFile(); + return (file.isDirectory() ? new ExplodedArchive(file) : new SimpleJarFileArchive((FileEntry) entry)); + } + + @Override + public boolean isExploded() { + return true; + } + + @Override + public String toString() { + try { + return getUrl().toString(); + } + catch (Exception ex) { + return "exploded archive"; + } + } + + /** + * File based {@link Entry} {@link Iterator}. + */ + private abstract static class AbstractIterator implements Iterator { + + private static final Comparator entryComparator = Comparator.comparing(File::getAbsolutePath); + + private final File root; + + private final boolean recursive; + + private final EntryFilter searchFilter; + + private final EntryFilter includeFilter; + + private final Deque> stack = new LinkedList<>(); + + private FileEntry current; + + private final String rootUrl; + + AbstractIterator(File root, boolean recursive, EntryFilter searchFilter, EntryFilter includeFilter) { + this.root = root; + this.rootUrl = this.root.toURI().getPath(); + this.recursive = recursive; + this.searchFilter = searchFilter; + this.includeFilter = includeFilter; + this.stack.add(listFiles(root)); + this.current = poll(); + } + + @Override + public boolean hasNext() { + return this.current != null; + } + + @Override + public T next() { + FileEntry entry = this.current; + if (entry == null) { + throw new NoSuchElementException(); + } + this.current = poll(); + return adapt(entry); + } + + private FileEntry poll() { + while (!this.stack.isEmpty()) { + while (this.stack.peek().hasNext()) { + File file = this.stack.peek().next(); + if (SKIPPED_NAMES.contains(file.getName())) { + continue; + } + FileEntry entry = getFileEntry(file); + if (isListable(entry)) { + this.stack.addFirst(listFiles(file)); + } + if (this.includeFilter == null || this.includeFilter.matches(entry)) { + return entry; + } + } + this.stack.poll(); + } + return null; + } + + private FileEntry getFileEntry(File file) { + URI uri = file.toURI(); + String name = uri.getPath().substring(this.rootUrl.length()); + try { + return new FileEntry(name, file, uri.toURL()); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + + private boolean isListable(FileEntry entry) { + return entry.isDirectory() && (this.recursive || entry.getFile().getParentFile().equals(this.root)) + && (this.searchFilter == null || this.searchFilter.matches(entry)) + && (this.includeFilter == null || !this.includeFilter.matches(entry)); + } + + private Iterator listFiles(File file) { + File[] files = file.listFiles(); + if (files == null) { + return Collections.emptyIterator(); + } + Arrays.sort(files, entryComparator); + return Arrays.asList(files).iterator(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove"); + } + + protected abstract T adapt(FileEntry entry); + + } + + private static class EntryIterator extends AbstractIterator { + + EntryIterator(File root, boolean recursive, EntryFilter searchFilter, EntryFilter includeFilter) { + super(root, recursive, searchFilter, includeFilter); + } + + @Override + protected Entry adapt(FileEntry entry) { + return entry; + } + + } + + private static class ArchiveIterator extends AbstractIterator { + + ArchiveIterator(File root, boolean recursive, EntryFilter searchFilter, EntryFilter includeFilter) { + super(root, recursive, searchFilter, includeFilter); + } + + @Override + protected Archive adapt(FileEntry entry) { + File file = entry.getFile(); + return (file.isDirectory() ? new ExplodedArchive(file) : new SimpleJarFileArchive(entry)); + } + + } + + /** + * {@link Entry} backed by a File. + */ + private static class FileEntry implements Entry { + + private final String name; + + private final File file; + + private final URL url; + + FileEntry(String name, File file, URL url) { + this.name = name; + this.file = file; + this.url = url; + } + + File getFile() { + return this.file; + } + + @Override + public boolean isDirectory() { + return this.file.isDirectory(); + } + + @Override + public String getName() { + return this.name; + } + + URL getUrl() { + return this.url; + } + + } + + /** + * {@link Archive} implementation backed by a simple JAR file that doesn't itself + * contain nested archives. + */ + private static class SimpleJarFileArchive implements Archive { + + private final URL url; + + SimpleJarFileArchive(FileEntry file) { + this.url = file.getUrl(); + } + + @Override + public URL getUrl() throws MalformedURLException { + return this.url; + } + + @Override + public Manifest getManifest() throws IOException { + return null; + } + + @Override + public Iterator getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) + throws IOException { + return Collections.emptyIterator(); + } + + @Override + @Deprecated(since = "2.3.10", forRemoval = false) + public Iterator iterator() { + return Collections.emptyIterator(); + } + + @Override + public String toString() { + try { + return getUrl().toString(); + } + catch (Exception ex) { + return "jar archive"; + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java new file mode 100755 index 000000000000..91e7bc53a486 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java @@ -0,0 +1,310 @@ +/* + * Copyright 2012-2023 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.loader.archive; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.UUID; +import java.util.jar.JarEntry; +import java.util.jar.Manifest; + +import org.springframework.boot.loader.jar.JarFile; + +/** + * {@link Archive} implementation backed by a {@link JarFile}. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @since 1.0.0 + */ +public class JarFileArchive implements Archive { + + private static final String UNPACK_MARKER = "UNPACK:"; + + private static final int BUFFER_SIZE = 32 * 1024; + + private static final FileAttribute[] NO_FILE_ATTRIBUTES = {}; + + private static final EnumSet DIRECTORY_PERMISSIONS = EnumSet.of(PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE); + + private static final EnumSet FILE_PERMISSIONS = EnumSet.of(PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE); + + private final JarFile jarFile; + + private URL url; + + private Path tempUnpackDirectory; + + public JarFileArchive(File file) throws IOException { + this(file, file.toURI().toURL()); + } + + public JarFileArchive(File file, URL url) throws IOException { + this(new JarFile(file)); + this.url = url; + } + + public JarFileArchive(JarFile jarFile) { + this.jarFile = jarFile; + } + + @Override + public URL getUrl() throws MalformedURLException { + if (this.url != null) { + return this.url; + } + return this.jarFile.getUrl(); + } + + @Override + public Manifest getManifest() throws IOException { + return this.jarFile.getManifest(); + } + + @Override + public Iterator getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) throws IOException { + return new NestedArchiveIterator(this.jarFile.iterator(), searchFilter, includeFilter); + } + + @Override + @Deprecated(since = "2.3.10", forRemoval = false) + public Iterator iterator() { + return new EntryIterator(this.jarFile.iterator(), null, null); + } + + @Override + public void close() throws IOException { + this.jarFile.close(); + } + + protected Archive getNestedArchive(Entry entry) throws IOException { + JarEntry jarEntry = ((JarFileEntry) entry).getJarEntry(); + if (jarEntry.getComment().startsWith(UNPACK_MARKER)) { + return getUnpackedNestedArchive(jarEntry); + } + try { + JarFile jarFile = this.jarFile.getNestedJarFile(jarEntry); + return new JarFileArchive(jarFile); + } + catch (Exception ex) { + throw new IllegalStateException("Failed to get nested archive for entry " + entry.getName(), ex); + } + } + + private Archive getUnpackedNestedArchive(JarEntry jarEntry) throws IOException { + String name = jarEntry.getName(); + if (name.lastIndexOf('/') != -1) { + name = name.substring(name.lastIndexOf('/') + 1); + } + Path path = getTempUnpackDirectory().resolve(name); + if (!Files.exists(path) || Files.size(path) != jarEntry.getSize()) { + unpack(jarEntry, path); + } + return new JarFileArchive(path.toFile(), path.toUri().toURL()); + } + + private Path getTempUnpackDirectory() { + if (this.tempUnpackDirectory == null) { + Path tempDirectory = Paths.get(System.getProperty("java.io.tmpdir")); + this.tempUnpackDirectory = createUnpackDirectory(tempDirectory); + } + return this.tempUnpackDirectory; + } + + private Path createUnpackDirectory(Path parent) { + int attempts = 0; + while (attempts++ < 1000) { + String fileName = Paths.get(this.jarFile.getName()).getFileName().toString(); + Path unpackDirectory = parent.resolve(fileName + "-spring-boot-libs-" + UUID.randomUUID()); + try { + createDirectory(unpackDirectory); + return unpackDirectory; + } + catch (IOException ex) { + } + } + throw new IllegalStateException("Failed to create unpack directory in directory '" + parent + "'"); + } + + private void unpack(JarEntry entry, Path path) throws IOException { + createFile(path); + path.toFile().deleteOnExit(); + try (InputStream inputStream = this.jarFile.getInputStream(entry); + OutputStream outputStream = Files.newOutputStream(path, StandardOpenOption.WRITE, + StandardOpenOption.TRUNCATE_EXISTING)) { + byte[] buffer = new byte[BUFFER_SIZE]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + outputStream.flush(); + } + } + + private void createDirectory(Path path) throws IOException { + Files.createDirectory(path, getFileAttributes(path.getFileSystem(), DIRECTORY_PERMISSIONS)); + } + + private void createFile(Path path) throws IOException { + Files.createFile(path, getFileAttributes(path.getFileSystem(), FILE_PERMISSIONS)); + } + + private FileAttribute[] getFileAttributes(FileSystem fileSystem, EnumSet ownerReadWrite) { + if (!fileSystem.supportedFileAttributeViews().contains("posix")) { + return NO_FILE_ATTRIBUTES; + } + return new FileAttribute[] { PosixFilePermissions.asFileAttribute(ownerReadWrite) }; + } + + @Override + public String toString() { + try { + return getUrl().toString(); + } + catch (Exception ex) { + return "jar archive"; + } + } + + /** + * Abstract base class for iterator implementations. + */ + private abstract static class AbstractIterator implements Iterator { + + private final Iterator iterator; + + private final EntryFilter searchFilter; + + private final EntryFilter includeFilter; + + private Entry current; + + AbstractIterator(Iterator iterator, EntryFilter searchFilter, EntryFilter includeFilter) { + this.iterator = iterator; + this.searchFilter = searchFilter; + this.includeFilter = includeFilter; + this.current = poll(); + } + + @Override + public boolean hasNext() { + return this.current != null; + } + + @Override + public T next() { + T result = adapt(this.current); + this.current = poll(); + return result; + } + + private Entry poll() { + while (this.iterator.hasNext()) { + JarFileEntry candidate = new JarFileEntry(this.iterator.next()); + if ((this.searchFilter == null || this.searchFilter.matches(candidate)) + && (this.includeFilter == null || this.includeFilter.matches(candidate))) { + return candidate; + } + } + return null; + } + + protected abstract T adapt(Entry entry); + + } + + /** + * {@link Archive.Entry} iterator implementation backed by {@link JarEntry}. + */ + private static class EntryIterator extends AbstractIterator { + + EntryIterator(Iterator iterator, EntryFilter searchFilter, EntryFilter includeFilter) { + super(iterator, searchFilter, includeFilter); + } + + @Override + protected Entry adapt(Entry entry) { + return entry; + } + + } + + /** + * Nested {@link Archive} iterator implementation backed by {@link JarEntry}. + */ + private class NestedArchiveIterator extends AbstractIterator { + + NestedArchiveIterator(Iterator iterator, EntryFilter searchFilter, EntryFilter includeFilter) { + super(iterator, searchFilter, includeFilter); + } + + @Override + protected Archive adapt(Entry entry) { + try { + return getNestedArchive(entry); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + } + + /** + * {@link Archive.Entry} implementation backed by a {@link JarEntry}. + */ + private static class JarFileEntry implements Entry { + + private final JarEntry jarEntry; + + JarFileEntry(JarEntry jarEntry) { + this.jarEntry = jarEntry; + } + + JarEntry getJarEntry() { + return this.jarEntry; + } + + @Override + public boolean isDirectory() { + return this.jarEntry.isDirectory(); + } + + @Override + public String getName() { + return this.jarEntry.getName(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/package-info.java new file mode 100644 index 000000000000..27ce99b006f0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Abstraction over logical Archives be they backed by a JAR file or unpacked into a + * directory. + * + * @see org.springframework.boot.loader.archive.Archive + */ +package org.springframework.boot.loader.archive; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java new file mode 100644 index 000000000000..e96d5ea81a05 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2023 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.loader.data; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +/** + * Interface that provides read-only random access to some underlying data. + * Implementations must allow concurrent reads in a thread-safe manner. + * + * @author Phillip Webb + * @since 1.0.0 + */ +public interface RandomAccessData { + + /** + * Returns an {@link InputStream} that can be used to read the underlying data. The + * caller is responsible close the underlying stream. + * @return a new input stream that can be used to read the underlying data. + * @throws IOException if the stream cannot be opened + */ + InputStream getInputStream() throws IOException; + + /** + * Returns a new {@link RandomAccessData} for a specific subsection of this data. + * @param offset the offset of the subsection + * @param length the length of the subsection + * @return the subsection data + */ + RandomAccessData getSubsection(long offset, long length); + + /** + * Reads all the data and returns it as a byte array. + * @return the data + * @throws IOException if the data cannot be read + */ + byte[] read() throws IOException; + + /** + * Reads the {@code length} bytes of data starting at the given {@code offset}. + * @param offset the offset from which data should be read + * @param length the number of bytes to be read + * @return the data + * @throws IOException if the data cannot be read + * @throws IndexOutOfBoundsException if offset is beyond the end of the file or + * subsection + * @throws EOFException if offset plus length is greater than the length of the file + * or subsection + */ + byte[] read(long offset, long length) throws IOException; + + /** + * Returns the size of the data. + * @return the size + */ + long getSize(); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java new file mode 100644 index 000000000000..4bd5d205418c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java @@ -0,0 +1,262 @@ +/* + * Copyright 2012-2023 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.loader.data; + +import java.io.EOFException; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; + +/** + * {@link RandomAccessData} implementation backed by a {@link RandomAccessFile}. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @since 1.0.0 + */ +public class RandomAccessDataFile implements RandomAccessData { + + private final FileAccess fileAccess; + + private final long offset; + + private final long length; + + /** + * Create a new {@link RandomAccessDataFile} backed by the specified file. + * @param file the underlying file + * @throws IllegalArgumentException if the file is null or does not exist + */ + public RandomAccessDataFile(File file) { + if (file == null) { + throw new IllegalArgumentException("File must not be null"); + } + this.fileAccess = new FileAccess(file); + this.offset = 0L; + this.length = file.length(); + } + + /** + * Private constructor used to create a {@link #getSubsection(long, long) subsection}. + * @param fileAccess provides access to the underlying file + * @param offset the offset of the section + * @param length the length of the section + */ + private RandomAccessDataFile(FileAccess fileAccess, long offset, long length) { + this.fileAccess = fileAccess; + this.offset = offset; + this.length = length; + } + + /** + * Returns the underlying File. + * @return the underlying file + */ + public File getFile() { + return this.fileAccess.file; + } + + @Override + public InputStream getInputStream() throws IOException { + return new DataInputStream(); + } + + @Override + public RandomAccessData getSubsection(long offset, long length) { + if (offset < 0 || length < 0 || offset + length > this.length) { + throw new IndexOutOfBoundsException(); + } + return new RandomAccessDataFile(this.fileAccess, this.offset + offset, length); + } + + @Override + public byte[] read() throws IOException { + return read(0, this.length); + } + + @Override + public byte[] read(long offset, long length) throws IOException { + if (offset > this.length) { + throw new IndexOutOfBoundsException(); + } + if (offset + length > this.length) { + throw new EOFException(); + } + byte[] bytes = new byte[(int) length]; + read(bytes, offset, 0, bytes.length); + return bytes; + } + + private int readByte(long position) throws IOException { + if (position >= this.length) { + return -1; + } + return this.fileAccess.readByte(this.offset + position); + } + + private int read(byte[] bytes, long position, int offset, int length) throws IOException { + if (position > this.length) { + return -1; + } + return this.fileAccess.read(bytes, this.offset + position, offset, length); + } + + @Override + public long getSize() { + return this.length; + } + + public void close() throws IOException { + this.fileAccess.close(); + } + + /** + * {@link InputStream} implementation for the {@link RandomAccessDataFile}. + */ + private class DataInputStream extends InputStream { + + private int position; + + @Override + public int read() throws IOException { + int read = RandomAccessDataFile.this.readByte(this.position); + if (read > -1) { + moveOn(1); + } + return read; + } + + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, (b != null) ? b.length : 0); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (b == null) { + throw new NullPointerException("Bytes must not be null"); + } + return doRead(b, off, len); + } + + /** + * Perform the actual read. + * @param b the bytes to read or {@code null} when reading a single byte + * @param off the offset of the byte array + * @param len the length of data to read + * @return the number of bytes read into {@code b} or the actual read byte if + * {@code b} is {@code null}. Returns -1 when the end of the stream is reached + * @throws IOException in case of I/O errors + */ + int doRead(byte[] b, int off, int len) throws IOException { + if (len == 0) { + return 0; + } + int cappedLen = cap(len); + if (cappedLen <= 0) { + return -1; + } + return (int) moveOn(RandomAccessDataFile.this.read(b, this.position, off, cappedLen)); + } + + @Override + public long skip(long n) throws IOException { + return (n <= 0) ? 0 : moveOn(cap(n)); + } + + @Override + public int available() throws IOException { + return (int) RandomAccessDataFile.this.length - this.position; + } + + /** + * Cap the specified value such that it cannot exceed the number of bytes + * remaining. + * @param n the value to cap + * @return the capped value + */ + private int cap(long n) { + return (int) Math.min(RandomAccessDataFile.this.length - this.position, n); + } + + /** + * Move the stream position forwards the specified amount. + * @param amount the amount to move + * @return the amount moved + */ + private long moveOn(int amount) { + this.position += amount; + return amount; + } + + } + + private static final class FileAccess { + + private final Object monitor = new Object(); + + private final File file; + + private RandomAccessFile randomAccessFile; + + private FileAccess(File file) { + this.file = file; + openIfNecessary(); + } + + private int read(byte[] bytes, long position, int offset, int length) throws IOException { + synchronized (this.monitor) { + openIfNecessary(); + this.randomAccessFile.seek(position); + return this.randomAccessFile.read(bytes, offset, length); + } + } + + private void openIfNecessary() { + if (this.randomAccessFile == null) { + try { + this.randomAccessFile = new RandomAccessFile(this.file, "r"); + } + catch (FileNotFoundException ex) { + throw new IllegalArgumentException( + String.format("File %s must exist", this.file.getAbsolutePath())); + } + } + } + + private void close() throws IOException { + synchronized (this.monitor) { + if (this.randomAccessFile != null) { + this.randomAccessFile.close(); + this.randomAccessFile = null; + } + } + } + + private int readByte(long position) throws IOException { + synchronized (this.monitor) { + openIfNecessary(); + this.randomAccessFile.seek(position); + return this.randomAccessFile.read(); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/package-info.java new file mode 100644 index 000000000000..34bf2ead4378 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Classes and interfaces to allow random access to a block of data. + * + * @see org.springframework.boot.loader.data.RandomAccessData + */ +package org.springframework.boot.loader.data; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java new file mode 100644 index 000000000000..6a98ef682189 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.Permission; + +/** + * Base class for extended variants of {@link java.util.jar.JarFile}. + * + * @author Phillip Webb + */ +abstract class AbstractJarFile extends java.util.jar.JarFile { + + /** + * Create a new {@link AbstractJarFile}. + * @param file the root jar file. + * @throws IOException on IO error + */ + AbstractJarFile(File file) throws IOException { + super(file); + } + + /** + * Return a URL that can be used to access this JAR file. NOTE: the specified URL + * cannot be serialized and or cloned. + * @return the URL + * @throws MalformedURLException if the URL is malformed + */ + abstract URL getUrl() throws MalformedURLException; + + /** + * Return the {@link JarFileType} of this instance. + * @return the jar file type + */ + abstract JarFileType getType(); + + /** + * Return the security permission for this JAR. + * @return the security permission. + */ + abstract Permission getPermission(); + + /** + * Return an {@link InputStream} for the entire jar contents. + * @return the contents input stream + * @throws IOException on IO error + */ + abstract InputStream getInputStream() throws IOException; + + /** + * The type of a {@link JarFile}. + */ + enum JarFileType { + + DIRECT, NESTED_DIRECTORY, NESTED_JAR + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java new file mode 100644 index 000000000000..cfe121b68996 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java @@ -0,0 +1,255 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.nio.charset.StandardCharsets; + +/** + * Simple wrapper around a byte array that represents an ASCII. Used for performance + * reasons to save constructing Strings for ZIP data. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +final class AsciiBytes { + + private static final String EMPTY_STRING = ""; + + private static final int[] INITIAL_BYTE_BITMASK = { 0x7F, 0x1F, 0x0F, 0x07 }; + + private static final int SUBSEQUENT_BYTE_BITMASK = 0x3F; + + private final byte[] bytes; + + private final int offset; + + private final int length; + + private String string; + + private int hash; + + /** + * Create a new {@link AsciiBytes} from the specified String. + * @param string the source string + */ + AsciiBytes(String string) { + this(string.getBytes(StandardCharsets.UTF_8)); + this.string = string; + } + + /** + * Create a new {@link AsciiBytes} from the specified bytes. NOTE: underlying bytes + * are not expected to change. + * @param bytes the source bytes + */ + AsciiBytes(byte[] bytes) { + this(bytes, 0, bytes.length); + } + + /** + * Create a new {@link AsciiBytes} from the specified bytes. NOTE: underlying bytes + * are not expected to change. + * @param bytes the source bytes + * @param offset the offset + * @param length the length + */ + AsciiBytes(byte[] bytes, int offset, int length) { + if (offset < 0 || length < 0 || (offset + length) > bytes.length) { + throw new IndexOutOfBoundsException(); + } + this.bytes = bytes; + this.offset = offset; + this.length = length; + } + + int length() { + return this.length; + } + + boolean startsWith(AsciiBytes prefix) { + if (this == prefix) { + return true; + } + if (prefix.length > this.length) { + return false; + } + for (int i = 0; i < prefix.length; i++) { + if (this.bytes[i + this.offset] != prefix.bytes[i + prefix.offset]) { + return false; + } + } + return true; + } + + boolean endsWith(AsciiBytes postfix) { + if (this == postfix) { + return true; + } + if (postfix.length > this.length) { + return false; + } + for (int i = 0; i < postfix.length; i++) { + if (this.bytes[this.offset + (this.length - 1) - i] != postfix.bytes[postfix.offset + (postfix.length - 1) + - i]) { + return false; + } + } + return true; + } + + AsciiBytes substring(int beginIndex) { + return substring(beginIndex, this.length); + } + + AsciiBytes substring(int beginIndex, int endIndex) { + int length = endIndex - beginIndex; + if (this.offset + length > this.bytes.length) { + throw new IndexOutOfBoundsException(); + } + return new AsciiBytes(this.bytes, this.offset + beginIndex, length); + } + + boolean matches(CharSequence name, char suffix) { + int charIndex = 0; + int nameLen = name.length(); + int totalLen = nameLen + ((suffix != 0) ? 1 : 0); + for (int i = this.offset; i < this.offset + this.length; i++) { + int b = this.bytes[i]; + int remainingUtfBytes = getNumberOfUtfBytes(b) - 1; + b &= INITIAL_BYTE_BITMASK[remainingUtfBytes]; + for (int j = 0; j < remainingUtfBytes; j++) { + b = (b << 6) + (this.bytes[++i] & SUBSEQUENT_BYTE_BITMASK); + } + char c = getChar(name, suffix, charIndex++); + if (b <= 0xFFFF) { + if (c != b) { + return false; + } + } + else { + if (c != ((b >> 0xA) + 0xD7C0)) { + return false; + } + c = getChar(name, suffix, charIndex++); + if (c != ((b & 0x3FF) + 0xDC00)) { + return false; + } + } + } + return charIndex == totalLen; + } + + private char getChar(CharSequence name, char suffix, int index) { + if (index < name.length()) { + return name.charAt(index); + } + if (index == name.length()) { + return suffix; + } + return 0; + } + + private int getNumberOfUtfBytes(int b) { + if ((b & 0x80) == 0) { + return 1; + } + int numberOfUtfBytes = 0; + while ((b & 0x80) != 0) { + b <<= 1; + numberOfUtfBytes++; + } + return numberOfUtfBytes; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (obj.getClass() == AsciiBytes.class) { + AsciiBytes other = (AsciiBytes) obj; + if (this.length == other.length) { + for (int i = 0; i < this.length; i++) { + if (this.bytes[this.offset + i] != other.bytes[other.offset + i]) { + return false; + } + } + return true; + } + } + return false; + } + + @Override + public int hashCode() { + int hash = this.hash; + if (hash == 0 && this.bytes.length > 0) { + for (int i = this.offset; i < this.offset + this.length; i++) { + int b = this.bytes[i]; + int remainingUtfBytes = getNumberOfUtfBytes(b) - 1; + b &= INITIAL_BYTE_BITMASK[remainingUtfBytes]; + for (int j = 0; j < remainingUtfBytes; j++) { + b = (b << 6) + (this.bytes[++i] & SUBSEQUENT_BYTE_BITMASK); + } + if (b <= 0xFFFF) { + hash = 31 * hash + b; + } + else { + hash = 31 * hash + ((b >> 0xA) + 0xD7C0); + hash = 31 * hash + ((b & 0x3FF) + 0xDC00); + } + } + this.hash = hash; + } + return hash; + } + + @Override + public String toString() { + if (this.string == null) { + if (this.length == 0) { + this.string = EMPTY_STRING; + } + else { + this.string = new String(this.bytes, this.offset, this.length, StandardCharsets.UTF_8); + } + } + return this.string; + } + + static String toString(byte[] bytes) { + return new String(bytes, StandardCharsets.UTF_8); + } + + static int hashCode(CharSequence charSequence) { + // We're compatible with String's hashCode() + if (charSequence instanceof StringSequence) { + // ... but save making an unnecessary String for StringSequence + return charSequence.hashCode(); + } + return charSequence.toString().hashCode(); + } + + static int hashCode(int hash, char suffix) { + return (suffix != 0) ? (31 * hash + suffix) : hash; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java new file mode 100644 index 000000000000..d46a22555dcb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +/** + * Utilities for dealing with bytes from ZIP files. + * + * @author Phillip Webb + */ +final class Bytes { + + private Bytes() { + } + + static long littleEndianValue(byte[] bytes, int offset, int length) { + long value = 0; + for (int i = length - 1; i >= 0; i--) { + value = ((value << 8) | (bytes[offset + i] & 0xFF)); + } + return value; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java new file mode 100644 index 000000000000..61db0b73f422 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java @@ -0,0 +1,258 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.io.IOException; + +import org.springframework.boot.loader.data.RandomAccessData; + +/** + * A ZIP File "End of central directory record" (EOCD). + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Camille Vienot + * @see Zip File Format + */ +class CentralDirectoryEndRecord { + + private static final int MINIMUM_SIZE = 22; + + private static final int MAXIMUM_COMMENT_LENGTH = 0xFFFF; + + private static final int MAXIMUM_SIZE = MINIMUM_SIZE + MAXIMUM_COMMENT_LENGTH; + + private static final int SIGNATURE = 0x06054b50; + + private static final int COMMENT_LENGTH_OFFSET = 20; + + private static final int READ_BLOCK_SIZE = 256; + + private final Zip64End zip64End; + + private byte[] block; + + private int offset; + + private int size; + + /** + * Create a new {@link CentralDirectoryEndRecord} instance from the specified + * {@link RandomAccessData}, searching backwards from the end until a valid block is + * located. + * @param data the source data + * @throws IOException in case of I/O errors + */ + CentralDirectoryEndRecord(RandomAccessData data) throws IOException { + this.block = createBlockFromEndOfData(data, READ_BLOCK_SIZE); + this.size = MINIMUM_SIZE; + this.offset = this.block.length - this.size; + while (!isValid()) { + this.size++; + if (this.size > this.block.length) { + if (this.size >= MAXIMUM_SIZE || this.size > data.getSize()) { + throw new IOException( + "Unable to find ZIP central directory records after reading " + this.size + " bytes"); + } + this.block = createBlockFromEndOfData(data, this.size + READ_BLOCK_SIZE); + } + this.offset = this.block.length - this.size; + } + long startOfCentralDirectoryEndRecord = data.getSize() - this.size; + Zip64Locator zip64Locator = Zip64Locator.find(data, startOfCentralDirectoryEndRecord); + this.zip64End = (zip64Locator != null) ? new Zip64End(data, zip64Locator) : null; + } + + private byte[] createBlockFromEndOfData(RandomAccessData data, int size) throws IOException { + int length = (int) Math.min(data.getSize(), size); + return data.read(data.getSize() - length, length); + } + + private boolean isValid() { + if (this.block.length < MINIMUM_SIZE || Bytes.littleEndianValue(this.block, this.offset + 0, 4) != SIGNATURE) { + return false; + } + // Total size must be the structure size + comment + long commentLength = Bytes.littleEndianValue(this.block, this.offset + COMMENT_LENGTH_OFFSET, 2); + return this.size == MINIMUM_SIZE + commentLength; + } + + /** + * Returns the location in the data that the archive actually starts. For most files + * the archive data will start at 0, however, it is possible to have prefixed bytes + * (often used for startup scripts) at the beginning of the data. + * @param data the source data + * @return the offset within the data where the archive begins + */ + long getStartOfArchive(RandomAccessData data) { + long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4); + long specifiedOffset = (this.zip64End != null) ? this.zip64End.centralDirectoryOffset + : Bytes.littleEndianValue(this.block, this.offset + 16, 4); + long zip64EndSize = (this.zip64End != null) ? this.zip64End.getSize() : 0L; + int zip64LocSize = (this.zip64End != null) ? Zip64Locator.ZIP64_LOCSIZE : 0; + long actualOffset = data.getSize() - this.size - length - zip64EndSize - zip64LocSize; + return actualOffset - specifiedOffset; + } + + /** + * Return the bytes of the "Central directory" based on the offset indicated in this + * record. + * @param data the source data + * @return the central directory data + */ + RandomAccessData getCentralDirectory(RandomAccessData data) { + if (this.zip64End != null) { + return this.zip64End.getCentralDirectory(data); + } + long offset = Bytes.littleEndianValue(this.block, this.offset + 16, 4); + long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4); + return data.getSubsection(offset, length); + } + + /** + * Return the number of ZIP entries in the file. + * @return the number of records in the zip + */ + int getNumberOfRecords() { + if (this.zip64End != null) { + return this.zip64End.getNumberOfRecords(); + } + long numberOfRecords = Bytes.littleEndianValue(this.block, this.offset + 10, 2); + return (int) numberOfRecords; + } + + String getComment() { + int commentLength = (int) Bytes.littleEndianValue(this.block, this.offset + COMMENT_LENGTH_OFFSET, 2); + AsciiBytes comment = new AsciiBytes(this.block, this.offset + COMMENT_LENGTH_OFFSET + 2, commentLength); + return comment.toString(); + } + + boolean isZip64() { + return this.zip64End != null; + } + + /** + * A Zip64 end of central directory record. + * + * @see Chapter + * 4.3.14 of Zip64 specification + */ + private static final class Zip64End { + + private static final int ZIP64_ENDTOT = 32; // total number of entries + + private static final int ZIP64_ENDSIZ = 40; // central directory size in bytes + + private static final int ZIP64_ENDOFF = 48; // offset of first CEN header + + private final Zip64Locator locator; + + private final long centralDirectoryOffset; + + private final long centralDirectoryLength; + + private final int numberOfRecords; + + private Zip64End(RandomAccessData data, Zip64Locator locator) throws IOException { + this.locator = locator; + byte[] block = data.read(locator.getZip64EndOffset(), 56); + this.centralDirectoryOffset = Bytes.littleEndianValue(block, ZIP64_ENDOFF, 8); + this.centralDirectoryLength = Bytes.littleEndianValue(block, ZIP64_ENDSIZ, 8); + this.numberOfRecords = (int) Bytes.littleEndianValue(block, ZIP64_ENDTOT, 8); + } + + /** + * Return the size of this zip 64 end of central directory record. + * @return size of this zip 64 end of central directory record + */ + private long getSize() { + return this.locator.getZip64EndSize(); + } + + /** + * Return the bytes of the "Central directory" based on the offset indicated in + * this record. + * @param data the source data + * @return the central directory data + */ + private RandomAccessData getCentralDirectory(RandomAccessData data) { + return data.getSubsection(this.centralDirectoryOffset, this.centralDirectoryLength); + } + + /** + * Return the number of entries in the zip64 archive. + * @return the number of records in the zip + */ + private int getNumberOfRecords() { + return this.numberOfRecords; + } + + } + + /** + * A Zip64 end of central directory locator. + * + * @see Chapter + * 4.3.15 of Zip64 specification + */ + private static final class Zip64Locator { + + static final int SIGNATURE = 0x07064b50; + + static final int ZIP64_LOCSIZE = 20; // locator size + + static final int ZIP64_LOCOFF = 8; // offset of zip64 end + + private final long zip64EndOffset; + + private final long offset; + + private Zip64Locator(long offset, byte[] block) { + this.offset = offset; + this.zip64EndOffset = Bytes.littleEndianValue(block, ZIP64_LOCOFF, 8); + } + + /** + * Return the size of the zip 64 end record located by this zip64 end locator. + * @return size of the zip 64 end record located by this zip64 end locator + */ + private long getZip64EndSize() { + return this.offset - this.zip64EndOffset; + } + + /** + * Return the offset to locate {@link Zip64End}. + * @return offset of the Zip64 end of central directory record + */ + private long getZip64EndOffset() { + return this.zip64EndOffset; + } + + private static Zip64Locator find(RandomAccessData data, long centralDirectoryEndOffset) throws IOException { + long offset = centralDirectoryEndOffset - ZIP64_LOCSIZE; + if (offset >= 0) { + byte[] block = data.read(offset, ZIP64_LOCSIZE); + if (Bytes.littleEndianValue(block, 0, 4) == SIGNATURE) { + return new Zip64Locator(offset, block); + } + } + return null; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java new file mode 100644 index 000000000000..19c88dda5241 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java @@ -0,0 +1,222 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.io.IOException; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.time.temporal.ValueRange; + +import org.springframework.boot.loader.data.RandomAccessData; + +/** + * A ZIP File "Central directory file header record" (CDFH). + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Dmytro Nosan + * @see Zip File Format + */ + +final class CentralDirectoryFileHeader implements FileHeader { + + private static final AsciiBytes SLASH = new AsciiBytes("/"); + + private static final byte[] NO_EXTRA = {}; + + private static final AsciiBytes NO_COMMENT = new AsciiBytes(""); + + private byte[] header; + + private int headerOffset; + + private AsciiBytes name; + + private byte[] extra; + + private AsciiBytes comment; + + private long localHeaderOffset; + + CentralDirectoryFileHeader() { + } + + CentralDirectoryFileHeader(byte[] header, int headerOffset, AsciiBytes name, byte[] extra, AsciiBytes comment, + long localHeaderOffset) { + this.header = header; + this.headerOffset = headerOffset; + this.name = name; + this.extra = extra; + this.comment = comment; + this.localHeaderOffset = localHeaderOffset; + } + + void load(byte[] data, int dataOffset, RandomAccessData variableData, long variableOffset, JarEntryFilter filter) + throws IOException { + // Load fixed part + this.header = data; + this.headerOffset = dataOffset; + long compressedSize = Bytes.littleEndianValue(data, dataOffset + 20, 4); + long uncompressedSize = Bytes.littleEndianValue(data, dataOffset + 24, 4); + long nameLength = Bytes.littleEndianValue(data, dataOffset + 28, 2); + long extraLength = Bytes.littleEndianValue(data, dataOffset + 30, 2); + long commentLength = Bytes.littleEndianValue(data, dataOffset + 32, 2); + long localHeaderOffset = Bytes.littleEndianValue(data, dataOffset + 42, 4); + // Load variable part + dataOffset += 46; + if (variableData != null) { + data = variableData.read(variableOffset + 46, nameLength + extraLength + commentLength); + dataOffset = 0; + } + this.name = new AsciiBytes(data, dataOffset, (int) nameLength); + if (filter != null) { + this.name = filter.apply(this.name); + } + this.extra = NO_EXTRA; + this.comment = NO_COMMENT; + if (extraLength > 0) { + this.extra = new byte[(int) extraLength]; + System.arraycopy(data, (int) (dataOffset + nameLength), this.extra, 0, this.extra.length); + } + this.localHeaderOffset = getLocalHeaderOffset(compressedSize, uncompressedSize, localHeaderOffset, this.extra); + if (commentLength > 0) { + this.comment = new AsciiBytes(data, (int) (dataOffset + nameLength + extraLength), (int) commentLength); + } + } + + private long getLocalHeaderOffset(long compressedSize, long uncompressedSize, long localHeaderOffset, byte[] extra) + throws IOException { + if (localHeaderOffset != 0xFFFFFFFFL) { + return localHeaderOffset; + } + int extraOffset = 0; + while (extraOffset < extra.length - 2) { + int id = (int) Bytes.littleEndianValue(extra, extraOffset, 2); + int length = (int) Bytes.littleEndianValue(extra, extraOffset, 2); + extraOffset += 4; + if (id == 1) { + int localHeaderExtraOffset = 0; + if (compressedSize == 0xFFFFFFFFL) { + localHeaderExtraOffset += 4; + } + if (uncompressedSize == 0xFFFFFFFFL) { + localHeaderExtraOffset += 4; + } + return Bytes.littleEndianValue(extra, extraOffset + localHeaderExtraOffset, 8); + } + extraOffset += length; + } + throw new IOException("Zip64 Extended Information Extra Field not found"); + } + + AsciiBytes getName() { + return this.name; + } + + @Override + public boolean hasName(CharSequence name, char suffix) { + return this.name.matches(name, suffix); + } + + boolean isDirectory() { + return this.name.endsWith(SLASH); + } + + @Override + public int getMethod() { + return (int) Bytes.littleEndianValue(this.header, this.headerOffset + 10, 2); + } + + long getTime() { + long datetime = Bytes.littleEndianValue(this.header, this.headerOffset + 12, 4); + return decodeMsDosFormatDateTime(datetime); + } + + /** + * Decode MS-DOS Date Time details. See + * Microsoft's documentation for more details of the format. + * @param datetime the date and time + * @return the date and time as milliseconds since the epoch + */ + private long decodeMsDosFormatDateTime(long datetime) { + int year = getChronoValue(((datetime >> 25) & 0x7f) + 1980, ChronoField.YEAR); + int month = getChronoValue((datetime >> 21) & 0x0f, ChronoField.MONTH_OF_YEAR); + int day = getChronoValue((datetime >> 16) & 0x1f, ChronoField.DAY_OF_MONTH); + int hour = getChronoValue((datetime >> 11) & 0x1f, ChronoField.HOUR_OF_DAY); + int minute = getChronoValue((datetime >> 5) & 0x3f, ChronoField.MINUTE_OF_HOUR); + int second = getChronoValue((datetime << 1) & 0x3e, ChronoField.SECOND_OF_MINUTE); + return ZonedDateTime.of(year, month, day, hour, minute, second, 0, ZoneId.systemDefault()) + .toInstant() + .truncatedTo(ChronoUnit.SECONDS) + .toEpochMilli(); + } + + long getCrc() { + return Bytes.littleEndianValue(this.header, this.headerOffset + 16, 4); + } + + @Override + public long getCompressedSize() { + return Bytes.littleEndianValue(this.header, this.headerOffset + 20, 4); + } + + @Override + public long getSize() { + return Bytes.littleEndianValue(this.header, this.headerOffset + 24, 4); + } + + byte[] getExtra() { + return this.extra; + } + + boolean hasExtra() { + return this.extra.length > 0; + } + + AsciiBytes getComment() { + return this.comment; + } + + @Override + public long getLocalHeaderOffset() { + return this.localHeaderOffset; + } + + @Override + public CentralDirectoryFileHeader clone() { + byte[] header = new byte[46]; + System.arraycopy(this.header, this.headerOffset, header, 0, header.length); + return new CentralDirectoryFileHeader(header, 0, this.name, header, this.comment, this.localHeaderOffset); + } + + static CentralDirectoryFileHeader fromRandomAccessData(RandomAccessData data, long offset, JarEntryFilter filter) + throws IOException { + CentralDirectoryFileHeader fileHeader = new CentralDirectoryFileHeader(); + byte[] bytes = data.read(offset, 46); + fileHeader.load(bytes, 0, data, offset, filter); + return fileHeader; + } + + private static int getChronoValue(long value, ChronoField field) { + ValueRange range = field.range(); + return Math.toIntExact(Math.min(Math.max(value, range.getMinimum()), range.getMaximum())); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java new file mode 100644 index 000000000000..eff96a56e2cc --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.boot.loader.data.RandomAccessData; + +/** + * Parses the central directory from a JAR file. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @see CentralDirectoryVisitor + */ +class CentralDirectoryParser { + + private static final int CENTRAL_DIRECTORY_HEADER_BASE_SIZE = 46; + + private final List visitors = new ArrayList<>(); + + T addVisitor(T visitor) { + this.visitors.add(visitor); + return visitor; + } + + /** + * Parse the source data, triggering {@link CentralDirectoryVisitor visitors}. + * @param data the source data + * @param skipPrefixBytes if prefix bytes should be skipped + * @return the actual archive data without any prefix bytes + * @throws IOException on error + */ + RandomAccessData parse(RandomAccessData data, boolean skipPrefixBytes) throws IOException { + CentralDirectoryEndRecord endRecord = new CentralDirectoryEndRecord(data); + if (skipPrefixBytes) { + data = getArchiveData(endRecord, data); + } + RandomAccessData centralDirectoryData = endRecord.getCentralDirectory(data); + visitStart(endRecord, centralDirectoryData); + parseEntries(endRecord, centralDirectoryData); + visitEnd(); + return data; + } + + private void parseEntries(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) + throws IOException { + byte[] bytes = centralDirectoryData.read(0, centralDirectoryData.getSize()); + CentralDirectoryFileHeader fileHeader = new CentralDirectoryFileHeader(); + int dataOffset = 0; + for (int i = 0; i < endRecord.getNumberOfRecords(); i++) { + fileHeader.load(bytes, dataOffset, null, 0, null); + visitFileHeader(dataOffset, fileHeader); + dataOffset += CENTRAL_DIRECTORY_HEADER_BASE_SIZE + fileHeader.getName().length() + + fileHeader.getComment().length() + fileHeader.getExtra().length; + } + } + + private RandomAccessData getArchiveData(CentralDirectoryEndRecord endRecord, RandomAccessData data) { + long offset = endRecord.getStartOfArchive(data); + if (offset == 0) { + return data; + } + return data.getSubsection(offset, data.getSize() - offset); + } + + private void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) { + for (CentralDirectoryVisitor visitor : this.visitors) { + visitor.visitStart(endRecord, centralDirectoryData); + } + } + + private void visitFileHeader(long dataOffset, CentralDirectoryFileHeader fileHeader) { + for (CentralDirectoryVisitor visitor : this.visitors) { + visitor.visitFileHeader(fileHeader, dataOffset); + } + } + + private void visitEnd() { + for (CentralDirectoryVisitor visitor : this.visitors) { + visitor.visitEnd(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java new file mode 100644 index 000000000000..22e04b329c30 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import org.springframework.boot.loader.data.RandomAccessData; + +/** + * Callback visitor triggered by {@link CentralDirectoryParser}. + * + * @author Phillip Webb + */ +interface CentralDirectoryVisitor { + + void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData); + + void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset); + + void visitEnd(); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java new file mode 100644 index 000000000000..7e4134fe5649 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.util.zip.ZipEntry; + +/** + * A file header record that has been loaded from a Jar file. + * + * @author Phillip Webb + * @see JarEntry + * @see CentralDirectoryFileHeader + */ +interface FileHeader { + + /** + * Returns {@code true} if the header has the given name. + * @param name the name to test + * @param suffix an additional suffix (or {@code 0}) + * @return {@code true} if the header has the given name + */ + boolean hasName(CharSequence name, char suffix); + + /** + * Return the offset of the load file header within the archive data. + * @return the local header offset + */ + long getLocalHeaderOffset(); + + /** + * Return the compressed size of the entry. + * @return the compressed size. + */ + long getCompressedSize(); + + /** + * Return the uncompressed size of the entry. + * @return the uncompressed size. + */ + long getSize(); + + /** + * Return the method used to compress the data. + * @return the zip compression method + * @see ZipEntry#STORED + * @see ZipEntry#DEFLATED + */ + int getMethod(); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java new file mode 100644 index 000000000000..932dea654867 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java @@ -0,0 +1,466 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.io.File; +import java.io.IOException; +import java.lang.ref.SoftReference; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +/** + * {@link URLStreamHandler} for Spring Boot loader {@link JarFile}s. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @since 1.0.0 + * @see JarFile#registerUrlProtocolHandler() + */ +public class Handler extends URLStreamHandler { + + // NOTE: in order to be found as a URL protocol handler, this class must be public, + // must be named Handler and must be in a package ending '.jar' + + private static final String JAR_PROTOCOL = "jar:"; + + private static final String FILE_PROTOCOL = "file:"; + + private static final String TOMCAT_WARFILE_PROTOCOL = "war:file:"; + + private static final String SEPARATOR = "!/"; + + private static final Pattern SEPARATOR_PATTERN = Pattern.compile(SEPARATOR, Pattern.LITERAL); + + private static final String CURRENT_DIR = "/./"; + + private static final Pattern CURRENT_DIR_PATTERN = Pattern.compile(CURRENT_DIR, Pattern.LITERAL); + + private static final String PARENT_DIR = "/../"; + + private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs"; + + private static final String[] FALLBACK_HANDLERS = { "sun.net.www.protocol.jar.Handler" }; + + private static URL jarContextUrl; + + private static SoftReference> rootFileCache; + + static { + rootFileCache = new SoftReference<>(null); + } + + private final JarFile jarFile; + + private URLStreamHandler fallbackHandler; + + public Handler() { + this(null); + } + + public Handler(JarFile jarFile) { + this.jarFile = jarFile; + } + + @Override + protected URLConnection openConnection(URL url) throws IOException { + if (this.jarFile != null && isUrlInJarFile(url, this.jarFile)) { + return JarURLConnection.get(url, this.jarFile); + } + try { + return JarURLConnection.get(url, getRootJarFileFromUrl(url)); + } + catch (Exception ex) { + return openFallbackConnection(url, ex); + } + } + + private boolean isUrlInJarFile(URL url, JarFile jarFile) throws MalformedURLException { + // Try the path first to save building a new url string each time + return url.getPath().startsWith(jarFile.getUrl().getPath()) + && url.toString().startsWith(jarFile.getUrlString()); + } + + private URLConnection openFallbackConnection(URL url, Exception reason) throws IOException { + try { + URLConnection connection = openFallbackTomcatConnection(url); + connection = (connection != null) ? connection : openFallbackContextConnection(url); + return (connection != null) ? connection : openFallbackHandlerConnection(url); + } + catch (Exception ex) { + if (reason instanceof IOException ioException) { + log(false, "Unable to open fallback handler", ex); + throw ioException; + } + log(true, "Unable to open fallback handler", ex); + if (reason instanceof RuntimeException runtimeException) { + throw runtimeException; + } + throw new IllegalStateException(reason); + } + } + + /** + * Attempt to open a Tomcat formatted 'jar:war:file:...' URL. This method allows us to + * use our own nested JAR support to open the content rather than the logic in + * {@code sun.net.www.protocol.jar.URLJarFile} which will extract the nested jar to + * the temp folder to that its content can be accessed. + * @param url the URL to open + * @return a {@link URLConnection} or {@code null} + */ + private URLConnection openFallbackTomcatConnection(URL url) { + String file = url.getFile(); + if (isTomcatWarUrl(file)) { + file = file.substring(TOMCAT_WARFILE_PROTOCOL.length()); + file = file.replaceFirst("\\*/", "!/"); + try { + URLConnection connection = openConnection(new URL("jar:file:" + file)); + connection.getInputStream().close(); + return connection; + } + catch (IOException ex) { + } + } + return null; + } + + private boolean isTomcatWarUrl(String file) { + if (file.startsWith(TOMCAT_WARFILE_PROTOCOL) || !file.contains("*/")) { + try { + URLConnection connection = new URL(file).openConnection(); + if (connection.getClass().getName().startsWith("org.apache.catalina")) { + return true; + } + } + catch (Exception ex) { + } + } + return false; + } + + /** + * Attempt to open a fallback connection by using a context URL captured before the + * jar handler was replaced with our own version. Since this method doesn't use + * reflection it won't trigger "illegal reflective access operation has occurred" + * warnings on Java 13+. + * @param url the URL to open + * @return a {@link URLConnection} or {@code null} + */ + private URLConnection openFallbackContextConnection(URL url) { + try { + if (jarContextUrl != null) { + return new URL(jarContextUrl, url.toExternalForm()).openConnection(); + } + } + catch (Exception ex) { + } + return null; + } + + /** + * Attempt to open a fallback connection by using reflection to access Java's default + * jar {@link URLStreamHandler}. + * @param url the URL to open + * @return the {@link URLConnection} + * @throws Exception if not connection could be opened + */ + private URLConnection openFallbackHandlerConnection(URL url) throws Exception { + URLStreamHandler fallbackHandler = getFallbackHandler(); + return new URL(null, url.toExternalForm(), fallbackHandler).openConnection(); + } + + private URLStreamHandler getFallbackHandler() { + if (this.fallbackHandler != null) { + return this.fallbackHandler; + } + for (String handlerClassName : FALLBACK_HANDLERS) { + try { + Class handlerClass = Class.forName(handlerClassName); + this.fallbackHandler = (URLStreamHandler) handlerClass.getDeclaredConstructor().newInstance(); + return this.fallbackHandler; + } + catch (Exception ex) { + // Ignore + } + } + throw new IllegalStateException("Unable to find fallback handler"); + } + + private void log(boolean warning, String message, Exception cause) { + try { + Level level = warning ? Level.WARNING : Level.FINEST; + Logger.getLogger(getClass().getName()).log(level, message, cause); + } + catch (Exception ex) { + if (warning) { + System.err.println("WARNING: " + message); + } + } + } + + @Override + protected void parseURL(URL context, String spec, int start, int limit) { + if (spec.regionMatches(true, 0, JAR_PROTOCOL, 0, JAR_PROTOCOL.length())) { + setFile(context, getFileFromSpec(spec.substring(start, limit))); + } + else { + setFile(context, getFileFromContext(context, spec.substring(start, limit))); + } + } + + private String getFileFromSpec(String spec) { + int separatorIndex = spec.lastIndexOf("!/"); + if (separatorIndex == -1) { + throw new IllegalArgumentException("No !/ in spec '" + spec + "'"); + } + try { + new URL(spec.substring(0, separatorIndex)); + return spec; + } + catch (MalformedURLException ex) { + throw new IllegalArgumentException("Invalid spec URL '" + spec + "'", ex); + } + } + + private String getFileFromContext(URL context, String spec) { + String file = context.getFile(); + if (spec.startsWith("/")) { + return trimToJarRoot(file) + SEPARATOR + spec.substring(1); + } + if (file.endsWith("/")) { + return file + spec; + } + int lastSlashIndex = file.lastIndexOf('/'); + if (lastSlashIndex == -1) { + throw new IllegalArgumentException("No / found in context URL's file '" + file + "'"); + } + return file.substring(0, lastSlashIndex + 1) + spec; + } + + private String trimToJarRoot(String file) { + int lastSeparatorIndex = file.lastIndexOf(SEPARATOR); + if (lastSeparatorIndex == -1) { + throw new IllegalArgumentException("No !/ found in context URL's file '" + file + "'"); + } + return file.substring(0, lastSeparatorIndex); + } + + private void setFile(URL context, String file) { + String path = normalize(file); + String query = null; + int queryIndex = path.lastIndexOf('?'); + if (queryIndex != -1) { + query = path.substring(queryIndex + 1); + path = path.substring(0, queryIndex); + } + setURL(context, JAR_PROTOCOL, null, -1, null, null, path, query, context.getRef()); + } + + private String normalize(String file) { + if (!file.contains(CURRENT_DIR) && !file.contains(PARENT_DIR)) { + return file; + } + int afterLastSeparatorIndex = file.lastIndexOf(SEPARATOR) + SEPARATOR.length(); + String afterSeparator = file.substring(afterLastSeparatorIndex); + afterSeparator = replaceParentDir(afterSeparator); + afterSeparator = replaceCurrentDir(afterSeparator); + return file.substring(0, afterLastSeparatorIndex) + afterSeparator; + } + + private String replaceParentDir(String file) { + int parentDirIndex; + while ((parentDirIndex = file.indexOf(PARENT_DIR)) >= 0) { + int precedingSlashIndex = file.lastIndexOf('/', parentDirIndex - 1); + if (precedingSlashIndex >= 0) { + file = file.substring(0, precedingSlashIndex) + file.substring(parentDirIndex + 3); + } + else { + file = file.substring(parentDirIndex + 4); + } + } + return file; + } + + private String replaceCurrentDir(String file) { + return CURRENT_DIR_PATTERN.matcher(file).replaceAll("/"); + } + + @Override + protected int hashCode(URL u) { + return hashCode(u.getProtocol(), u.getFile()); + } + + private int hashCode(String protocol, String file) { + int result = (protocol != null) ? protocol.hashCode() : 0; + int separatorIndex = file.indexOf(SEPARATOR); + if (separatorIndex == -1) { + return result + file.hashCode(); + } + String source = file.substring(0, separatorIndex); + String entry = canonicalize(file.substring(separatorIndex + 2)); + try { + result += new URL(source).hashCode(); + } + catch (MalformedURLException ex) { + result += source.hashCode(); + } + result += entry.hashCode(); + return result; + } + + @Override + protected boolean sameFile(URL u1, URL u2) { + if (!u1.getProtocol().equals("jar") || !u2.getProtocol().equals("jar")) { + return false; + } + int separator1 = u1.getFile().indexOf(SEPARATOR); + int separator2 = u2.getFile().indexOf(SEPARATOR); + if (separator1 == -1 || separator2 == -1) { + return super.sameFile(u1, u2); + } + String nested1 = u1.getFile().substring(separator1 + SEPARATOR.length()); + String nested2 = u2.getFile().substring(separator2 + SEPARATOR.length()); + if (!nested1.equals(nested2)) { + String canonical1 = canonicalize(nested1); + String canonical2 = canonicalize(nested2); + if (!canonical1.equals(canonical2)) { + return false; + } + } + String root1 = u1.getFile().substring(0, separator1); + String root2 = u2.getFile().substring(0, separator2); + try { + return super.sameFile(new URL(root1), new URL(root2)); + } + catch (MalformedURLException ex) { + // Continue + } + return super.sameFile(u1, u2); + } + + private String canonicalize(String path) { + return SEPARATOR_PATTERN.matcher(path).replaceAll("/"); + } + + public JarFile getRootJarFileFromUrl(URL url) throws IOException { + String spec = url.getFile(); + int separatorIndex = spec.indexOf(SEPARATOR); + if (separatorIndex == -1) { + throw new MalformedURLException("Jar URL does not contain !/ separator"); + } + String name = spec.substring(0, separatorIndex); + return getRootJarFile(name); + } + + private JarFile getRootJarFile(String name) throws IOException { + try { + if (!name.startsWith(FILE_PROTOCOL)) { + throw new IllegalStateException("Not a file URL"); + } + File file = new File(URI.create(name)); + Map cache = rootFileCache.get(); + JarFile result = (cache != null) ? cache.get(file) : null; + if (result == null) { + result = new JarFile(file); + addToRootFileCache(file, result); + } + return result; + } + catch (Exception ex) { + throw new IOException("Unable to open root Jar file '" + name + "'", ex); + } + } + + /** + * Add the given {@link JarFile} to the root file cache. + * @param sourceFile the source file to add + * @param jarFile the jar file. + */ + static void addToRootFileCache(File sourceFile, JarFile jarFile) { + Map cache = rootFileCache.get(); + if (cache == null) { + cache = new ConcurrentHashMap<>(); + rootFileCache = new SoftReference<>(cache); + } + cache.put(sourceFile, jarFile); + } + + /** + * If possible, capture a URL that is configured with the original jar handler so that + * we can use it as a fallback context later. We can only do this if we know that we + * can reset the handlers after. + */ + static void captureJarContextUrl() { + if (canResetCachedUrlHandlers()) { + String handlers = System.getProperty(PROTOCOL_HANDLER); + try { + System.clearProperty(PROTOCOL_HANDLER); + try { + resetCachedUrlHandlers(); + jarContextUrl = new URL("jar:file:context.jar!/"); + URLConnection connection = jarContextUrl.openConnection(); + if (connection instanceof JarURLConnection) { + jarContextUrl = null; + } + } + catch (Exception ex) { + } + } + finally { + if (handlers == null) { + System.clearProperty(PROTOCOL_HANDLER); + } + else { + System.setProperty(PROTOCOL_HANDLER, handlers); + } + } + resetCachedUrlHandlers(); + } + } + + private static boolean canResetCachedUrlHandlers() { + try { + resetCachedUrlHandlers(); + return true; + } + catch (Error ex) { + return false; + } + } + + private static void resetCachedUrlHandlers() { + URL.setURLStreamHandlerFactory(null); + } + + /** + * Set if a generic static exception can be thrown when a URL cannot be connected. + * This optimization is used during class loading to save creating lots of exceptions + * which are then swallowed. + * @param useFastConnectionExceptions if fast connection exceptions can be used. + */ + public static void setUseFastConnectionExceptions(boolean useFastConnectionExceptions) { + JarURLConnection.setUseFastExceptions(useFastConnectionExceptions); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java new file mode 100644 index 000000000000..8f54dc3070df --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java @@ -0,0 +1,120 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.CodeSigner; +import java.security.cert.Certificate; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +/** + * Extended variant of {@link java.util.jar.JarEntry} returned by {@link JarFile}s. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +class JarEntry extends java.util.jar.JarEntry implements FileHeader { + + private final int index; + + private final AsciiBytes name; + + private final AsciiBytes headerName; + + private final JarFile jarFile; + + private final long localHeaderOffset; + + private volatile JarEntryCertification certification; + + JarEntry(JarFile jarFile, int index, CentralDirectoryFileHeader header, AsciiBytes nameAlias) { + super((nameAlias != null) ? nameAlias.toString() : header.getName().toString()); + this.index = index; + this.name = (nameAlias != null) ? nameAlias : header.getName(); + this.headerName = header.getName(); + this.jarFile = jarFile; + this.localHeaderOffset = header.getLocalHeaderOffset(); + setCompressedSize(header.getCompressedSize()); + setMethod(header.getMethod()); + setCrc(header.getCrc()); + setComment(header.getComment().toString()); + setSize(header.getSize()); + setTime(header.getTime()); + if (header.hasExtra()) { + setExtra(header.getExtra()); + } + } + + int getIndex() { + return this.index; + } + + AsciiBytes getAsciiBytesName() { + return this.name; + } + + @Override + public boolean hasName(CharSequence name, char suffix) { + return this.headerName.matches(name, suffix); + } + + /** + * Return a {@link URL} for this {@link JarEntry}. + * @return the URL for the entry + * @throws MalformedURLException if the URL is not valid + */ + URL getUrl() throws MalformedURLException { + return new URL(this.jarFile.getUrl(), getName()); + } + + @Override + public Attributes getAttributes() throws IOException { + Manifest manifest = this.jarFile.getManifest(); + return (manifest != null) ? manifest.getAttributes(getName()) : null; + } + + @Override + public Certificate[] getCertificates() { + return getCertification().getCertificates(); + } + + @Override + public CodeSigner[] getCodeSigners() { + return getCertification().getCodeSigners(); + } + + private JarEntryCertification getCertification() { + if (!this.jarFile.isSigned()) { + return JarEntryCertification.NONE; + } + JarEntryCertification certification = this.certification; + if (certification == null) { + certification = this.jarFile.getCertification(this); + this.certification = certification; + } + return certification; + } + + @Override + public long getLocalHeaderOffset() { + return this.localHeaderOffset; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java new file mode 100644 index 000000000000..ffd629e09428 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.security.CodeSigner; +import java.security.cert.Certificate; + +/** + * {@link Certificate} and {@link CodeSigner} details for a {@link JarEntry} from a signed + * {@link JarFile}. + * + * @author Phillip Webb + */ +class JarEntryCertification { + + static final JarEntryCertification NONE = new JarEntryCertification(null, null); + + private final Certificate[] certificates; + + private final CodeSigner[] codeSigners; + + JarEntryCertification(Certificate[] certificates, CodeSigner[] codeSigners) { + this.certificates = certificates; + this.codeSigners = codeSigners; + } + + Certificate[] getCertificates() { + return (this.certificates != null) ? this.certificates.clone() : null; + } + + CodeSigner[] getCodeSigners() { + return (this.codeSigners != null) ? this.codeSigners.clone() : null; + } + + static JarEntryCertification from(java.util.jar.JarEntry certifiedEntry) { + Certificate[] certificates = (certifiedEntry != null) ? certifiedEntry.getCertificates() : null; + CodeSigner[] codeSigners = (certifiedEntry != null) ? certifiedEntry.getCodeSigners() : null; + if (certificates == null && codeSigners == null) { + return NONE; + } + return new JarEntryCertification(certificates, codeSigners); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java new file mode 100644 index 000000000000..6804f0ba37f9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +/** + * Interface that can be used to filter and optionally rename jar entries. + * + * @author Phillip Webb + */ +interface JarEntryFilter { + + /** + * Apply the jar entry filter. + * @param name the current entry name. This may be different that the original entry + * name if a previous filter has been applied + * @return the new name of the entry or {@code null} if the entry should not be + * included. + */ + AsciiBytes apply(AsciiBytes name); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java new file mode 100644 index 000000000000..6e548048dbf0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java @@ -0,0 +1,475 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.io.File; +import java.io.FilePermission; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.SoftReference; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; +import java.security.Permission; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Supplier; +import java.util.jar.Manifest; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import java.util.zip.ZipEntry; + +import org.springframework.boot.loader.data.RandomAccessData; +import org.springframework.boot.loader.data.RandomAccessDataFile; + +/** + * Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but + * offers the following additional functionality. + *
      + *
    • A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based + * on any directory entry.
    • + *
    • A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for + * embedded JAR files (as long as their entry is not compressed).
    • + *
    + * + * @author Phillip Webb + * @author Andy Wilkinson + * @since 1.0.0 + */ +public class JarFile extends AbstractJarFile implements Iterable { + + private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; + + private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs"; + + private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader"; + + private static final AsciiBytes META_INF = new AsciiBytes("META-INF/"); + + private static final AsciiBytes SIGNATURE_FILE_EXTENSION = new AsciiBytes(".SF"); + + private static final String READ_ACTION = "read"; + + private final RandomAccessDataFile rootFile; + + private final String pathFromRoot; + + private final RandomAccessData data; + + private final JarFileType type; + + private URL url; + + private String urlString; + + private final JarFileEntries entries; + + private final Supplier manifestSupplier; + + private SoftReference manifest; + + private boolean signed; + + private String comment; + + private volatile boolean closed; + + private volatile JarFileWrapper wrapper; + + /** + * Create a new {@link JarFile} backed by the specified file. + * @param file the root jar file + * @throws IOException if the file cannot be read + */ + public JarFile(File file) throws IOException { + this(new RandomAccessDataFile(file)); + } + + /** + * Create a new {@link JarFile} backed by the specified file. + * @param file the root jar file + * @throws IOException if the file cannot be read + */ + JarFile(RandomAccessDataFile file) throws IOException { + this(file, "", file, JarFileType.DIRECT); + } + + /** + * Private constructor used to create a new {@link JarFile} either directly or from a + * nested entry. + * @param rootFile the root jar file + * @param pathFromRoot the name of this file + * @param data the underlying data + * @param type the type of the jar file + * @throws IOException if the file cannot be read + */ + private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, RandomAccessData data, JarFileType type) + throws IOException { + this(rootFile, pathFromRoot, data, null, type, null); + } + + private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, RandomAccessData data, JarEntryFilter filter, + JarFileType type, Supplier manifestSupplier) throws IOException { + super(rootFile.getFile()); + super.close(); + this.rootFile = rootFile; + this.pathFromRoot = pathFromRoot; + CentralDirectoryParser parser = new CentralDirectoryParser(); + this.entries = parser.addVisitor(new JarFileEntries(this, filter)); + this.type = type; + parser.addVisitor(centralDirectoryVisitor()); + try { + this.data = parser.parse(data, filter == null); + } + catch (RuntimeException ex) { + try { + this.rootFile.close(); + super.close(); + } + catch (IOException ioex) { + } + throw ex; + } + this.manifestSupplier = (manifestSupplier != null) ? manifestSupplier : () -> { + try (InputStream inputStream = getInputStream(MANIFEST_NAME)) { + if (inputStream == null) { + return null; + } + return new Manifest(inputStream); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + }; + } + + private CentralDirectoryVisitor centralDirectoryVisitor() { + return new CentralDirectoryVisitor() { + + @Override + public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) { + JarFile.this.comment = endRecord.getComment(); + } + + @Override + public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) { + AsciiBytes name = fileHeader.getName(); + if (name.startsWith(META_INF) && name.endsWith(SIGNATURE_FILE_EXTENSION)) { + JarFile.this.signed = true; + } + } + + @Override + public void visitEnd() { + } + + }; + } + + JarFileWrapper getWrapper() throws IOException { + JarFileWrapper wrapper = this.wrapper; + if (wrapper == null) { + wrapper = new JarFileWrapper(this); + this.wrapper = wrapper; + } + return wrapper; + } + + @Override + Permission getPermission() { + return new FilePermission(this.rootFile.getFile().getPath(), READ_ACTION); + } + + protected final RandomAccessDataFile getRootJarFile() { + return this.rootFile; + } + + RandomAccessData getData() { + return this.data; + } + + @Override + public Manifest getManifest() throws IOException { + Manifest manifest = (this.manifest != null) ? this.manifest.get() : null; + if (manifest == null) { + try { + manifest = this.manifestSupplier.get(); + } + catch (RuntimeException ex) { + throw new IOException(ex); + } + this.manifest = new SoftReference<>(manifest); + } + return manifest; + } + + @Override + public Enumeration entries() { + return new JarEntryEnumeration(this.entries.iterator()); + } + + @Override + public Stream stream() { + Spliterator spliterator = Spliterators.spliterator(iterator(), size(), + Spliterator.ORDERED | Spliterator.DISTINCT | Spliterator.IMMUTABLE | Spliterator.NONNULL); + return StreamSupport.stream(spliterator, false); + } + + /** + * Return an iterator for the contained entries. + * @since 2.3.0 + * @see java.lang.Iterable#iterator() + */ + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Iterator iterator() { + return (Iterator) this.entries.iterator(this::ensureOpen); + } + + public JarEntry getJarEntry(CharSequence name) { + return this.entries.getEntry(name); + } + + @Override + public JarEntry getJarEntry(String name) { + return (JarEntry) getEntry(name); + } + + public boolean containsEntry(String name) { + return this.entries.containsEntry(name); + } + + @Override + public ZipEntry getEntry(String name) { + ensureOpen(); + return this.entries.getEntry(name); + } + + @Override + InputStream getInputStream() throws IOException { + return this.data.getInputStream(); + } + + @Override + public synchronized InputStream getInputStream(ZipEntry entry) throws IOException { + ensureOpen(); + if (entry instanceof JarEntry jarEntry) { + return this.entries.getInputStream(jarEntry); + } + return getInputStream((entry != null) ? entry.getName() : null); + } + + InputStream getInputStream(String name) throws IOException { + return this.entries.getInputStream(name); + } + + /** + * Return a nested {@link JarFile} loaded from the specified entry. + * @param entry the zip entry + * @return a {@link JarFile} for the entry + * @throws IOException if the nested jar file cannot be read + */ + public synchronized JarFile getNestedJarFile(ZipEntry entry) throws IOException { + return getNestedJarFile((JarEntry) entry); + } + + /** + * Return a nested {@link JarFile} loaded from the specified entry. + * @param entry the zip entry + * @return a {@link JarFile} for the entry + * @throws IOException if the nested jar file cannot be read + */ + public synchronized JarFile getNestedJarFile(JarEntry entry) throws IOException { + try { + return createJarFileFromEntry(entry); + } + catch (Exception ex) { + throw new IOException("Unable to open nested jar file '" + entry.getName() + "'", ex); + } + } + + private JarFile createJarFileFromEntry(JarEntry entry) throws IOException { + if (entry.isDirectory()) { + return createJarFileFromDirectoryEntry(entry); + } + return createJarFileFromFileEntry(entry); + } + + private JarFile createJarFileFromDirectoryEntry(JarEntry entry) throws IOException { + AsciiBytes name = entry.getAsciiBytesName(); + JarEntryFilter filter = (candidate) -> { + if (candidate.startsWith(name) && !candidate.equals(name)) { + return candidate.substring(name.length()); + } + return null; + }; + return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName().substring(0, name.length() - 1), + this.data, filter, JarFileType.NESTED_DIRECTORY, this.manifestSupplier); + } + + private JarFile createJarFileFromFileEntry(JarEntry entry) throws IOException { + if (entry.getMethod() != ZipEntry.STORED) { + throw new IllegalStateException( + "Unable to open nested entry '" + entry.getName() + "'. It has been compressed and nested " + + "jar files must be stored without compression. Please check the " + + "mechanism used to create your executable jar file"); + } + RandomAccessData entryData = this.entries.getEntryData(entry.getName()); + return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName(), entryData, + JarFileType.NESTED_JAR); + } + + @Override + public String getComment() { + ensureOpen(); + return this.comment; + } + + @Override + public int size() { + ensureOpen(); + return this.entries.getSize(); + } + + @Override + public void close() throws IOException { + if (this.closed) { + return; + } + super.close(); + if (this.type == JarFileType.DIRECT) { + this.rootFile.close(); + } + this.closed = true; + } + + private void ensureOpen() { + if (this.closed) { + throw new IllegalStateException("zip file closed"); + } + } + + boolean isClosed() { + return this.closed; + } + + String getUrlString() throws MalformedURLException { + if (this.urlString == null) { + this.urlString = getUrl().toString(); + } + return this.urlString; + } + + @Override + public URL getUrl() throws MalformedURLException { + if (this.url == null) { + String file = this.rootFile.getFile().toURI() + this.pathFromRoot + "!/"; + file = file.replace("file:////", "file://"); // Fix UNC paths + this.url = new URL("jar", "", -1, file, new Handler(this)); + } + return this.url; + } + + @Override + public String toString() { + return getName(); + } + + @Override + public String getName() { + return this.rootFile.getFile() + this.pathFromRoot; + } + + boolean isSigned() { + return this.signed; + } + + JarEntryCertification getCertification(JarEntry entry) { + try { + return this.entries.getCertification(entry); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + public void clearCache() { + this.entries.clearCache(); + } + + protected String getPathFromRoot() { + return this.pathFromRoot; + } + + @Override + JarFileType getType() { + return this.type; + } + + /** + * Register a {@literal 'java.protocol.handler.pkgs'} property so that a + * {@link URLStreamHandler} will be located to deal with jar URLs. + */ + public static void registerUrlProtocolHandler() { + Handler.captureJarContextUrl(); + String handlers = System.getProperty(PROTOCOL_HANDLER, ""); + System.setProperty(PROTOCOL_HANDLER, + ((handlers == null || handlers.isEmpty()) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE)); + resetCachedUrlHandlers(); + } + + /** + * Reset any cached handlers just in case a jar protocol has already been used. We + * reset the handler by trying to set a null {@link URLStreamHandlerFactory} which + * should have no effect other than clearing the handlers cache. + */ + private static void resetCachedUrlHandlers() { + try { + URL.setURLStreamHandlerFactory(null); + } + catch (Error ex) { + // Ignore + } + } + + /** + * An {@link Enumeration} on {@linkplain java.util.jar.JarEntry jar entries}. + */ + private static class JarEntryEnumeration implements Enumeration { + + private final Iterator iterator; + + JarEntryEnumeration(Iterator iterator) { + this.iterator = iterator; + } + + @Override + public boolean hasMoreElements() { + return this.iterator.hasNext(); + } + + @Override + public java.util.jar.JarEntry nextElement() { + return this.iterator.next(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java new file mode 100644 index 000000000000..d151c8d80a85 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java @@ -0,0 +1,491 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.jar.Attributes; +import java.util.jar.Attributes.Name; +import java.util.jar.JarInputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; + +import org.springframework.boot.loader.data.RandomAccessData; + +/** + * Provides access to entries from a {@link JarFile}. In order to reduce memory + * consumption entry details are stored using arrays. The {@code hashCodes} array stores + * the hash code of the entry name, the {@code centralDirectoryOffsets} provides the + * offset to the central directory record and {@code positions} provides the original + * order position of the entry. The arrays are stored in hashCode order so that a binary + * search can be used to find a name. + *

    + * A typical Spring Boot application will have somewhere in the region of 10,500 entries + * which should consume about 122K. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +class JarFileEntries implements CentralDirectoryVisitor, Iterable { + + private static final Runnable NO_VALIDATION = () -> { + }; + + private static final String META_INF_PREFIX = "META-INF/"; + + private static final Name MULTI_RELEASE = new Name("Multi-Release"); + + private static final int BASE_VERSION = 8; + + private static final int RUNTIME_VERSION = Runtime.version().feature(); + + private static final long LOCAL_FILE_HEADER_SIZE = 30; + + private static final char SLASH = '/'; + + private static final char NO_SUFFIX = 0; + + protected static final int ENTRY_CACHE_SIZE = 25; + + private final JarFile jarFile; + + private final JarEntryFilter filter; + + private RandomAccessData centralDirectoryData; + + private int size; + + private int[] hashCodes; + + private Offsets centralDirectoryOffsets; + + private int[] positions; + + private Boolean multiReleaseJar; + + private JarEntryCertification[] certifications; + + private final Map entriesCache = Collections + .synchronizedMap(new LinkedHashMap<>(16, 0.75f, true) { + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() >= ENTRY_CACHE_SIZE; + } + + }); + + JarFileEntries(JarFile jarFile, JarEntryFilter filter) { + this.jarFile = jarFile; + this.filter = filter; + } + + @Override + public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) { + int maxSize = endRecord.getNumberOfRecords(); + this.centralDirectoryData = centralDirectoryData; + this.hashCodes = new int[maxSize]; + this.centralDirectoryOffsets = Offsets.from(endRecord); + this.positions = new int[maxSize]; + } + + @Override + public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) { + AsciiBytes name = applyFilter(fileHeader.getName()); + if (name != null) { + add(name, dataOffset); + } + } + + private void add(AsciiBytes name, long dataOffset) { + this.hashCodes[this.size] = name.hashCode(); + this.centralDirectoryOffsets.set(this.size, dataOffset); + this.positions[this.size] = this.size; + this.size++; + } + + @Override + public void visitEnd() { + sort(0, this.size - 1); + int[] positions = this.positions; + this.positions = new int[positions.length]; + for (int i = 0; i < this.size; i++) { + this.positions[positions[i]] = i; + } + } + + int getSize() { + return this.size; + } + + private void sort(int left, int right) { + // Quick sort algorithm, uses hashCodes as the source but sorts all arrays + if (left < right) { + int pivot = this.hashCodes[left + (right - left) / 2]; + int i = left; + int j = right; + while (i <= j) { + while (this.hashCodes[i] < pivot) { + i++; + } + while (this.hashCodes[j] > pivot) { + j--; + } + if (i <= j) { + swap(i, j); + i++; + j--; + } + } + if (left < j) { + sort(left, j); + } + if (right > i) { + sort(i, right); + } + } + } + + private void swap(int i, int j) { + swap(this.hashCodes, i, j); + this.centralDirectoryOffsets.swap(i, j); + swap(this.positions, i, j); + } + + @Override + public Iterator iterator() { + return new EntryIterator(NO_VALIDATION); + } + + Iterator iterator(Runnable validator) { + return new EntryIterator(validator); + } + + boolean containsEntry(CharSequence name) { + return getEntry(name, FileHeader.class, true) != null; + } + + JarEntry getEntry(CharSequence name) { + return getEntry(name, JarEntry.class, true); + } + + InputStream getInputStream(String name) throws IOException { + FileHeader entry = getEntry(name, FileHeader.class, false); + return getInputStream(entry); + } + + InputStream getInputStream(FileHeader entry) throws IOException { + if (entry == null) { + return null; + } + InputStream inputStream = getEntryData(entry).getInputStream(); + if (entry.getMethod() == ZipEntry.DEFLATED) { + inputStream = new ZipInflaterInputStream(inputStream, (int) entry.getSize()); + } + return inputStream; + } + + RandomAccessData getEntryData(String name) throws IOException { + FileHeader entry = getEntry(name, FileHeader.class, false); + if (entry == null) { + return null; + } + return getEntryData(entry); + } + + private RandomAccessData getEntryData(FileHeader entry) throws IOException { + // aspectjrt-1.7.4.jar has a different ext bytes length in the + // local directory to the central directory. We need to re-read + // here to skip them + RandomAccessData data = this.jarFile.getData(); + byte[] localHeader = data.read(entry.getLocalHeaderOffset(), LOCAL_FILE_HEADER_SIZE); + long nameLength = Bytes.littleEndianValue(localHeader, 26, 2); + long extraLength = Bytes.littleEndianValue(localHeader, 28, 2); + return data.getSubsection(entry.getLocalHeaderOffset() + LOCAL_FILE_HEADER_SIZE + nameLength + extraLength, + entry.getCompressedSize()); + } + + private T getEntry(CharSequence name, Class type, boolean cacheEntry) { + T entry = doGetEntry(name, type, cacheEntry, null); + if (!isMetaInfEntry(name) && isMultiReleaseJar()) { + int version = RUNTIME_VERSION; + AsciiBytes nameAlias = (entry instanceof JarEntry jarEntry) ? jarEntry.getAsciiBytesName() + : new AsciiBytes(name.toString()); + while (version > BASE_VERSION) { + T versionedEntry = doGetEntry("META-INF/versions/" + version + "/" + name, type, cacheEntry, nameAlias); + if (versionedEntry != null) { + return versionedEntry; + } + version--; + } + } + return entry; + } + + private boolean isMetaInfEntry(CharSequence name) { + return name.toString().startsWith(META_INF_PREFIX); + } + + private boolean isMultiReleaseJar() { + Boolean multiRelease = this.multiReleaseJar; + if (multiRelease != null) { + return multiRelease; + } + try { + Manifest manifest = this.jarFile.getManifest(); + if (manifest == null) { + multiRelease = false; + } + else { + Attributes attributes = manifest.getMainAttributes(); + multiRelease = attributes.containsKey(MULTI_RELEASE); + } + } + catch (IOException ex) { + multiRelease = false; + } + this.multiReleaseJar = multiRelease; + return multiRelease; + } + + private T doGetEntry(CharSequence name, Class type, boolean cacheEntry, + AsciiBytes nameAlias) { + int hashCode = AsciiBytes.hashCode(name); + T entry = getEntry(hashCode, name, NO_SUFFIX, type, cacheEntry, nameAlias); + if (entry == null) { + hashCode = AsciiBytes.hashCode(hashCode, SLASH); + entry = getEntry(hashCode, name, SLASH, type, cacheEntry, nameAlias); + } + return entry; + } + + private T getEntry(int hashCode, CharSequence name, char suffix, Class type, + boolean cacheEntry, AsciiBytes nameAlias) { + int index = getFirstIndex(hashCode); + while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) { + T entry = getEntry(index, type, cacheEntry, nameAlias); + if (entry.hasName(name, suffix)) { + return entry; + } + index++; + } + return null; + } + + @SuppressWarnings("unchecked") + private T getEntry(int index, Class type, boolean cacheEntry, AsciiBytes nameAlias) { + try { + long offset = this.centralDirectoryOffsets.get(index); + FileHeader cached = this.entriesCache.get(index); + FileHeader entry = (cached != null) ? cached + : CentralDirectoryFileHeader.fromRandomAccessData(this.centralDirectoryData, offset, this.filter); + if (CentralDirectoryFileHeader.class.equals(entry.getClass()) && type.equals(JarEntry.class)) { + entry = new JarEntry(this.jarFile, index, (CentralDirectoryFileHeader) entry, nameAlias); + } + if (cacheEntry && cached != entry) { + this.entriesCache.put(index, entry); + } + return (T) entry; + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + private int getFirstIndex(int hashCode) { + int index = Arrays.binarySearch(this.hashCodes, 0, this.size, hashCode); + if (index < 0) { + return -1; + } + while (index > 0 && this.hashCodes[index - 1] == hashCode) { + index--; + } + return index; + } + + void clearCache() { + this.entriesCache.clear(); + } + + private AsciiBytes applyFilter(AsciiBytes name) { + return (this.filter != null) ? this.filter.apply(name) : name; + } + + JarEntryCertification getCertification(JarEntry entry) throws IOException { + JarEntryCertification[] certifications = this.certifications; + if (certifications == null) { + certifications = new JarEntryCertification[this.size]; + // We fall back to use JarInputStream to obtain the certs. This isn't that + // fast, but hopefully doesn't happen too often. + try (JarInputStream certifiedJarStream = new JarInputStream(this.jarFile.getData().getInputStream())) { + java.util.jar.JarEntry certifiedEntry; + while ((certifiedEntry = certifiedJarStream.getNextJarEntry()) != null) { + // Entry must be closed to trigger a read and set entry certificates + certifiedJarStream.closeEntry(); + int index = getEntryIndex(certifiedEntry.getName()); + if (index != -1) { + certifications[index] = JarEntryCertification.from(certifiedEntry); + } + } + } + this.certifications = certifications; + } + JarEntryCertification certification = certifications[entry.getIndex()]; + return (certification != null) ? certification : JarEntryCertification.NONE; + } + + private int getEntryIndex(CharSequence name) { + int hashCode = AsciiBytes.hashCode(name); + int index = getFirstIndex(hashCode); + while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) { + FileHeader candidate = getEntry(index, FileHeader.class, false, null); + if (candidate.hasName(name, NO_SUFFIX)) { + return index; + } + index++; + } + return -1; + } + + private static void swap(int[] array, int i, int j) { + int temp = array[i]; + array[i] = array[j]; + array[j] = temp; + } + + private static void swap(long[] array, int i, int j) { + long temp = array[i]; + array[i] = array[j]; + array[j] = temp; + } + + /** + * Iterator for contained entries. + */ + private final class EntryIterator implements Iterator { + + private final Runnable validator; + + private int index = 0; + + private EntryIterator(Runnable validator) { + this.validator = validator; + validator.run(); + } + + @Override + public boolean hasNext() { + this.validator.run(); + return this.index < JarFileEntries.this.size; + } + + @Override + public JarEntry next() { + this.validator.run(); + if (!hasNext()) { + throw new NoSuchElementException(); + } + int entryIndex = JarFileEntries.this.positions[this.index]; + this.index++; + return getEntry(entryIndex, JarEntry.class, false, null); + } + + } + + /** + * Interface to manage offsets to central directory records. Regular zip files are + * backed by an {@code int[]} based implementation, Zip64 files are backed by a + * {@code long[]} and will consume more memory. + */ + private interface Offsets { + + void set(int index, long value); + + long get(int index); + + void swap(int i, int j); + + static Offsets from(CentralDirectoryEndRecord endRecord) { + int size = endRecord.getNumberOfRecords(); + return endRecord.isZip64() ? new Zip64Offsets(size) : new ZipOffsets(size); + } + + } + + /** + * {@link Offsets} implementation for regular zip files. + */ + private static final class ZipOffsets implements Offsets { + + private final int[] offsets; + + private ZipOffsets(int size) { + this.offsets = new int[size]; + } + + @Override + public void swap(int i, int j) { + JarFileEntries.swap(this.offsets, i, j); + } + + @Override + public void set(int index, long value) { + this.offsets[index] = (int) value; + } + + @Override + public long get(int index) { + return this.offsets[index]; + } + + } + + /** + * {@link Offsets} implementation for zip64 files. + */ + private static final class Zip64Offsets implements Offsets { + + private final long[] offsets; + + private Zip64Offsets(int size) { + this.offsets = new long[size]; + } + + @Override + public void swap(int i, int j) { + JarFileEntries.swap(this.offsets, i, j); + } + + @Override + public void set(int index, long value) { + this.offsets[index] = value; + } + + @Override + public long get(int index) { + return this.offsets[index]; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java new file mode 100644 index 000000000000..b65358947ad1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java @@ -0,0 +1,126 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.Permission; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.Manifest; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; + +/** + * A wrapper used to create a copy of a {@link JarFile} so that it can be safely closed + * without closing the original. + * + * @author Phillip Webb + */ +class JarFileWrapper extends AbstractJarFile { + + private final JarFile parent; + + JarFileWrapper(JarFile parent) throws IOException { + super(parent.getRootJarFile().getFile()); + this.parent = parent; + super.close(); + } + + @Override + URL getUrl() throws MalformedURLException { + return this.parent.getUrl(); + } + + @Override + JarFileType getType() { + return this.parent.getType(); + } + + @Override + Permission getPermission() { + return this.parent.getPermission(); + } + + @Override + public Manifest getManifest() throws IOException { + return this.parent.getManifest(); + } + + @Override + public Enumeration entries() { + return this.parent.entries(); + } + + @Override + public Stream stream() { + return this.parent.stream(); + } + + @Override + public JarEntry getJarEntry(String name) { + return this.parent.getJarEntry(name); + } + + @Override + public ZipEntry getEntry(String name) { + return this.parent.getEntry(name); + } + + @Override + InputStream getInputStream() throws IOException { + return this.parent.getInputStream(); + } + + @Override + public synchronized InputStream getInputStream(ZipEntry ze) throws IOException { + return this.parent.getInputStream(ze); + } + + @Override + public String getComment() { + return this.parent.getComment(); + } + + @Override + public int size() { + return this.parent.size(); + } + + @Override + public String toString() { + return this.parent.toString(); + } + + @Override + public String getName() { + return this.parent.getName(); + } + + static JarFile unwrap(java.util.jar.JarFile jarFile) { + if (jarFile instanceof JarFile file) { + return file; + } + if (jarFile instanceof JarFileWrapper wrapper) { + return unwrap(wrapper.parent); + } + throw new IllegalStateException("Not a JarFile or Wrapper"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java new file mode 100644 index 000000000000..859ae88ab000 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java @@ -0,0 +1,393 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLEncoder; +import java.net.URLStreamHandler; +import java.security.Permission; + +/** + * {@link java.net.JarURLConnection} used to support {@link JarFile#getUrl()}. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Rostyslav Dudka + */ +final class JarURLConnection extends java.net.JarURLConnection { + + private static final ThreadLocal useFastExceptions = new ThreadLocal<>(); + + private static final FileNotFoundException FILE_NOT_FOUND_EXCEPTION = new FileNotFoundException( + "Jar file or entry not found"); + + private static final IllegalStateException NOT_FOUND_CONNECTION_EXCEPTION = new IllegalStateException( + FILE_NOT_FOUND_EXCEPTION); + + private static final String SEPARATOR = "!/"; + + private static final URL EMPTY_JAR_URL; + + static { + try { + EMPTY_JAR_URL = new URL("jar:", null, 0, "file:!/", new URLStreamHandler() { + @Override + protected URLConnection openConnection(URL u) throws IOException { + // Stub URLStreamHandler to prevent the wrong JAR Handler from being + // Instantiated and cached. + return null; + } + }); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + + private static final JarEntryName EMPTY_JAR_ENTRY_NAME = new JarEntryName(new StringSequence("")); + + private static final JarURLConnection NOT_FOUND_CONNECTION = JarURLConnection.notFound(); + + private final AbstractJarFile jarFile; + + private Permission permission; + + private URL jarFileUrl; + + private final JarEntryName jarEntryName; + + private java.util.jar.JarEntry jarEntry; + + private JarURLConnection(URL url, AbstractJarFile jarFile, JarEntryName jarEntryName) throws IOException { + // What we pass to super is ultimately ignored + super(EMPTY_JAR_URL); + this.url = url; + this.jarFile = jarFile; + this.jarEntryName = jarEntryName; + } + + @Override + public void connect() throws IOException { + if (this.jarFile == null) { + throw FILE_NOT_FOUND_EXCEPTION; + } + if (!this.jarEntryName.isEmpty() && this.jarEntry == null) { + this.jarEntry = this.jarFile.getJarEntry(getEntryName()); + if (this.jarEntry == null) { + throwFileNotFound(this.jarEntryName, this.jarFile); + } + } + this.connected = true; + } + + @Override + public java.util.jar.JarFile getJarFile() throws IOException { + connect(); + return this.jarFile; + } + + @Override + public URL getJarFileURL() { + if (this.jarFile == null) { + throw NOT_FOUND_CONNECTION_EXCEPTION; + } + if (this.jarFileUrl == null) { + this.jarFileUrl = buildJarFileUrl(); + } + return this.jarFileUrl; + } + + private URL buildJarFileUrl() { + try { + String spec = this.jarFile.getUrl().getFile(); + if (spec.endsWith(SEPARATOR)) { + spec = spec.substring(0, spec.length() - SEPARATOR.length()); + } + if (!spec.contains(SEPARATOR)) { + return new URL(spec); + } + return new URL("jar:" + spec); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + + @Override + public java.util.jar.JarEntry getJarEntry() throws IOException { + if (this.jarEntryName == null || this.jarEntryName.isEmpty()) { + return null; + } + connect(); + return this.jarEntry; + } + + @Override + public String getEntryName() { + if (this.jarFile == null) { + throw NOT_FOUND_CONNECTION_EXCEPTION; + } + return this.jarEntryName.toString(); + } + + @Override + public InputStream getInputStream() throws IOException { + if (this.jarFile == null) { + throw FILE_NOT_FOUND_EXCEPTION; + } + if (this.jarEntryName.isEmpty() && this.jarFile.getType() == JarFile.JarFileType.DIRECT) { + throw new IOException("no entry name specified"); + } + connect(); + InputStream inputStream = (this.jarEntryName.isEmpty() ? this.jarFile.getInputStream() + : this.jarFile.getInputStream(this.jarEntry)); + if (inputStream == null) { + throwFileNotFound(this.jarEntryName, this.jarFile); + } + return inputStream; + } + + private void throwFileNotFound(Object entry, AbstractJarFile jarFile) throws FileNotFoundException { + if (Boolean.TRUE.equals(useFastExceptions.get())) { + throw FILE_NOT_FOUND_EXCEPTION; + } + throw new FileNotFoundException("JAR entry " + entry + " not found in " + jarFile.getName()); + } + + @Override + public int getContentLength() { + long length = getContentLengthLong(); + if (length > Integer.MAX_VALUE) { + return -1; + } + return (int) length; + } + + @Override + public long getContentLengthLong() { + if (this.jarFile == null) { + return -1; + } + try { + if (this.jarEntryName.isEmpty()) { + return this.jarFile.size(); + } + java.util.jar.JarEntry entry = getJarEntry(); + return (entry != null) ? (int) entry.getSize() : -1; + } + catch (IOException ex) { + return -1; + } + } + + @Override + public Object getContent() throws IOException { + connect(); + return this.jarEntryName.isEmpty() ? this.jarFile : super.getContent(); + } + + @Override + public String getContentType() { + return (this.jarEntryName != null) ? this.jarEntryName.getContentType() : null; + } + + @Override + public Permission getPermission() throws IOException { + if (this.jarFile == null) { + throw FILE_NOT_FOUND_EXCEPTION; + } + if (this.permission == null) { + this.permission = this.jarFile.getPermission(); + } + return this.permission; + } + + @Override + public long getLastModified() { + if (this.jarFile == null || this.jarEntryName.isEmpty()) { + return 0; + } + try { + java.util.jar.JarEntry entry = getJarEntry(); + return (entry != null) ? entry.getTime() : 0; + } + catch (IOException ex) { + return 0; + } + } + + static void setUseFastExceptions(boolean useFastExceptions) { + JarURLConnection.useFastExceptions.set(useFastExceptions); + } + + static JarURLConnection get(URL url, JarFile jarFile) throws IOException { + StringSequence spec = new StringSequence(url.getFile()); + int index = indexOfRootSpec(spec, jarFile.getPathFromRoot()); + if (index == -1) { + return (Boolean.TRUE.equals(useFastExceptions.get()) ? NOT_FOUND_CONNECTION + : new JarURLConnection(url, null, EMPTY_JAR_ENTRY_NAME)); + } + int separator; + while ((separator = spec.indexOf(SEPARATOR, index)) > 0) { + JarEntryName entryName = JarEntryName.get(spec.subSequence(index, separator)); + JarEntry jarEntry = jarFile.getJarEntry(entryName.toCharSequence()); + if (jarEntry == null) { + return JarURLConnection.notFound(jarFile, entryName); + } + jarFile = jarFile.getNestedJarFile(jarEntry); + index = separator + SEPARATOR.length(); + } + JarEntryName jarEntryName = JarEntryName.get(spec, index); + if (Boolean.TRUE.equals(useFastExceptions.get()) && !jarEntryName.isEmpty() + && !jarFile.containsEntry(jarEntryName.toString())) { + return NOT_FOUND_CONNECTION; + } + return new JarURLConnection(url, jarFile.getWrapper(), jarEntryName); + } + + private static int indexOfRootSpec(StringSequence file, String pathFromRoot) { + int separatorIndex = file.indexOf(SEPARATOR); + if (separatorIndex < 0 || !file.startsWith(pathFromRoot, separatorIndex)) { + return -1; + } + return separatorIndex + SEPARATOR.length() + pathFromRoot.length(); + } + + private static JarURLConnection notFound() { + try { + return notFound(null, null); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + private static JarURLConnection notFound(JarFile jarFile, JarEntryName jarEntryName) throws IOException { + if (Boolean.TRUE.equals(useFastExceptions.get())) { + return NOT_FOUND_CONNECTION; + } + return new JarURLConnection(null, jarFile, jarEntryName); + } + + /** + * A JarEntryName parsed from a URL String. + */ + static class JarEntryName { + + private final StringSequence name; + + private String contentType; + + JarEntryName(StringSequence spec) { + this.name = decode(spec); + } + + private StringSequence decode(StringSequence source) { + if (source.isEmpty() || (source.indexOf('%') < 0)) { + return source; + } + ByteArrayOutputStream bos = new ByteArrayOutputStream(source.length()); + write(source.toString(), bos); + // AsciiBytes is what is used to store the JarEntries so make it symmetric + return new StringSequence(AsciiBytes.toString(bos.toByteArray())); + } + + private void write(String source, ByteArrayOutputStream outputStream) { + int length = source.length(); + for (int i = 0; i < length; i++) { + int c = source.charAt(i); + if (c > 127) { + try { + String encoded = URLEncoder.encode(String.valueOf((char) c), "UTF-8"); + write(encoded, outputStream); + } + catch (UnsupportedEncodingException ex) { + throw new IllegalStateException(ex); + } + } + else { + if (c == '%') { + if ((i + 2) >= length) { + throw new IllegalArgumentException( + "Invalid encoded sequence \"" + source.substring(i) + "\""); + } + c = decodeEscapeSequence(source, i); + i += 2; + } + outputStream.write(c); + } + } + } + + private char decodeEscapeSequence(String source, int i) { + int hi = Character.digit(source.charAt(i + 1), 16); + int lo = Character.digit(source.charAt(i + 2), 16); + if (hi == -1 || lo == -1) { + throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\""); + } + return ((char) ((hi << 4) + lo)); + } + + CharSequence toCharSequence() { + return this.name; + } + + @Override + public String toString() { + return this.name.toString(); + } + + boolean isEmpty() { + return this.name.isEmpty(); + } + + String getContentType() { + if (this.contentType == null) { + this.contentType = deduceContentType(); + } + return this.contentType; + } + + private String deduceContentType() { + // Guess the content type, don't bother with streams as mark is not supported + String type = isEmpty() ? "x-java/jar" : null; + type = (type != null) ? type : guessContentTypeFromName(toString()); + type = (type != null) ? type : "content/unknown"; + return type; + } + + static JarEntryName get(StringSequence spec) { + return get(spec, 0); + } + + static JarEntryName get(StringSequence spec, int beginIndex) { + if (spec.length() <= beginIndex) { + return EMPTY_JAR_ENTRY_NAME; + } + return new JarEntryName(spec.subSequence(beginIndex)); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java new file mode 100644 index 000000000000..12850a4ebe3e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java @@ -0,0 +1,157 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.util.Objects; + +/** + * A {@link CharSequence} backed by a single shared {@link String}. Unlike a regular + * {@link String}, {@link #subSequence(int, int)} operations will not copy the underlying + * character array. + * + * @author Phillip Webb + */ +final class StringSequence implements CharSequence { + + private final String source; + + private final int start; + + private final int end; + + private int hash; + + StringSequence(String source) { + this(source, 0, (source != null) ? source.length() : -1); + } + + StringSequence(String source, int start, int end) { + Objects.requireNonNull(source, "Source must not be null"); + if (start < 0) { + throw new StringIndexOutOfBoundsException(start); + } + if (end > source.length()) { + throw new StringIndexOutOfBoundsException(end); + } + this.source = source; + this.start = start; + this.end = end; + } + + StringSequence subSequence(int start) { + return subSequence(start, length()); + } + + @Override + public StringSequence subSequence(int start, int end) { + int subSequenceStart = this.start + start; + int subSequenceEnd = this.start + end; + if (subSequenceStart > this.end) { + throw new StringIndexOutOfBoundsException(start); + } + if (subSequenceEnd > this.end) { + throw new StringIndexOutOfBoundsException(end); + } + if (start == 0 && subSequenceEnd == this.end) { + return this; + } + return new StringSequence(this.source, subSequenceStart, subSequenceEnd); + } + + /** + * Returns {@code true} if the sequence is empty. Public to be compatible with JDK 15. + * @return {@code true} if {@link #length()} is {@code 0}, otherwise {@code false} + */ + public boolean isEmpty() { + return length() == 0; + } + + @Override + public int length() { + return this.end - this.start; + } + + @Override + public char charAt(int index) { + return this.source.charAt(this.start + index); + } + + int indexOf(char ch) { + return this.source.indexOf(ch, this.start) - this.start; + } + + int indexOf(String str) { + return this.source.indexOf(str, this.start) - this.start; + } + + int indexOf(String str, int fromIndex) { + return this.source.indexOf(str, this.start + fromIndex) - this.start; + } + + boolean startsWith(String prefix) { + return startsWith(prefix, 0); + } + + boolean startsWith(String prefix, int offset) { + int prefixLength = prefix.length(); + int length = length(); + if (length - prefixLength - offset < 0) { + return false; + } + return this.source.startsWith(prefix, this.start + offset); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof CharSequence other)) { + return false; + } + int n = length(); + if (n != other.length()) { + return false; + } + int i = 0; + while (n-- != 0) { + if (charAt(i) != other.charAt(i)) { + return false; + } + i++; + } + return true; + } + + @Override + public int hashCode() { + int hash = this.hash; + if (hash == 0 && length() > 0) { + for (int i = this.start; i < this.end; i++) { + hash = 31 * hash + this.source.charAt(i); + } + this.hash = hash; + } + return hash; + } + + @Override + public String toString() { + return this.source.substring(this.start, this.end); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java new file mode 100644 index 000000000000..67624460ccd7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +/** + * {@link InflaterInputStream} that supports the writing of an extra "dummy" byte (which + * is required with JDK 6) and returns accurate available() results. + * + * @author Phillip Webb + */ +class ZipInflaterInputStream extends InflaterInputStream { + + private int available; + + private boolean extraBytesWritten; + + ZipInflaterInputStream(InputStream inputStream, int size) { + super(inputStream, new Inflater(true), getInflaterBufferSize(size)); + this.available = size; + } + + @Override + public int available() throws IOException { + if (this.available < 0) { + return super.available(); + } + return this.available; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int result = super.read(b, off, len); + if (result != -1) { + this.available -= result; + } + return result; + } + + @Override + public void close() throws IOException { + super.close(); + this.inf.end(); + } + + @Override + protected void fill() throws IOException { + try { + super.fill(); + } + catch (EOFException ex) { + if (this.extraBytesWritten) { + throw ex; + } + this.len = 1; + this.buf[0] = 0x0; + this.extraBytesWritten = true; + this.inf.setInput(this.buf, 0, this.len); + } + } + + private static int getInflaterBufferSize(long size) { + size += 2; // inflater likes some space + size = (size > 65536) ? 8192 : size; + size = (size <= 0) ? 4096 : size; + return (int) size; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/package-info.java new file mode 100644 index 000000000000..638afe45f497 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Support for loading and manipulating JAR/WAR files. + */ +package org.springframework.boot.loader.jar; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java new file mode 100644 index 000000000000..162e4a6a7396 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2023 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.loader.jarmode; + +/** + * Interface registered in {@code spring.factories} to provides extended 'jarmode' + * support. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public interface JarMode { + + /** + * Returns if this accepts and can run the given mode. + * @param mode the mode to check + * @return if this instance accepts the mode + */ + boolean accepts(String mode); + + /** + * Run the jar in the given mode. + * @param mode the mode to use + * @param args any program arguments + */ + void run(String mode, String[] args); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java new file mode 100644 index 000000000000..44fcb7902ee7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2023 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.loader.jarmode; + +import java.util.List; + +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.util.ClassUtils; + +/** + * Delegate class used to launch the fat jar in a specific mode. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public final class JarModeLauncher { + + static final String DISABLE_SYSTEM_EXIT = JarModeLauncher.class.getName() + ".DISABLE_SYSTEM_EXIT"; + + private JarModeLauncher() { + } + + public static void main(String[] args) { + String mode = System.getProperty("jarmode"); + List candidates = SpringFactoriesLoader.loadFactories(JarMode.class, + ClassUtils.getDefaultClassLoader()); + for (JarMode candidate : candidates) { + if (candidate.accepts(mode)) { + candidate.run(mode, args); + return; + } + } + System.err.println("Unsupported jarmode '" + mode + "'"); + if (!Boolean.getBoolean(DISABLE_SYSTEM_EXIT)) { + System.exit(1); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java new file mode 100644 index 000000000000..2e17175690a5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2023 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.loader.jarmode; + +import java.util.Arrays; + +/** + * {@link JarMode} for testing. + * + * @author Phillip Webb + */ +class TestJarMode implements JarMode { + + @Override + public boolean accepts(String mode) { + return "test".equals(mode); + } + + @Override + public void run(String mode, String[] args) { + System.out.println("running in " + mode + " jar mode " + Arrays.asList(args)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/package-info.java new file mode 100644 index 000000000000..2f3b5a74e8fd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Support for launching the JAR using jarmode. + * + * @see org.springframework.boot.loader.jarmode.JarModeLauncher + */ +package org.springframework.boot.loader.jarmode; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java new file mode 100644 index 000000000000..5beb8d109640 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2023 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.loader.launch; + +/** + * Repackaged {@link org.springframework.boot.loader.JarLauncher}. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public final class JarLauncher { + + private JarLauncher() { + } + + public static void main(String[] args) throws Exception { + org.springframework.boot.loader.JarLauncher.main(args); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java new file mode 100644 index 000000000000..d80fb0bb7105 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2023 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.loader.launch; + +/** + * Repackaged {@link org.springframework.boot.loader.PropertiesLauncher}. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public final class PropertiesLauncher { + + private PropertiesLauncher() { + } + + public static void main(String[] args) throws Exception { + org.springframework.boot.loader.PropertiesLauncher.main(args); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java new file mode 100644 index 000000000000..9392d3bf2b45 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2023 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.loader.launch; + +/** + * Repackaged {@link org.springframework.boot.loader.WarLauncher}. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public final class WarLauncher { + + private WarLauncher() { + } + + public static void main(String[] args) throws Exception { + org.springframework.boot.loader.WarLauncher.main(args); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/package-info.java new file mode 100644 index 000000000000..7968d509a2bb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Repackaged launcher classes. + * + * @see org.springframework.boot.loader.launch.JarLauncher + * @see org.springframework.boot.loader.launch.WarLauncher + */ +package org.springframework.boot.loader.launch; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/package-info.java new file mode 100644 index 000000000000..4b32f644f542 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/package-info.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * System that allows self-contained JAR/WAR archives to be launched using + * {@code java -jar}. Archives can include nested packaged dependency JARs (there is no + * need to create shade style jars) and are executed without unpacking. The only + * constraint is that nested JARs must be stored in the archive uncompressed. + * + * @see org.springframework.boot.loader.JarLauncher + * @see org.springframework.boot.loader.WarLauncher + */ +package org.springframework.boot.loader; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java new file mode 100644 index 000000000000..df00705e9eec --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java @@ -0,0 +1,232 @@ +/* + * Copyright 2012-2023 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.loader.util; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Properties; +import java.util.Set; + +/** + * Helper class for resolving placeholders in texts. Usually applied to file paths. + *

    + * A text may contain {@code $ ...} placeholders, to be resolved as system properties: + * e.g. {@code $ user.dir}. Default values can be supplied using the ":" separator between + * key and value. + *

    + * Adapted from Spring. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Dave Syer + * @since 1.0.0 + * @see System#getProperty(String) + */ +public abstract class SystemPropertyUtils { + + /** + * Prefix for system property placeholders: "${". + */ + public static final String PLACEHOLDER_PREFIX = "${"; + + /** + * Suffix for system property placeholders: "}". + */ + public static final String PLACEHOLDER_SUFFIX = "}"; + + /** + * Value separator for system property placeholders: ":". + */ + public static final String VALUE_SEPARATOR = ":"; + + private static final String SIMPLE_PREFIX = PLACEHOLDER_PREFIX.substring(1); + + /** + * Resolve ${...} placeholders in the given text, replacing them with corresponding + * system property values. + * @param text the String to resolve + * @return the resolved String + * @throws IllegalArgumentException if there is an unresolvable placeholder + * @see #PLACEHOLDER_PREFIX + * @see #PLACEHOLDER_SUFFIX + */ + public static String resolvePlaceholders(String text) { + if (text == null) { + return text; + } + return parseStringValue(null, text, text, new HashSet<>()); + } + + /** + * Resolve ${...} placeholders in the given text, replacing them with corresponding + * system property values. + * @param properties a properties instance to use in addition to System + * @param text the String to resolve + * @return the resolved String + * @throws IllegalArgumentException if there is an unresolvable placeholder + * @see #PLACEHOLDER_PREFIX + * @see #PLACEHOLDER_SUFFIX + */ + public static String resolvePlaceholders(Properties properties, String text) { + if (text == null) { + return text; + } + return parseStringValue(properties, text, text, new HashSet<>()); + } + + private static String parseStringValue(Properties properties, String value, String current, + Set visitedPlaceholders) { + + StringBuilder buf = new StringBuilder(current); + + int startIndex = current.indexOf(PLACEHOLDER_PREFIX); + while (startIndex != -1) { + int endIndex = findPlaceholderEndIndex(buf, startIndex); + if (endIndex != -1) { + String placeholder = buf.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex); + String originalPlaceholder = placeholder; + if (!visitedPlaceholders.add(originalPlaceholder)) { + throw new IllegalArgumentException( + "Circular placeholder reference '" + originalPlaceholder + "' in property definitions"); + } + // Recursive invocation, parsing placeholders contained in the + // placeholder + // key. + placeholder = parseStringValue(properties, value, placeholder, visitedPlaceholders); + // Now obtain the value for the fully resolved key... + String propVal = resolvePlaceholder(properties, value, placeholder); + if (propVal == null) { + int separatorIndex = placeholder.indexOf(VALUE_SEPARATOR); + if (separatorIndex != -1) { + String actualPlaceholder = placeholder.substring(0, separatorIndex); + String defaultValue = placeholder.substring(separatorIndex + VALUE_SEPARATOR.length()); + propVal = resolvePlaceholder(properties, value, actualPlaceholder); + if (propVal == null) { + propVal = defaultValue; + } + } + } + if (propVal != null) { + // Recursive invocation, parsing placeholders contained in the + // previously resolved placeholder value. + propVal = parseStringValue(properties, value, propVal, visitedPlaceholders); + buf.replace(startIndex, endIndex + PLACEHOLDER_SUFFIX.length(), propVal); + startIndex = buf.indexOf(PLACEHOLDER_PREFIX, startIndex + propVal.length()); + } + else { + // Proceed with unprocessed value. + startIndex = buf.indexOf(PLACEHOLDER_PREFIX, endIndex + PLACEHOLDER_SUFFIX.length()); + } + visitedPlaceholders.remove(originalPlaceholder); + } + else { + startIndex = -1; + } + } + + return buf.toString(); + } + + private static String resolvePlaceholder(Properties properties, String text, String placeholderName) { + String propVal = getProperty(placeholderName, null, text); + if (propVal != null) { + return propVal; + } + return (properties != null) ? properties.getProperty(placeholderName) : null; + } + + public static String getProperty(String key) { + return getProperty(key, null, ""); + } + + public static String getProperty(String key, String defaultValue) { + return getProperty(key, defaultValue, ""); + } + + /** + * Search the System properties and environment variables for a value with the + * provided key. Environment variables in {@code UPPER_CASE} style are allowed where + * System properties would normally be {@code lower.case}. + * @param key the key to resolve + * @param defaultValue the default value + * @param text optional extra context for an error message if the key resolution fails + * (e.g. if System properties are not accessible) + * @return a static property value or null of not found + */ + public static String getProperty(String key, String defaultValue, String text) { + try { + String propVal = System.getProperty(key); + if (propVal == null) { + // Fall back to searching the system environment. + propVal = System.getenv(key); + } + if (propVal == null) { + // Try with underscores. + String name = key.replace('.', '_'); + propVal = System.getenv(name); + } + if (propVal == null) { + // Try uppercase with underscores as well. + String name = key.toUpperCase(Locale.ENGLISH).replace('.', '_'); + propVal = System.getenv(name); + } + if (propVal != null) { + return propVal; + } + } + catch (Throwable ex) { + System.err.println("Could not resolve key '" + key + "' in '" + text + + "' as system property or in environment: " + ex); + } + return defaultValue; + } + + private static int findPlaceholderEndIndex(CharSequence buf, int startIndex) { + int index = startIndex + PLACEHOLDER_PREFIX.length(); + int withinNestedPlaceholder = 0; + while (index < buf.length()) { + if (substringMatch(buf, index, PLACEHOLDER_SUFFIX)) { + if (withinNestedPlaceholder > 0) { + withinNestedPlaceholder--; + index = index + PLACEHOLDER_SUFFIX.length(); + } + else { + return index; + } + } + else if (substringMatch(buf, index, SIMPLE_PREFIX)) { + withinNestedPlaceholder++; + index = index + SIMPLE_PREFIX.length(); + } + else { + index++; + } + } + return -1; + } + + private static boolean substringMatch(CharSequence str, int index, CharSequence substring) { + for (int j = 0; j < substring.length(); j++) { + int i = index + j; + if (i >= str.length() || str.charAt(i) != substring.charAt(j)) { + return false; + } + } + return true; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/package-info.java new file mode 100644 index 000000000000..d3d7eef2d9db --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Utilities used by Spring Boot's JAR loading. + */ +package org.springframework.boot.loader.util; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java new file mode 100644 index 000000000000..60e3cb2765eb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java @@ -0,0 +1,149 @@ +/* + * Copyright 2012-2023 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.loader; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; + +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.archive.Archive; +import org.springframework.util.FileCopyUtils; + +/** + * Base class for testing {@link ExecutableArchiveLauncher} implementations. + * + * @author Andy Wilkinson + * @author Madhura Bhave + * @author Scott Frederick + */ +public abstract class AbstractExecutableArchiveLauncherTests { + + @TempDir + File tempDir; + + protected File createJarArchive(String name, String entryPrefix) throws IOException { + return createJarArchive(name, entryPrefix, false, Collections.emptyList()); + } + + @SuppressWarnings("resource") + protected File createJarArchive(String name, String entryPrefix, boolean indexed, List extraLibs) + throws IOException { + return createJarArchive(name, null, entryPrefix, indexed, extraLibs); + } + + @SuppressWarnings("resource") + protected File createJarArchive(String name, Manifest manifest, String entryPrefix, boolean indexed, + List extraLibs) throws IOException { + File archive = new File(this.tempDir, name); + JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(archive)); + if (manifest != null) { + jarOutputStream.putNextEntry(new JarEntry("META-INF/")); + jarOutputStream.putNextEntry(new JarEntry("META-INF/MANIFEST.MF")); + manifest.write(jarOutputStream); + jarOutputStream.closeEntry(); + } + jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/")); + jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/classes/")); + jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/lib/")); + if (indexed) { + jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/classpath.idx")); + Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8); + writer.write("- \"" + entryPrefix + "/lib/foo.jar\"\n"); + writer.write("- \"" + entryPrefix + "/lib/bar.jar\"\n"); + writer.write("- \"" + entryPrefix + "/lib/baz.jar\"\n"); + writer.flush(); + jarOutputStream.closeEntry(); + } + addNestedJars(entryPrefix, "/lib/foo.jar", jarOutputStream); + addNestedJars(entryPrefix, "/lib/bar.jar", jarOutputStream); + addNestedJars(entryPrefix, "/lib/baz.jar", jarOutputStream); + for (String lib : extraLibs) { + addNestedJars(entryPrefix, "/lib/" + lib, jarOutputStream); + } + jarOutputStream.close(); + return archive; + } + + private void addNestedJars(String entryPrefix, String lib, JarOutputStream jarOutputStream) throws IOException { + JarEntry libFoo = new JarEntry(entryPrefix + lib); + libFoo.setMethod(ZipEntry.STORED); + ByteArrayOutputStream fooJarStream = new ByteArrayOutputStream(); + new JarOutputStream(fooJarStream).close(); + libFoo.setSize(fooJarStream.size()); + CRC32 crc32 = new CRC32(); + crc32.update(fooJarStream.toByteArray()); + libFoo.setCrc(crc32.getValue()); + jarOutputStream.putNextEntry(libFoo); + jarOutputStream.write(fooJarStream.toByteArray()); + } + + protected File explode(File archive) throws IOException { + File exploded = new File(this.tempDir, "exploded"); + exploded.mkdirs(); + JarFile jarFile = new JarFile(archive); + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + File entryFile = new File(exploded, entry.getName()); + if (entry.isDirectory()) { + entryFile.mkdirs(); + } + else { + FileCopyUtils.copy(jarFile.getInputStream(entry), new FileOutputStream(entryFile)); + } + } + jarFile.close(); + return exploded; + } + + protected Set getUrls(List archives) throws MalformedURLException { + Set urls = new LinkedHashSet<>(archives.size()); + for (Archive archive : archives) { + urls.add(archive.getUrl()); + } + return urls; + } + + protected final URL toUrl(File file) { + try { + return file.toURI().toURL(); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java new file mode 100644 index 000000000000..4cd1b4e8d280 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2012-2023 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.loader; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link ClassPathIndexFile}. + * + * @author Madhura Bhave + * @author Phillip Webb + */ +class ClassPathIndexFileTests { + + @TempDir + File temp; + + @Test + void loadIfPossibleWhenRootIsNotFileReturnsNull() { + assertThatIllegalArgumentException() + .isThrownBy(() -> ClassPathIndexFile.loadIfPossible(new URL("https://example.com/file"), "test.idx")) + .withMessage("URL does not reference a file"); + } + + @Test + void loadIfPossibleWhenRootDoesNotExistReturnsNull() throws Exception { + File root = new File(this.temp, "missing"); + assertThat(ClassPathIndexFile.loadIfPossible(root.toURI().toURL(), "test.idx")).isNull(); + } + + @Test + void loadIfPossibleWhenRootIsDirectoryThrowsException() throws Exception { + File root = new File(this.temp, "directory"); + root.mkdirs(); + assertThat(ClassPathIndexFile.loadIfPossible(root.toURI().toURL(), "test.idx")).isNull(); + } + + @Test + void loadIfPossibleReturnsInstance() throws Exception { + ClassPathIndexFile indexFile = copyAndLoadTestIndexFile(); + assertThat(indexFile).isNotNull(); + } + + @Test + void sizeReturnsNumberOfLines() throws Exception { + ClassPathIndexFile indexFile = copyAndLoadTestIndexFile(); + assertThat(indexFile.size()).isEqualTo(5); + } + + @Test + void getUrlsReturnsUrls() throws Exception { + ClassPathIndexFile indexFile = copyAndLoadTestIndexFile(); + List urls = indexFile.getUrls(); + List expected = new ArrayList<>(); + expected.add(new File(this.temp, "BOOT-INF/layers/one/lib/a.jar")); + expected.add(new File(this.temp, "BOOT-INF/layers/one/lib/b.jar")); + expected.add(new File(this.temp, "BOOT-INF/layers/one/lib/c.jar")); + expected.add(new File(this.temp, "BOOT-INF/layers/two/lib/d.jar")); + expected.add(new File(this.temp, "BOOT-INF/layers/two/lib/e.jar")); + assertThat(urls).containsExactly(expected.stream().map(this::toUrl).toArray(URL[]::new)); + } + + private URL toUrl(File file) { + try { + return file.toURI().toURL(); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + + private ClassPathIndexFile copyAndLoadTestIndexFile() throws IOException { + copyTestIndexFile(); + ClassPathIndexFile indexFile = ClassPathIndexFile.loadIfPossible(this.temp.toURI().toURL(), "test.idx"); + return indexFile; + } + + private void copyTestIndexFile() throws IOException { + Files.copy(getClass().getResourceAsStream("classpath-index-file.idx"), + new File(this.temp, "test.idx").toPath()); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java new file mode 100644 index 000000000000..afa32a7c4f18 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java @@ -0,0 +1,154 @@ +/* + * Copyright 2012-2023 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.loader; + +import java.io.File; +import java.io.FileOutputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.Attributes.Name; +import java.util.jar.Manifest; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.archive.ExplodedArchive; +import org.springframework.boot.loader.archive.JarFileArchive; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.test.tools.SourceFile; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.function.ThrowingConsumer; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JarLauncher}. + * + * @author Andy Wilkinson + * @author Madhura Bhave + */ +class JarLauncherTests extends AbstractExecutableArchiveLauncherTests { + + @Test + void explodedJarHasOnlyBootInfClassesAndContentsOfBootInfLibOnClasspath() throws Exception { + File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF")); + JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true)); + List archives = new ArrayList<>(); + launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); + assertThat(getUrls(archives)).containsExactlyInAnyOrder(getExpectedFileUrls(explodedRoot)); + for (Archive archive : archives) { + archive.close(); + } + } + + @Test + void archivedJarHasOnlyBootInfClassesAndContentsOfBootInfLibOnClasspath() throws Exception { + File jarRoot = createJarArchive("archive.jar", "BOOT-INF"); + try (JarFileArchive archive = new JarFileArchive(jarRoot)) { + JarLauncher launcher = new JarLauncher(archive); + List classPathArchives = new ArrayList<>(); + launcher.getClassPathArchivesIterator().forEachRemaining(classPathArchives::add); + assertThat(classPathArchives).hasSize(4); + assertThat(getUrls(classPathArchives)).containsOnly( + new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/classes!/"), + new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/foo.jar!/"), + new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/bar.jar!/"), + new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/baz.jar!/")); + for (Archive classPathArchive : classPathArchives) { + classPathArchive.close(); + } + } + } + + @Test + void explodedJarShouldPreserveClasspathOrderWhenIndexPresent() throws Exception { + File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF", true, Collections.emptyList())); + JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true)); + Iterator archives = launcher.getClassPathArchivesIterator(); + URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives); + URL[] urls = classLoader.getURLs(); + assertThat(urls).containsExactly(getExpectedFileUrls(explodedRoot)); + } + + @Test + void jarFilesPresentInBootInfLibsAndNotInClasspathIndexShouldBeAddedAfterBootInfClasses() throws Exception { + ArrayList extraLibs = new ArrayList<>(Arrays.asList("extra-1.jar", "extra-2.jar")); + File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF", true, extraLibs)); + JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true)); + Iterator archives = launcher.getClassPathArchivesIterator(); + URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives); + URL[] urls = classLoader.getURLs(); + List expectedFiles = getExpectedFilesWithExtraLibs(explodedRoot); + URL[] expectedFileUrls = expectedFiles.stream().map(this::toUrl).toArray(URL[]::new); + assertThat(urls).containsExactly(expectedFileUrls); + } + + @Test + void explodedJarDefinedPackagesIncludeManifestAttributes() { + Manifest manifest = new Manifest(); + Attributes attributes = manifest.getMainAttributes(); + attributes.put(Name.MANIFEST_VERSION, "1.0"); + attributes.put(Name.IMPLEMENTATION_TITLE, "test"); + SourceFile sourceFile = SourceFile.of("explodedsample/ExampleClass.java", + new ClassPathResource("explodedsample/ExampleClass.txt")); + TestCompiler.forSystem().compile(sourceFile, ThrowingConsumer.of((compiled) -> { + File explodedRoot = explode( + createJarArchive("archive.jar", manifest, "BOOT-INF", true, Collections.emptyList())); + File target = new File(explodedRoot, "BOOT-INF/classes/explodedsample/ExampleClass.class"); + target.getParentFile().mkdirs(); + FileCopyUtils.copy(compiled.getClassLoader().getResourceAsStream("explodedsample/ExampleClass.class"), + new FileOutputStream(target)); + JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true)); + Iterator archives = launcher.getClassPathArchivesIterator(); + URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives); + Class loaded = classLoader.loadClass("explodedsample.ExampleClass"); + assertThat(loaded.getPackage().getImplementationTitle()).isEqualTo("test"); + })); + } + + protected final URL[] getExpectedFileUrls(File explodedRoot) { + return getExpectedFiles(explodedRoot).stream().map(this::toUrl).toArray(URL[]::new); + } + + protected final List getExpectedFiles(File parent) { + List expected = new ArrayList<>(); + expected.add(new File(parent, "BOOT-INF/classes")); + expected.add(new File(parent, "BOOT-INF/lib/foo.jar")); + expected.add(new File(parent, "BOOT-INF/lib/bar.jar")); + expected.add(new File(parent, "BOOT-INF/lib/baz.jar")); + return expected; + } + + protected final List getExpectedFilesWithExtraLibs(File parent) { + List expected = new ArrayList<>(); + expected.add(new File(parent, "BOOT-INF/classes")); + expected.add(new File(parent, "BOOT-INF/lib/extra-1.jar")); + expected.add(new File(parent, "BOOT-INF/lib/extra-2.jar")); + expected.add(new File(parent, "BOOT-INF/lib/foo.jar")); + expected.add(new File(parent, "BOOT-INF/lib/bar.jar")); + expected.add(new File(parent, "BOOT-INF/lib/baz.jar")); + return expected; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java new file mode 100644 index 000000000000..58084bba8ab6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2012-2023 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.loader; + +import java.io.File; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLConnection; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.jar.JarFile; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LaunchedURLClassLoader}. + * + * @author Dave Syer + * @author Phillip Webb + * @author Andy Wilkinson + */ +@SuppressWarnings("resource") +class LaunchedURLClassLoaderTests { + + @TempDir + File tempDir; + + @Test + void resolveResourceFromArchive() throws Exception { + LaunchedURLClassLoader loader = new LaunchedURLClassLoader( + new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") }, getClass().getClassLoader()); + assertThat(loader.getResource("demo/Application.java")).isNotNull(); + } + + @Test + void resolveResourcesFromArchive() throws Exception { + LaunchedURLClassLoader loader = new LaunchedURLClassLoader( + new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") }, getClass().getClassLoader()); + assertThat(loader.getResources("demo/Application.java").hasMoreElements()).isTrue(); + } + + @Test + void resolveRootPathFromArchive() throws Exception { + LaunchedURLClassLoader loader = new LaunchedURLClassLoader( + new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") }, getClass().getClassLoader()); + assertThat(loader.getResource("")).isNotNull(); + } + + @Test + void resolveRootResourcesFromArchive() throws Exception { + LaunchedURLClassLoader loader = new LaunchedURLClassLoader( + new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") }, getClass().getClassLoader()); + assertThat(loader.getResources("").hasMoreElements()).isTrue(); + } + + @Test + void resolveFromNested() throws Exception { + File file = new File(this.tempDir, "test.jar"); + TestJarCreator.createTestJar(file); + try (JarFile jarFile = new JarFile(file)) { + URL url = jarFile.getUrl(); + try (LaunchedURLClassLoader loader = new LaunchedURLClassLoader(new URL[] { url }, null)) { + URL resource = loader.getResource("nested.jar!/3.dat"); + assertThat(resource).hasToString(url + "nested.jar!/3.dat"); + try (InputStream input = resource.openConnection().getInputStream()) { + assertThat(input.read()).isEqualTo(3); + } + } + } + } + + @Test + void resolveFromNestedWhileThreadIsInterrupted() throws Exception { + File file = new File(this.tempDir, "test.jar"); + TestJarCreator.createTestJar(file); + try (JarFile jarFile = new JarFile(file)) { + URL url = jarFile.getUrl(); + try (LaunchedURLClassLoader loader = new LaunchedURLClassLoader(new URL[] { url }, null)) { + Thread.currentThread().interrupt(); + URL resource = loader.getResource("nested.jar!/3.dat"); + assertThat(resource).hasToString(url + "nested.jar!/3.dat"); + URLConnection connection = resource.openConnection(); + try (InputStream input = connection.getInputStream()) { + assertThat(input.read()).isEqualTo(3); + } + ((JarURLConnection) connection).getJarFile().close(); + } + finally { + Thread.interrupted(); + } + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java new file mode 100644 index 000000000000..ab7c296b38b9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java @@ -0,0 +1,433 @@ +/* + * Copyright 2012-2023 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.loader; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.ref.SoftReference; +import java.net.URL; +import java.net.URLClassLoader; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +import org.assertj.core.api.Condition; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.archive.ExplodedArchive; +import org.springframework.boot.loader.archive.JarFileArchive; +import org.springframework.boot.loader.jar.Handler; +import org.springframework.boot.loader.jar.JarFile; +import org.springframework.boot.testsupport.system.CapturedOutput; +import org.springframework.boot.testsupport.system.OutputCaptureExtension; +import org.springframework.core.io.FileSystemResource; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.hamcrest.Matchers.containsString; + +/** + * Tests for {@link PropertiesLauncher}. + * + * @author Dave Syer + * @author Andy Wilkinson + */ +@ExtendWith(OutputCaptureExtension.class) +class PropertiesLauncherTests { + + @TempDir + File tempDir; + + private PropertiesLauncher launcher; + + private ClassLoader contextClassLoader; + + private CapturedOutput output; + + @BeforeEach + void setup(CapturedOutput capturedOutput) throws Exception { + this.contextClassLoader = Thread.currentThread().getContextClassLoader(); + clearHandlerCache(); + System.setProperty("loader.home", new File("src/test/resources").getAbsolutePath()); + this.output = capturedOutput; + } + + @AfterEach + void close() throws Exception { + Thread.currentThread().setContextClassLoader(this.contextClassLoader); + System.clearProperty("loader.home"); + System.clearProperty("loader.path"); + System.clearProperty("loader.main"); + System.clearProperty("loader.config.name"); + System.clearProperty("loader.config.location"); + System.clearProperty("loader.system"); + System.clearProperty("loader.classLoader"); + clearHandlerCache(); + if (this.launcher != null) { + this.launcher.close(); + } + } + + @SuppressWarnings("unchecked") + private void clearHandlerCache() throws Exception { + Map rootFileCache = ((SoftReference>) ReflectionTestUtils + .getField(Handler.class, "rootFileCache")).get(); + if (rootFileCache != null) { + for (JarFile rootJarFile : rootFileCache.values()) { + rootJarFile.close(); + } + rootFileCache.clear(); + } + } + + @Test + void testDefaultHome() { + System.clearProperty("loader.home"); + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getHomeDirectory()).isEqualTo(new File(System.getProperty("user.dir"))); + } + + @Test + void testAlternateHome() throws Exception { + System.setProperty("loader.home", "src/test/resources/home"); + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getHomeDirectory()).isEqualTo(new File(System.getProperty("loader.home"))); + assertThat(this.launcher.getMainClass()).isEqualTo("demo.HomeApplication"); + } + + @Test + void testNonExistentHome() { + System.setProperty("loader.home", "src/test/resources/nonexistent"); + assertThatIllegalStateException().isThrownBy(PropertiesLauncher::new) + .withMessageContaining("Invalid source directory") + .withCauseInstanceOf(IllegalArgumentException.class); + } + + @Test + void testUserSpecifiedMain() throws Exception { + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getMainClass()).isEqualTo("demo.Application"); + assertThat(System.getProperty("loader.main")).isNull(); + } + + @Test + void testUserSpecifiedConfigName() throws Exception { + System.setProperty("loader.config.name", "foo"); + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getMainClass()).isEqualTo("my.Application"); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[etc/]"); + } + + @Test + void testRootOfClasspathFirst() throws Exception { + System.setProperty("loader.config.name", "bar"); + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getMainClass()).isEqualTo("my.BarApplication"); + } + + @Test + void testUserSpecifiedDotPath() { + System.setProperty("loader.path", "."); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[.]"); + } + + @Test + void testUserSpecifiedSlashPath() throws Exception { + System.setProperty("loader.path", "jars/"); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[jars/]"); + List archives = new ArrayList<>(); + this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); + assertThat(archives).areExactly(1, endingWith("app.jar")); + } + + @Test + void testUserSpecifiedWildcardPath() throws Exception { + System.setProperty("loader.path", "jars/*"); + System.setProperty("loader.main", "demo.Application"); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[jars/]"); + this.launcher.launch(new String[0]); + waitFor("Hello World"); + } + + @Test + void testUserSpecifiedJarPath() throws Exception { + System.setProperty("loader.path", "jars/app.jar"); + System.setProperty("loader.main", "demo.Application"); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[jars/app.jar]"); + this.launcher.launch(new String[0]); + waitFor("Hello World"); + } + + @Test + void testUserSpecifiedRootOfJarPath() throws Exception { + System.setProperty("loader.path", "jar:file:./src/test/resources/nested-jars/app.jar!/"); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths")) + .hasToString("[jar:file:./src/test/resources/nested-jars/app.jar!/]"); + List archives = new ArrayList<>(); + this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); + assertThat(archives).areExactly(1, endingWith("foo.jar!/")); + assertThat(archives).areExactly(1, endingWith("app.jar")); + } + + @Test + void testUserSpecifiedRootOfJarPathWithDot() throws Exception { + System.setProperty("loader.path", "nested-jars/app.jar!/./"); + this.launcher = new PropertiesLauncher(); + List archives = new ArrayList<>(); + this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); + assertThat(archives).areExactly(1, endingWith("foo.jar!/")); + assertThat(archives).areExactly(1, endingWith("app.jar")); + } + + @Test + void testUserSpecifiedRootOfJarPathWithDotAndJarPrefix() throws Exception { + System.setProperty("loader.path", "jar:file:./src/test/resources/nested-jars/app.jar!/./"); + this.launcher = new PropertiesLauncher(); + List archives = new ArrayList<>(); + this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); + assertThat(archives).areExactly(1, endingWith("foo.jar!/")); + } + + @Test + void testUserSpecifiedJarFileWithNestedArchives() throws Exception { + System.setProperty("loader.path", "nested-jars/app.jar"); + System.setProperty("loader.main", "demo.Application"); + this.launcher = new PropertiesLauncher(); + List archives = new ArrayList<>(); + this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); + assertThat(archives).areExactly(1, endingWith("foo.jar!/")); + assertThat(archives).areExactly(1, endingWith("app.jar")); + } + + @Test + void testUserSpecifiedNestedJarPath() throws Exception { + System.setProperty("loader.path", "nested-jars/nested-jar-app.jar!/BOOT-INF/classes/"); + System.setProperty("loader.main", "demo.Application"); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths")) + .hasToString("[nested-jars/nested-jar-app.jar!/BOOT-INF/classes/]"); + this.launcher.launch(new String[0]); + waitFor("Hello World"); + } + + @Test + void testUserSpecifiedDirectoryContainingJarFileWithNestedArchives() throws Exception { + System.setProperty("loader.path", "nested-jars"); + System.setProperty("loader.main", "demo.Application"); + this.launcher = new PropertiesLauncher(); + this.launcher.launch(new String[0]); + waitFor("Hello World"); + } + + @Test + void testUserSpecifiedJarPathWithDot() throws Exception { + System.setProperty("loader.path", "./jars/app.jar"); + System.setProperty("loader.main", "demo.Application"); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[jars/app.jar]"); + this.launcher.launch(new String[0]); + waitFor("Hello World"); + } + + @Test + void testUserSpecifiedClassLoader() throws Exception { + System.setProperty("loader.path", "jars/app.jar"); + System.setProperty("loader.classLoader", URLClassLoader.class.getName()); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[jars/app.jar]"); + this.launcher.launch(new String[0]); + waitFor("Hello World"); + } + + @Test + void testUserSpecifiedClassPathOrder() throws Exception { + System.setProperty("loader.path", "more-jars/app.jar,jars/app.jar"); + System.setProperty("loader.classLoader", URLClassLoader.class.getName()); + this.launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(this.launcher, "paths")) + .hasToString("[more-jars/app.jar, jars/app.jar]"); + this.launcher.launch(new String[0]); + waitFor("Hello Other World"); + } + + @Test + void testCustomClassLoaderCreation() throws Exception { + System.setProperty("loader.classLoader", TestLoader.class.getName()); + this.launcher = new PropertiesLauncher(); + ClassLoader loader = this.launcher.createClassLoader(archives()); + assertThat(loader).isNotNull(); + assertThat(loader.getClass().getName()).isEqualTo(TestLoader.class.getName()); + } + + private Iterator archives() throws Exception { + List archives = new ArrayList<>(); + String path = System.getProperty("java.class.path"); + for (String url : path.split(File.pathSeparator)) { + Archive archive = archive(url); + if (archive != null) { + archives.add(archive); + } + } + return archives.iterator(); + } + + private Archive archive(String url) throws IOException { + File file = new FileSystemResource(url).getFile(); + if (!file.exists()) { + return null; + } + if (url.endsWith(".jar")) { + return new JarFileArchive(file); + } + return new ExplodedArchive(file); + } + + @Test + void testUserSpecifiedConfigPathWins() throws Exception { + System.setProperty("loader.config.name", "foo"); + System.setProperty("loader.config.location", "classpath:bar.properties"); + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getMainClass()).isEqualTo("my.BarApplication"); + } + + @Test + void testSystemPropertySpecifiedMain() throws Exception { + System.setProperty("loader.main", "foo.Bar"); + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getMainClass()).isEqualTo("foo.Bar"); + } + + @Test + void testSystemPropertiesSet() { + System.setProperty("loader.system", "true"); + new PropertiesLauncher(); + assertThat(System.getProperty("loader.main")).isEqualTo("demo.Application"); + } + + @Test + void testArgsEnhanced() throws Exception { + System.setProperty("loader.args", "foo"); + this.launcher = new PropertiesLauncher(); + assertThat(Arrays.asList(this.launcher.getArgs("bar"))).hasToString("[foo, bar]"); + } + + @SuppressWarnings("unchecked") + @Test + void testLoadPathCustomizedUsingManifest() throws Exception { + System.setProperty("loader.home", this.tempDir.getAbsolutePath()); + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().putValue("Loader-Path", "/foo.jar, /bar"); + File manifestFile = new File(this.tempDir, "META-INF/MANIFEST.MF"); + manifestFile.getParentFile().mkdirs(); + try (FileOutputStream manifestStream = new FileOutputStream(manifestFile)) { + manifest.write(manifestStream); + } + this.launcher = new PropertiesLauncher(); + assertThat((List) ReflectionTestUtils.getField(this.launcher, "paths")).containsExactly("/foo.jar", + "/bar/"); + } + + @Test + void testManifestWithPlaceholders() throws Exception { + System.setProperty("loader.home", "src/test/resources/placeholders"); + this.launcher = new PropertiesLauncher(); + assertThat(this.launcher.getMainClass()).isEqualTo("demo.FooApplication"); + } + + @Test + void encodedFileUrlLoaderPathIsHandledCorrectly() throws Exception { + File loaderPath = new File(this.tempDir, "loader path"); + loaderPath.mkdir(); + System.setProperty("loader.path", loaderPath.toURI().toURL().toString()); + this.launcher = new PropertiesLauncher(); + List archives = new ArrayList<>(); + this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); + assertThat(archives).hasSize(1); + File archiveRoot = (File) ReflectionTestUtils.getField(archives.get(0), "root"); + assertThat(archiveRoot).isEqualTo(loaderPath); + } + + @Test // gh-21575 + void loadResourceFromJarFile() throws Exception { + File jarFile = new File(this.tempDir, "app.jar"); + TestJarCreator.createTestJar(jarFile); + System.setProperty("loader.home", this.tempDir.getAbsolutePath()); + System.setProperty("loader.path", "app.jar"); + this.launcher = new PropertiesLauncher(); + try { + this.launcher.launch(new String[0]); + } + catch (Exception ex) { + // Expected ClassNotFoundException + LaunchedURLClassLoader classLoader = (LaunchedURLClassLoader) Thread.currentThread() + .getContextClassLoader(); + classLoader.close(); + } + URL resource = new URL("jar:" + jarFile.toURI() + "!/nested.jar!/3.dat"); + byte[] bytes = FileCopyUtils.copyToByteArray(resource.openStream()); + assertThat(bytes).isNotEmpty(); + } + + private void waitFor(String value) { + Awaitility.waitAtMost(Duration.ofSeconds(5)).until(this.output::toString, containsString(value)); + } + + private Condition endingWith(String value) { + return new Condition<>() { + + @Override + public boolean matches(Archive archive) { + return archive.toString().endsWith(value); + } + + }; + } + + static class TestLoader extends URLClassLoader { + + TestLoader(ClassLoader parent) { + super(new URL[0], parent); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + return super.findClass(name); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java new file mode 100644 index 000000000000..c5c5fd3b95c9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java @@ -0,0 +1,151 @@ +/* + * Copyright 2012-2023 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.loader; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; + +/** + * Creates a simple test jar. + * + * @author Phillip Webb + */ +public abstract class TestJarCreator { + + private static final int BASE_VERSION = 8; + + private static final int RUNTIME_VERSION; + + static { + int version; + try { + Object runtimeVersion = Runtime.class.getMethod("version").invoke(null); + version = (int) runtimeVersion.getClass().getMethod("major").invoke(runtimeVersion); + } + catch (Throwable ex) { + version = BASE_VERSION; + } + RUNTIME_VERSION = version; + } + + public static void createTestJar(File file) throws Exception { + createTestJar(file, false); + } + + public static void createTestJar(File file, boolean unpackNested) throws Exception { + FileOutputStream fileOutputStream = new FileOutputStream(file); + try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) { + jarOutputStream.setComment("outer"); + writeManifest(jarOutputStream, "j1"); + writeEntry(jarOutputStream, "1.dat", 1); + writeEntry(jarOutputStream, "2.dat", 2); + writeDirEntry(jarOutputStream, "d/"); + writeEntry(jarOutputStream, "d/9.dat", 9); + writeDirEntry(jarOutputStream, "special/"); + writeEntry(jarOutputStream, "special/\u00EB.dat", '\u00EB'); + writeNestedEntry("nested.jar", unpackNested, jarOutputStream); + writeNestedEntry("another-nested.jar", unpackNested, jarOutputStream); + writeNestedEntry("space nested.jar", unpackNested, jarOutputStream); + writeNestedMultiReleaseEntry("multi-release.jar", unpackNested, jarOutputStream); + } + } + + private static void writeNestedEntry(String name, boolean unpackNested, JarOutputStream jarOutputStream) + throws Exception { + writeNestedEntry(name, unpackNested, jarOutputStream, false); + } + + private static void writeNestedMultiReleaseEntry(String name, boolean unpackNested, JarOutputStream jarOutputStream) + throws Exception { + writeNestedEntry(name, unpackNested, jarOutputStream, true); + } + + private static void writeNestedEntry(String name, boolean unpackNested, JarOutputStream jarOutputStream, + boolean multiRelease) throws Exception { + JarEntry nestedEntry = new JarEntry(name); + byte[] nestedJarData = getNestedJarData(multiRelease); + nestedEntry.setSize(nestedJarData.length); + nestedEntry.setCompressedSize(nestedJarData.length); + if (unpackNested) { + nestedEntry.setComment("UNPACK:0000000000000000000000000000000000000000"); + } + CRC32 crc32 = new CRC32(); + crc32.update(nestedJarData); + nestedEntry.setCrc(crc32.getValue()); + nestedEntry.setMethod(ZipEntry.STORED); + jarOutputStream.putNextEntry(nestedEntry); + jarOutputStream.write(nestedJarData); + jarOutputStream.closeEntry(); + } + + private static byte[] getNestedJarData(boolean multiRelease) throws Exception { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + JarOutputStream jarOutputStream = new JarOutputStream(byteArrayOutputStream); + jarOutputStream.setComment("nested"); + writeManifest(jarOutputStream, "j2", multiRelease); + if (multiRelease) { + writeEntry(jarOutputStream, "multi-release.dat", BASE_VERSION); + writeEntry(jarOutputStream, String.format("META-INF/versions/%d/multi-release.dat", RUNTIME_VERSION), + RUNTIME_VERSION); + } + else { + writeEntry(jarOutputStream, "3.dat", 3); + writeEntry(jarOutputStream, "4.dat", 4); + writeEntry(jarOutputStream, "\u00E4.dat", '\u00E4'); + } + jarOutputStream.close(); + return byteArrayOutputStream.toByteArray(); + } + + private static void writeManifest(JarOutputStream jarOutputStream, String name) throws Exception { + writeManifest(jarOutputStream, name, false); + } + + private static void writeManifest(JarOutputStream jarOutputStream, String name, boolean multiRelease) + throws Exception { + writeDirEntry(jarOutputStream, "META-INF/"); + Manifest manifest = new Manifest(); + manifest.getMainAttributes().putValue("Built-By", name); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + if (multiRelease) { + manifest.getMainAttributes().putValue("Multi-Release", Boolean.toString(true)); + } + jarOutputStream.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF")); + manifest.write(jarOutputStream); + jarOutputStream.closeEntry(); + } + + private static void writeDirEntry(JarOutputStream jarOutputStream, String name) throws IOException { + jarOutputStream.putNextEntry(new JarEntry(name)); + jarOutputStream.closeEntry(); + } + + private static void writeEntry(JarOutputStream jarOutputStream, String name, int data) throws IOException { + jarOutputStream.putNextEntry(new JarEntry(name)); + jarOutputStream.write(new byte[] { (byte) data }); + jarOutputStream.closeEntry(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java new file mode 100644 index 000000000000..fbab8d36ed0a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java @@ -0,0 +1,121 @@ +/* + * Copyright 2012-2023 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.loader; + +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.archive.ExplodedArchive; +import org.springframework.boot.loader.archive.JarFileArchive; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link WarLauncher}. + * + * @author Andy Wilkinson + * @author Scott Frederick + */ +class WarLauncherTests extends AbstractExecutableArchiveLauncherTests { + + @Test + void explodedWarHasOnlyWebInfClassesAndContentsOfWebInfLibOnClasspath() throws Exception { + File explodedRoot = explode(createJarArchive("archive.war", "WEB-INF")); + WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true)); + List archives = new ArrayList<>(); + launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); + assertThat(getUrls(archives)).containsExactlyInAnyOrder(getExpectedFileUrls(explodedRoot)); + for (Archive archive : archives) { + archive.close(); + } + } + + @Test + void archivedWarHasOnlyWebInfClassesAndContentsOfWebInfLibOnClasspath() throws Exception { + File jarRoot = createJarArchive("archive.war", "WEB-INF"); + try (JarFileArchive archive = new JarFileArchive(jarRoot)) { + WarLauncher launcher = new WarLauncher(archive); + List classPathArchives = new ArrayList<>(); + launcher.getClassPathArchivesIterator().forEachRemaining(classPathArchives::add); + assertThat(getUrls(classPathArchives)).containsOnly( + new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/classes!/"), + new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/foo.jar!/"), + new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/bar.jar!/"), + new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/baz.jar!/")); + for (Archive classPathArchive : classPathArchives) { + classPathArchive.close(); + } + } + } + + @Test + void explodedWarShouldPreserveClasspathOrderWhenIndexPresent() throws Exception { + File explodedRoot = explode(createJarArchive("archive.war", "WEB-INF", true, Collections.emptyList())); + WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true)); + Iterator archives = launcher.getClassPathArchivesIterator(); + URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives); + URL[] urls = classLoader.getURLs(); + assertThat(urls).containsExactly(getExpectedFileUrls(explodedRoot)); + } + + @Test + void warFilesPresentInWebInfLibsAndNotInClasspathIndexShouldBeAddedAfterWebInfClasses() throws Exception { + ArrayList extraLibs = new ArrayList<>(Arrays.asList("extra-1.jar", "extra-2.jar")); + File explodedRoot = explode(createJarArchive("archive.war", "WEB-INF", true, extraLibs)); + WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true)); + Iterator archives = launcher.getClassPathArchivesIterator(); + URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives); + URL[] urls = classLoader.getURLs(); + List expectedFiles = getExpectedFilesWithExtraLibs(explodedRoot); + URL[] expectedFileUrls = expectedFiles.stream().map(this::toUrl).toArray(URL[]::new); + assertThat(urls).containsExactly(expectedFileUrls); + } + + protected final URL[] getExpectedFileUrls(File explodedRoot) { + return getExpectedFiles(explodedRoot).stream().map(this::toUrl).toArray(URL[]::new); + } + + protected final List getExpectedFiles(File parent) { + List expected = new ArrayList<>(); + expected.add(new File(parent, "WEB-INF/classes")); + expected.add(new File(parent, "WEB-INF/lib/foo.jar")); + expected.add(new File(parent, "WEB-INF/lib/bar.jar")); + expected.add(new File(parent, "WEB-INF/lib/baz.jar")); + return expected; + } + + protected final List getExpectedFilesWithExtraLibs(File parent) { + List expected = new ArrayList<>(); + expected.add(new File(parent, "WEB-INF/classes")); + expected.add(new File(parent, "WEB-INF/lib/extra-1.jar")); + expected.add(new File(parent, "WEB-INF/lib/extra-2.jar")); + expected.add(new File(parent, "WEB-INF/lib/foo.jar")); + expected.add(new File(parent, "WEB-INF/lib/bar.jar")); + expected.add(new File(parent, "WEB-INF/lib/baz.jar")); + return expected; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java new file mode 100755 index 000000000000..77d2ce185c44 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java @@ -0,0 +1,189 @@ +/* + * Copyright 2012-2023 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.loader.archive; + +import java.io.File; +import java.io.FileOutputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.TestJarCreator; +import org.springframework.boot.loader.archive.Archive.Entry; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ExplodedArchive}. + * + * @author Phillip Webb + * @author Dave Syer + * @author Andy Wilkinson + */ +class ExplodedArchiveTests { + + @TempDir + File tempDir; + + private File rootDirectory; + + private ExplodedArchive archive; + + @BeforeEach + void setup() throws Exception { + createArchive(); + } + + @AfterEach + void tearDown() throws Exception { + if (this.archive != null) { + this.archive.close(); + } + } + + private void createArchive() throws Exception { + createArchive(null); + } + + private void createArchive(String directoryName) throws Exception { + File file = new File(this.tempDir, "test.jar"); + TestJarCreator.createTestJar(file); + this.rootDirectory = (StringUtils.hasText(directoryName) ? new File(this.tempDir, directoryName) + : new File(this.tempDir, UUID.randomUUID().toString())); + JarFile jarFile = new JarFile(file); + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + File destination = new File(this.rootDirectory.getAbsolutePath() + File.separator + entry.getName()); + destination.getParentFile().mkdirs(); + if (entry.isDirectory()) { + destination.mkdir(); + } + else { + FileCopyUtils.copy(jarFile.getInputStream(entry), new FileOutputStream(destination)); + } + } + this.archive = new ExplodedArchive(this.rootDirectory); + jarFile.close(); + } + + @Test + void getManifest() throws Exception { + assertThat(this.archive.getManifest().getMainAttributes().getValue("Built-By")).isEqualTo("j1"); + } + + @Test + void getEntries() { + Map entries = getEntriesMap(this.archive); + assertThat(entries).hasSize(12); + } + + @Test + void getUrl() throws Exception { + assertThat(this.archive.getUrl()).isEqualTo(this.rootDirectory.toURI().toURL()); + } + + @Test + void getUrlWithSpaceInPath() throws Exception { + createArchive("spaces in the name"); + assertThat(this.archive.getUrl()).isEqualTo(this.rootDirectory.toURI().toURL()); + } + + @Test + void getNestedArchive() throws Exception { + Entry entry = getEntriesMap(this.archive).get("nested.jar"); + Archive nested = this.archive.getNestedArchive(entry); + assertThat(nested.getUrl()).hasToString(this.rootDirectory.toURI() + "nested.jar"); + nested.close(); + } + + @Test + void nestedDirArchive() throws Exception { + Entry entry = getEntriesMap(this.archive).get("d/"); + Archive nested = this.archive.getNestedArchive(entry); + Map nestedEntries = getEntriesMap(nested); + assertThat(nestedEntries).hasSize(1); + assertThat(nested.getUrl()).hasToString("file:" + this.rootDirectory.toURI().getPath() + "d/"); + } + + @Test + void getNonRecursiveEntriesForRoot() throws Exception { + try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("/"), false)) { + Map entries = getEntriesMap(explodedArchive); + assertThat(entries).hasSizeGreaterThan(1); + } + } + + @Test + void getNonRecursiveManifest() throws Exception { + try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("src/test/resources/root"))) { + assertThat(explodedArchive.getManifest()).isNotNull(); + Map entries = getEntriesMap(explodedArchive); + assertThat(entries).hasSize(4); + } + } + + @Test + void getNonRecursiveManifestEvenIfNonRecursive() throws Exception { + try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("src/test/resources/root"), false)) { + assertThat(explodedArchive.getManifest()).isNotNull(); + Map entries = getEntriesMap(explodedArchive); + assertThat(entries).hasSize(3); + } + } + + @Test + void getResourceAsStream() throws Exception { + try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("src/test/resources/root"))) { + assertThat(explodedArchive.getManifest()).isNotNull(); + URLClassLoader loader = new URLClassLoader(new URL[] { explodedArchive.getUrl() }); + assertThat(loader.getResourceAsStream("META-INF/spring/application.xml")).isNotNull(); + loader.close(); + } + } + + @Test + void getResourceAsStreamNonRecursive() throws Exception { + try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("src/test/resources/root"), false)) { + assertThat(explodedArchive.getManifest()).isNotNull(); + URLClassLoader loader = new URLClassLoader(new URL[] { explodedArchive.getUrl() }); + assertThat(loader.getResourceAsStream("META-INF/spring/application.xml")).isNotNull(); + loader.close(); + } + } + + private Map getEntriesMap(Archive archive) { + Map entries = new HashMap<>(); + for (Archive.Entry entry : archive) { + entries.put(entry.getName(), entry); + } + return entries; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java new file mode 100755 index 000000000000..4b2ce93af634 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java @@ -0,0 +1,207 @@ +/* + * Copyright 2012-2023 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.loader.archive; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URL; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.TestJarCreator; +import org.springframework.boot.loader.archive.Archive.Entry; +import org.springframework.boot.loader.jar.JarFile; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JarFileArchive}. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Camille Vienot + */ +class JarFileArchiveTests { + + @TempDir + File tempDir; + + private File rootJarFile; + + private JarFileArchive archive; + + private String rootJarFileUrl; + + @BeforeEach + void setup() throws Exception { + setup(false); + } + + @AfterEach + void tearDown() throws Exception { + this.archive.close(); + } + + private void setup(boolean unpackNested) throws Exception { + this.rootJarFile = new File(this.tempDir, "root.jar"); + this.rootJarFileUrl = this.rootJarFile.toURI().toString(); + TestJarCreator.createTestJar(this.rootJarFile, unpackNested); + if (this.archive != null) { + this.archive.close(); + } + this.archive = new JarFileArchive(this.rootJarFile); + } + + @Test + void getManifest() throws Exception { + assertThat(this.archive.getManifest().getMainAttributes().getValue("Built-By")).isEqualTo("j1"); + } + + @Test + void getEntries() { + Map entries = getEntriesMap(this.archive); + assertThat(entries).hasSize(12); + } + + @Test + void getUrl() throws Exception { + URL url = this.archive.getUrl(); + assertThat(url).hasToString(this.rootJarFileUrl); + } + + @Test + void getNestedArchive() throws Exception { + Entry entry = getEntriesMap(this.archive).get("nested.jar"); + try (Archive nested = this.archive.getNestedArchive(entry)) { + assertThat(nested.getUrl()).hasToString("jar:" + this.rootJarFileUrl + "!/nested.jar!/"); + } + } + + @Test + void getNestedUnpackedArchive() throws Exception { + setup(true); + Entry entry = getEntriesMap(this.archive).get("nested.jar"); + try (Archive nested = this.archive.getNestedArchive(entry)) { + assertThat(nested.getUrl().toString()).startsWith("file:"); + assertThat(nested.getUrl().toString()).endsWith("/nested.jar"); + } + } + + @Test + void unpackedLocationsAreUniquePerArchive() throws Exception { + setup(true); + Entry entry = getEntriesMap(this.archive).get("nested.jar"); + URL firstNestedUrl; + try (Archive firstNested = this.archive.getNestedArchive(entry)) { + firstNestedUrl = firstNested.getUrl(); + } + this.archive.close(); + setup(true); + entry = getEntriesMap(this.archive).get("nested.jar"); + try (Archive secondNested = this.archive.getNestedArchive(entry)) { + URL secondNestedUrl = secondNested.getUrl(); + assertThat(secondNestedUrl).isNotEqualTo(firstNestedUrl); + } + } + + @Test + void unpackedLocationsFromSameArchiveShareSameParent() throws Exception { + setup(true); + try (Archive nestedArchive = this.archive.getNestedArchive(getEntriesMap(this.archive).get("nested.jar")); + Archive anotherNestedArchive = this.archive + .getNestedArchive(getEntriesMap(this.archive).get("another-nested.jar"))) { + File nested = new File(nestedArchive.getUrl().toURI()); + File anotherNested = new File(anotherNestedArchive.getUrl().toURI()); + assertThat(nested).hasParent(anotherNested.getParent()); + } + } + + @Test + void filesInZip64ArchivesAreAllListed() throws IOException { + File file = new File(this.tempDir, "test.jar"); + FileCopyUtils.copy(writeZip64Jar(), file); + try (JarFileArchive zip64Archive = new JarFileArchive(file)) { + @SuppressWarnings("deprecation") + Iterator entries = zip64Archive.iterator(); + for (int i = 0; i < 65537; i++) { + assertThat(entries.hasNext()).as(i + "nth file is present").isTrue(); + entries.next(); + } + } + } + + @Test + void nestedZip64ArchivesAreHandledGracefully() throws Exception { + File file = new File(this.tempDir, "test.jar"); + try (JarOutputStream output = new JarOutputStream(new FileOutputStream(file))) { + JarEntry zip64JarEntry = new JarEntry("nested/zip64.jar"); + output.putNextEntry(zip64JarEntry); + byte[] zip64JarData = writeZip64Jar(); + zip64JarEntry.setSize(zip64JarData.length); + zip64JarEntry.setCompressedSize(zip64JarData.length); + zip64JarEntry.setMethod(ZipEntry.STORED); + CRC32 crc32 = new CRC32(); + crc32.update(zip64JarData); + zip64JarEntry.setCrc(crc32.getValue()); + output.write(zip64JarData); + output.closeEntry(); + } + try (JarFile jarFile = new JarFile(file)) { + ZipEntry nestedEntry = jarFile.getEntry("nested/zip64.jar"); + try (JarFile nestedJarFile = jarFile.getNestedJarFile(nestedEntry)) { + Iterator iterator = nestedJarFile.iterator(); + for (int i = 0; i < 65537; i++) { + assertThat(iterator.hasNext()).as(i + "nth file is present").isTrue(); + iterator.next(); + } + } + } + } + + private byte[] writeZip64Jar() throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (JarOutputStream jarOutput = new JarOutputStream(bytes)) { + for (int i = 0; i < 65537; i++) { + jarOutput.putNextEntry(new JarEntry(i + ".dat")); + jarOutput.closeEntry(); + } + } + return bytes.toByteArray(); + } + + private Map getEntriesMap(Archive archive) { + Map entries = new HashMap<>(); + for (Archive.Entry entry : archive) { + entries.put(entry.getName(), entry); + } + return entries; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java new file mode 100644 index 000000000000..6713814def75 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java @@ -0,0 +1,300 @@ +/* + * Copyright 2012-2023 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.loader.data; + +import java.io.EOFException; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +/** + * Tests for {@link RandomAccessDataFile}. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +class RandomAccessDataFileTests { + + private static final byte[] BYTES; + + static { + BYTES = new byte[256]; + for (int i = 0; i < BYTES.length; i++) { + BYTES[i] = (byte) i; + } + } + + private File tempFile; + + private RandomAccessDataFile file; + + private InputStream inputStream; + + @BeforeEach + void setup(@TempDir File tempDir) throws Exception { + this.tempFile = new File(tempDir, "tempFile"); + FileOutputStream outputStream = new FileOutputStream(this.tempFile); + outputStream.write(BYTES); + outputStream.close(); + this.file = new RandomAccessDataFile(this.tempFile); + this.inputStream = this.file.getInputStream(); + } + + @AfterEach + void cleanup() throws Exception { + this.inputStream.close(); + this.file.close(); + } + + @Test + void fileNotNull() { + assertThatIllegalArgumentException().isThrownBy(() -> new RandomAccessDataFile(null)) + .withMessageContaining("File must not be null"); + } + + @Test + void fileExists() { + File file = new File("/does/not/exist"); + assertThatIllegalArgumentException().isThrownBy(() -> new RandomAccessDataFile(file)) + .withMessageContaining(String.format("File %s must exist", file.getAbsolutePath())); + } + + @Test + void readWithOffsetAndLengthShouldRead() throws Exception { + byte[] read = this.file.read(2, 3); + assertThat(read).isEqualTo(new byte[] { 2, 3, 4 }); + } + + @Test + void readWhenOffsetIsBeyondEOFShouldThrowException() { + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.read(257, 0)); + } + + @Test + void readWhenOffsetIsBeyondEndOfSubsectionShouldThrowException() { + RandomAccessData subsection = this.file.getSubsection(0, 10); + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> subsection.read(11, 0)); + } + + @Test + void readWhenOffsetPlusLengthGreaterThanEOFShouldThrowException() { + assertThatExceptionOfType(EOFException.class).isThrownBy(() -> this.file.read(256, 1)); + } + + @Test + void readWhenOffsetPlusLengthGreaterThanEndOfSubsectionShouldThrowException() { + RandomAccessData subsection = this.file.getSubsection(0, 10); + assertThatExceptionOfType(EOFException.class).isThrownBy(() -> subsection.read(10, 1)); + } + + @Test + void inputStreamRead() throws Exception { + for (int i = 0; i <= 255; i++) { + assertThat(this.inputStream.read()).isEqualTo(i); + } + } + + @Test + void inputStreamReadNullBytes() { + assertThatNullPointerException().isThrownBy(() -> this.inputStream.read(null)) + .withMessage("Bytes must not be null"); + } + + @Test + void inputStreamReadNullBytesWithOffset() { + assertThatNullPointerException().isThrownBy(() -> this.inputStream.read(null, 0, 1)) + .withMessage("Bytes must not be null"); + } + + @Test + void inputStreamReadBytes() throws Exception { + byte[] b = new byte[256]; + int amountRead = this.inputStream.read(b); + assertThat(b).isEqualTo(BYTES); + assertThat(amountRead).isEqualTo(256); + } + + @Test + void inputStreamReadOffsetBytes() throws Exception { + byte[] b = new byte[7]; + this.inputStream.skip(1); + int amountRead = this.inputStream.read(b, 2, 3); + assertThat(b).isEqualTo(new byte[] { 0, 0, 1, 2, 3, 0, 0 }); + assertThat(amountRead).isEqualTo(3); + } + + @Test + void inputStreamReadMoreBytesThanAvailable() throws Exception { + byte[] b = new byte[257]; + int amountRead = this.inputStream.read(b); + assertThat(b).startsWith(BYTES); + assertThat(amountRead).isEqualTo(256); + } + + @Test + void inputStreamReadPastEnd() throws Exception { + this.inputStream.skip(255); + assertThat(this.inputStream.read()).isEqualTo(0xFF); + assertThat(this.inputStream.read()).isEqualTo(-1); + assertThat(this.inputStream.read()).isEqualTo(-1); + } + + @Test + void inputStreamReadZeroLength() throws Exception { + byte[] b = new byte[] { 0x0F }; + int amountRead = this.inputStream.read(b, 0, 0); + assertThat(b).isEqualTo(new byte[] { 0x0F }); + assertThat(amountRead).isZero(); + assertThat(this.inputStream.read()).isZero(); + } + + @Test + void inputStreamSkip() throws Exception { + long amountSkipped = this.inputStream.skip(4); + assertThat(this.inputStream.read()).isEqualTo(4); + assertThat(amountSkipped).isEqualTo(4L); + } + + @Test + void inputStreamSkipMoreThanAvailable() throws Exception { + long amountSkipped = this.inputStream.skip(257); + assertThat(this.inputStream.read()).isEqualTo(-1); + assertThat(amountSkipped).isEqualTo(256L); + } + + @Test + void inputStreamSkipPastEnd() throws Exception { + this.inputStream.skip(256); + long amountSkipped = this.inputStream.skip(1); + assertThat(amountSkipped).isZero(); + } + + @Test + void inputStreamAvailable() throws Exception { + assertThat(this.inputStream.available()).isEqualTo(256); + this.inputStream.skip(56); + assertThat(this.inputStream.available()).isEqualTo(200); + this.inputStream.skip(200); + assertThat(this.inputStream.available()).isZero(); + } + + @Test + void subsectionNegativeOffset() { + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.getSubsection(-1, 1)); + } + + @Test + void subsectionNegativeLength() { + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.getSubsection(0, -1)); + } + + @Test + void subsectionZeroLength() throws Exception { + RandomAccessData subsection = this.file.getSubsection(0, 0); + assertThat(subsection.getInputStream().read()).isEqualTo(-1); + } + + @Test + void subsectionTooBig() { + this.file.getSubsection(0, 256); + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.getSubsection(0, 257)); + } + + @Test + void subsectionTooBigWithOffset() { + this.file.getSubsection(1, 255); + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.getSubsection(1, 256)); + } + + @Test + void subsection() throws Exception { + RandomAccessData subsection = this.file.getSubsection(1, 1); + assertThat(subsection.getInputStream().read()).isOne(); + } + + @Test + void inputStreamReadPastSubsection() throws Exception { + RandomAccessData subsection = this.file.getSubsection(1, 2); + InputStream inputStream = subsection.getInputStream(); + assertThat(inputStream.read()).isOne(); + assertThat(inputStream.read()).isEqualTo(2); + assertThat(inputStream.read()).isEqualTo(-1); + } + + @Test + void inputStreamReadBytesPastSubsection() throws Exception { + RandomAccessData subsection = this.file.getSubsection(1, 2); + InputStream inputStream = subsection.getInputStream(); + byte[] b = new byte[3]; + int amountRead = inputStream.read(b); + assertThat(b).isEqualTo(new byte[] { 1, 2, 0 }); + assertThat(amountRead).isEqualTo(2); + } + + @Test + void inputStreamSkipPastSubsection() throws Exception { + RandomAccessData subsection = this.file.getSubsection(1, 2); + InputStream inputStream = subsection.getInputStream(); + assertThat(inputStream.skip(3)).isEqualTo(2L); + assertThat(inputStream.read()).isEqualTo(-1); + } + + @Test + void inputStreamSkipNegative() throws Exception { + assertThat(this.inputStream.skip(-1)).isZero(); + } + + @Test + void getFile() { + assertThat(this.file.getFile()).isEqualTo(this.tempFile); + } + + @Test + void concurrentReads() throws Exception { + ExecutorService executorService = Executors.newFixedThreadPool(20); + List> results = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + results.add(executorService.submit(() -> { + InputStream subsectionInputStream = RandomAccessDataFileTests.this.file.getSubsection(0, 256) + .getInputStream(); + byte[] b = new byte[256]; + subsectionInputStream.read(b); + return Arrays.equals(b, BYTES); + })); + } + for (Future future : results) { + assertThat(future.get()).isTrue(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java new file mode 100644 index 000000000000..dd2505016386 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java @@ -0,0 +1,196 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link AsciiBytes}. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +class AsciiBytesTests { + + private static final char NO_SUFFIX = 0; + + @Test + void createFromBytes() { + AsciiBytes bytes = new AsciiBytes(new byte[] { 65, 66 }); + assertThat(bytes).hasToString("AB"); + } + + @Test + void createFromBytesWithOffset() { + AsciiBytes bytes = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2); + assertThat(bytes).hasToString("BC"); + } + + @Test + void createFromString() { + AsciiBytes bytes = new AsciiBytes("AB"); + assertThat(bytes).hasToString("AB"); + } + + @Test + void length() { + AsciiBytes b1 = new AsciiBytes(new byte[] { 65, 66 }); + AsciiBytes b2 = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2); + assertThat(b1.length()).isEqualTo(2); + assertThat(b2.length()).isEqualTo(2); + } + + @Test + void startWith() { + AsciiBytes abc = new AsciiBytes(new byte[] { 65, 66, 67 }); + AsciiBytes ab = new AsciiBytes(new byte[] { 65, 66 }); + AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67 }, 1, 2); + AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 }); + assertThat(abc.startsWith(abc)).isTrue(); + assertThat(abc.startsWith(ab)).isTrue(); + assertThat(abc.startsWith(bc)).isFalse(); + assertThat(abc.startsWith(abcd)).isFalse(); + } + + @Test + void endsWith() { + AsciiBytes abc = new AsciiBytes(new byte[] { 65, 66, 67 }); + AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67 }, 1, 2); + AsciiBytes ab = new AsciiBytes(new byte[] { 65, 66 }); + AsciiBytes aabc = new AsciiBytes(new byte[] { 65, 65, 66, 67 }); + assertThat(abc.endsWith(abc)).isTrue(); + assertThat(abc.endsWith(bc)).isTrue(); + assertThat(abc.endsWith(ab)).isFalse(); + assertThat(abc.endsWith(aabc)).isFalse(); + } + + @Test + void substringFromBeingIndex() { + AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 }); + assertThat(abcd.substring(0)).hasToString("ABCD"); + assertThat(abcd.substring(1)).hasToString("BCD"); + assertThat(abcd.substring(2)).hasToString("CD"); + assertThat(abcd.substring(3)).hasToString("D"); + assertThat(abcd.substring(4).toString()).isEmpty(); + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> abcd.substring(5)); + } + + @Test + void substring() { + AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 }); + assertThat(abcd.substring(0, 4)).hasToString("ABCD"); + assertThat(abcd.substring(1, 3)).hasToString("BC"); + assertThat(abcd.substring(3, 4)).hasToString("D"); + assertThat(abcd.substring(3, 3).toString()).isEmpty(); + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> abcd.substring(3, 5)); + } + + @Test + void hashCodeAndEquals() { + AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 }); + AsciiBytes bc = new AsciiBytes(new byte[] { 66, 67 }); + AsciiBytes bc_substring = new AsciiBytes(new byte[] { 65, 66, 67, 68 }).substring(1, 3); + AsciiBytes bc_string = new AsciiBytes("BC"); + assertThat(bc).hasSameHashCodeAs(bc); + assertThat(bc).hasSameHashCodeAs(bc_substring); + assertThat(bc).hasSameHashCodeAs(bc_string); + assertThat(bc).isEqualTo(bc); + assertThat(bc).isEqualTo(bc_substring); + assertThat(bc).isEqualTo(bc_string); + assertThat(bc.hashCode()).isNotEqualTo(abcd.hashCode()); + assertThat(bc).isNotEqualTo(abcd); + } + + @Test + void hashCodeSameAsString() { + hashCodeSameAsString("abcABC123xyz!"); + } + + @Test + void hashCodeSameAsStringWithSpecial() { + hashCodeSameAsString("special/\u00EB.dat"); + } + + @Test + void hashCodeSameAsStringWithCyrillicCharacters() { + hashCodeSameAsString("\u0432\u0435\u0441\u043D\u0430"); + } + + @Test + void hashCodeSameAsStringWithEmoji() { + hashCodeSameAsString("\ud83d\udca9"); + } + + private void hashCodeSameAsString(String input) { + assertThat(new AsciiBytes(input)).hasSameHashCodeAs(input); + } + + @Test + void matchesSameAsString() { + matchesSameAsString("abcABC123xyz!"); + } + + @Test + void matchesSameAsStringWithSpecial() { + matchesSameAsString("special/\u00EB.dat"); + } + + @Test + void matchesSameAsStringWithCyrillicCharacters() { + matchesSameAsString("\u0432\u0435\u0441\u043D\u0430"); + } + + @Test + void matchesDifferentLengths() { + assertThat(new AsciiBytes("abc").matches("ab", NO_SUFFIX)).isFalse(); + assertThat(new AsciiBytes("abc").matches("abcd", NO_SUFFIX)).isFalse(); + assertThat(new AsciiBytes("abc").matches("abc", NO_SUFFIX)).isTrue(); + assertThat(new AsciiBytes("abc").matches("a", 'b')).isFalse(); + assertThat(new AsciiBytes("abc").matches("abc", 'd')).isFalse(); + assertThat(new AsciiBytes("abc").matches("ab", 'c')).isTrue(); + } + + @Test + void matchesSuffix() { + assertThat(new AsciiBytes("ab").matches("a", 'b')).isTrue(); + } + + @Test + void matchesSameAsStringWithEmoji() { + matchesSameAsString("\ud83d\udca9"); + } + + @Test + void hashCodeFromInstanceMatchesHashCodeFromString() { + String name = "fonts/宋体/simsun.ttf"; + assertThat(new AsciiBytes(name).hashCode()).isEqualTo(AsciiBytes.hashCode(name)); + } + + @Test + void instanceCreatedFromCharSequenceMatchesSameCharSequence() { + String name = "fonts/宋体/simsun.ttf"; + assertThat(new AsciiBytes(name).matches(name, NO_SUFFIX)).isTrue(); + } + + private void matchesSameAsString(String input) { + assertThat(new AsciiBytes(input).matches(input, NO_SUFFIX)).isTrue(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java new file mode 100644 index 000000000000..4d15c21fe30e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.TestJarCreator; +import org.springframework.boot.loader.data.RandomAccessData; +import org.springframework.boot.loader.data.RandomAccessDataFile; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CentralDirectoryParser}. + * + * @author Phillip Webb + */ +class CentralDirectoryParserTests { + + private File jarFile; + + private RandomAccessDataFile jarData; + + @BeforeEach + void setup(@TempDir File tempDir) throws Exception { + this.jarFile = new File(tempDir, "test.jar"); + TestJarCreator.createTestJar(this.jarFile); + this.jarData = new RandomAccessDataFile(this.jarFile); + } + + @AfterEach + void tearDown() throws IOException { + this.jarData.close(); + } + + @Test + void visitsInOrder() throws Exception { + MockCentralDirectoryVisitor visitor = new MockCentralDirectoryVisitor(); + CentralDirectoryParser parser = new CentralDirectoryParser(); + parser.addVisitor(visitor); + parser.parse(this.jarData, false); + List invocations = visitor.getInvocations(); + assertThat(invocations).startsWith("visitStart").endsWith("visitEnd").contains("visitFileHeader"); + } + + @Test + void visitRecords() throws Exception { + Collector collector = new Collector(); + CentralDirectoryParser parser = new CentralDirectoryParser(); + parser.addVisitor(collector); + parser.parse(this.jarData, false); + Iterator headers = collector.getHeaders().iterator(); + assertThat(headers.next().getName()).hasToString("META-INF/"); + assertThat(headers.next().getName()).hasToString("META-INF/MANIFEST.MF"); + assertThat(headers.next().getName()).hasToString("1.dat"); + assertThat(headers.next().getName()).hasToString("2.dat"); + assertThat(headers.next().getName()).hasToString("d/"); + assertThat(headers.next().getName()).hasToString("d/9.dat"); + assertThat(headers.next().getName()).hasToString("special/"); + assertThat(headers.next().getName()).hasToString("special/\u00EB.dat"); + assertThat(headers.next().getName()).hasToString("nested.jar"); + assertThat(headers.next().getName()).hasToString("another-nested.jar"); + assertThat(headers.next().getName()).hasToString("space nested.jar"); + assertThat(headers.next().getName()).hasToString("multi-release.jar"); + assertThat(headers.hasNext()).isFalse(); + } + + static class Collector implements CentralDirectoryVisitor { + + private final List headers = new ArrayList<>(); + + @Override + public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) { + } + + @Override + public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) { + this.headers.add(fileHeader.clone()); + } + + @Override + public void visitEnd() { + } + + List getHeaders() { + return this.headers; + } + + } + + static class MockCentralDirectoryVisitor implements CentralDirectoryVisitor { + + private final List invocations = new ArrayList<>(); + + @Override + public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) { + this.invocations.add("visitStart"); + } + + @Override + public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) { + this.invocations.add("visitFileHeader"); + } + + @Override + public void visitEnd() { + this.invocations.add("visitEnd"); + } + + List getInvocations() { + return this.invocations; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java new file mode 100644 index 000000000000..1a64de64312c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java @@ -0,0 +1,210 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.TestJarCreator; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Handler}. + * + * @author Andy Wilkinson + */ +@ExtendWith(JarUrlProtocolHandler.class) +class HandlerTests { + + private final Handler handler = new Handler(); + + @Test + void parseUrlWithJarRootContextAndAbsoluteSpecThatUsesContext() throws MalformedURLException { + String spec = "/entry.txt"; + URL context = createUrl("file:example.jar!/"); + this.handler.parseURL(context, spec, 0, spec.length()); + assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt"); + } + + @Test + void parseUrlWithDirectoryEntryContextAndAbsoluteSpecThatUsesContext() throws MalformedURLException { + String spec = "/entry.txt"; + URL context = createUrl("file:example.jar!/dir/"); + this.handler.parseURL(context, spec, 0, spec.length()); + assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt"); + } + + @Test + void parseUrlWithJarRootContextAndRelativeSpecThatUsesContext() throws MalformedURLException { + String spec = "entry.txt"; + URL context = createUrl("file:example.jar!/"); + this.handler.parseURL(context, spec, 0, spec.length()); + assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt"); + } + + @Test + void parseUrlWithDirectoryEntryContextAndRelativeSpecThatUsesContext() throws MalformedURLException { + String spec = "entry.txt"; + URL context = createUrl("file:example.jar!/dir/"); + this.handler.parseURL(context, spec, 0, spec.length()); + assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/dir/entry.txt"); + } + + @Test + void parseUrlWithFileEntryContextAndRelativeSpecThatUsesContext() throws MalformedURLException { + String spec = "entry.txt"; + URL context = createUrl("file:example.jar!/dir/file"); + this.handler.parseURL(context, spec, 0, spec.length()); + assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/dir/entry.txt"); + } + + @Test + void parseUrlWithSpecThatIgnoresContext() throws MalformedURLException { + JarFile.registerUrlProtocolHandler(); + String spec = "jar:file:/other.jar!/nested!/entry.txt"; + URL context = createUrl("file:example.jar!/dir/file"); + this.handler.parseURL(context, spec, 0, spec.length()); + assertThat(context.toExternalForm()).isEqualTo("jar:jar:file:/other.jar!/nested!/entry.txt"); + } + + @Test + void sameFileReturnsFalseForUrlsWithDifferentProtocols() throws MalformedURLException { + assertThat(this.handler.sameFile(new URL("jar:file:foo.jar!/content.txt"), new URL("file:/foo.jar"))).isFalse(); + } + + @Test + void sameFileReturnsFalseForDifferentFileInSameJar() throws MalformedURLException { + assertThat(this.handler.sameFile(new URL("jar:file:foo.jar!/the/path/to/the/first/content.txt"), + new URL("jar:file:/foo.jar!/content.txt"))) + .isFalse(); + } + + @Test + void sameFileReturnsFalseForSameFileInDifferentJars() throws MalformedURLException { + assertThat(this.handler.sameFile(new URL("jar:file:/the/path/to/the/first.jar!/content.txt"), + new URL("jar:file:/second.jar!/content.txt"))) + .isFalse(); + } + + @Test + void sameFileReturnsTrueForSameFileInSameJar() throws MalformedURLException { + assertThat(this.handler.sameFile(new URL("jar:file:/the/path/to/the/first.jar!/content.txt"), + new URL("jar:file:/the/path/to/the/first.jar!/content.txt"))) + .isTrue(); + } + + @Test + void sameFileReturnsTrueForUrlsThatReferenceSameFileViaNestedArchiveAndFromRootOfJar() + throws MalformedURLException { + assertThat(this.handler.sameFile(new URL("jar:file:/test.jar!/BOOT-INF/classes!/foo.txt"), + new URL("jar:file:/test.jar!/BOOT-INF/classes/foo.txt"))) + .isTrue(); + } + + @Test + void hashCodesAreEqualForUrlsThatReferenceSameFileViaNestedArchiveAndFromRootOfJar() throws MalformedURLException { + assertThat(this.handler.hashCode(new URL("jar:file:/test.jar!/BOOT-INF/classes!/foo.txt"))) + .isEqualTo(this.handler.hashCode(new URL("jar:file:/test.jar!/BOOT-INF/classes/foo.txt"))); + } + + @Test + void urlWithSpecReferencingParentDirectory() throws MalformedURLException { + assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes!/xsd/directoryA/a.xsd", + "../directoryB/c/d/e.xsd"); + } + + @Test + void urlWithSpecReferencingAncestorDirectoryOutsideJarStopsAtJarRoot() throws MalformedURLException { + assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes!/xsd/directoryA/a.xsd", + "../../../../../../directoryB/b.xsd"); + } + + @Test + void urlWithSpecReferencingCurrentDirectory() throws MalformedURLException { + assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes!/xsd/directoryA/a.xsd", + "./directoryB/c/d/e.xsd"); + } + + @Test + void urlWithRef() throws MalformedURLException { + assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes", "!/foo.txt#alpha"); + } + + @Test + void urlWithQuery() throws MalformedURLException { + assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes", "!/foo.txt?alpha"); + } + + @Test + void fallbackToJdksJarUrlStreamHandler(@TempDir File tempDir) throws Exception { + File testJar = new File(tempDir, "test.jar"); + TestJarCreator.createTestJar(testJar); + URLConnection connection = new URL(null, "jar:" + testJar.toURI().toURL() + "!/nested.jar!/", this.handler) + .openConnection(); + assertThat(connection).isInstanceOf(JarURLConnection.class); + ((JarURLConnection) connection).getJarFile().close(); + URLConnection jdkConnection = new URL(null, "jar:file:" + testJar.toURI().toURL() + "!/nested.jar!/", + this.handler) + .openConnection(); + assertThat(jdkConnection).isNotInstanceOf(JarURLConnection.class); + assertThat(jdkConnection.getClass().getName()).endsWith(".JarURLConnection"); + } + + @Test + void whenJarHasAPlusInItsPathConnectionJarFileMatchesOriginalJarFile(@TempDir File tempDir) throws Exception { + File testJar = new File(tempDir, "t+e+s+t.jar"); + TestJarCreator.createTestJar(testJar); + URL url = new URL(null, "jar:" + testJar.toURI().toURL() + "!/nested.jar!/3.dat", this.handler); + JarURLConnection connection = (JarURLConnection) url.openConnection(); + try (JarFile jarFile = JarFileWrapper.unwrap(connection.getJarFile())) { + assertThat(jarFile.getRootJarFile().getFile()).isEqualTo(testJar); + } + } + + @Test + void whenJarHasASpaceInItsPathConnectionJarFileMatchesOriginalJarFile(@TempDir File tempDir) throws Exception { + File testJar = new File(tempDir, "t e s t.jar"); + TestJarCreator.createTestJar(testJar); + URL url = new URL(null, "jar:" + testJar.toURI().toURL() + "!/nested.jar!/3.dat", this.handler); + JarURLConnection connection = (JarURLConnection) url.openConnection(); + try (JarFile jarFile = JarFileWrapper.unwrap(connection.getJarFile())) { + assertThat(jarFile.getRootJarFile().getFile()).isEqualTo(testJar); + } + } + + private void assertStandardAndCustomHandlerUrlsAreEqual(String context, String spec) throws MalformedURLException { + URL standardUrl = new URL(new URL("jar:" + context), spec); + URL customHandlerUrl = new URL(new URL("jar", null, -1, context, this.handler), spec); + assertThat(customHandlerUrl).hasToString(standardUrl.toString()); + assertThat(customHandlerUrl.getFile()).isEqualTo(standardUrl.getFile()); + assertThat(customHandlerUrl.getPath()).isEqualTo(standardUrl.getPath()); + assertThat(customHandlerUrl.getQuery()).isEqualTo(standardUrl.getQuery()); + assertThat(customHandlerUrl.getRef()).isEqualTo(standardUrl.getRef()); + } + + private URL createUrl(String file) throws MalformedURLException { + return new URL("jar", null, -1, file, this.handler); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java new file mode 100644 index 000000000000..b37a99183a72 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java @@ -0,0 +1,736 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FilePermission; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.stream.Stream; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.TestJarCreator; +import org.springframework.boot.loader.data.RandomAccessDataFile; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StopWatch; +import org.springframework.util.StreamUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIOException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.spy; + +/** + * Tests for {@link JarFile}. + * + * @author Phillip Webb + * @author Martin Lau + * @author Andy Wilkinson + * @author Madhura Bhave + */ +@ExtendWith(JarUrlProtocolHandler.class) +class JarFileTests { + + private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs"; + + private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader"; + + @TempDir + File tempDir; + + private File rootJarFile; + + private JarFile jarFile; + + @BeforeEach + void setup() throws Exception { + this.rootJarFile = new File(this.tempDir, "root.jar"); + TestJarCreator.createTestJar(this.rootJarFile); + this.jarFile = new JarFile(this.rootJarFile); + } + + @AfterEach + void tearDown() throws Exception { + this.jarFile.close(); + } + + @Test + void jdkJarFile() throws Exception { + // Sanity checks to see how the default jar file operates + java.util.jar.JarFile jarFile = new java.util.jar.JarFile(this.rootJarFile); + assertThat(jarFile.getComment()).isEqualTo("outer"); + Enumeration entries = jarFile.entries(); + assertThat(entries.nextElement().getName()).isEqualTo("META-INF/"); + assertThat(entries.nextElement().getName()).isEqualTo("META-INF/MANIFEST.MF"); + assertThat(entries.nextElement().getName()).isEqualTo("1.dat"); + assertThat(entries.nextElement().getName()).isEqualTo("2.dat"); + assertThat(entries.nextElement().getName()).isEqualTo("d/"); + assertThat(entries.nextElement().getName()).isEqualTo("d/9.dat"); + assertThat(entries.nextElement().getName()).isEqualTo("special/"); + assertThat(entries.nextElement().getName()).isEqualTo("special/\u00EB.dat"); + assertThat(entries.nextElement().getName()).isEqualTo("nested.jar"); + assertThat(entries.nextElement().getName()).isEqualTo("another-nested.jar"); + assertThat(entries.nextElement().getName()).isEqualTo("space nested.jar"); + assertThat(entries.nextElement().getName()).isEqualTo("multi-release.jar"); + assertThat(entries.hasMoreElements()).isFalse(); + URL jarUrl = new URL("jar:" + this.rootJarFile.toURI() + "!/"); + URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { jarUrl }); + assertThat(urlClassLoader.getResource("special/\u00EB.dat")).isNotNull(); + assertThat(urlClassLoader.getResource("d/9.dat")).isNotNull(); + urlClassLoader.close(); + jarFile.close(); + } + + @Test + void createFromFile() throws Exception { + JarFile jarFile = new JarFile(this.rootJarFile); + assertThat(jarFile.getName()).isNotNull(); + jarFile.close(); + } + + @Test + void getManifest() throws Exception { + assertThat(this.jarFile.getManifest().getMainAttributes().getValue("Built-By")).isEqualTo("j1"); + } + + @Test + void getManifestEntry() throws Exception { + ZipEntry entry = this.jarFile.getJarEntry("META-INF/MANIFEST.MF"); + Manifest manifest = new Manifest(this.jarFile.getInputStream(entry)); + assertThat(manifest.getMainAttributes().getValue("Built-By")).isEqualTo("j1"); + } + + @Test + void getEntries() { + Enumeration entries = this.jarFile.entries(); + assertThat(entries.nextElement().getName()).isEqualTo("META-INF/"); + assertThat(entries.nextElement().getName()).isEqualTo("META-INF/MANIFEST.MF"); + assertThat(entries.nextElement().getName()).isEqualTo("1.dat"); + assertThat(entries.nextElement().getName()).isEqualTo("2.dat"); + assertThat(entries.nextElement().getName()).isEqualTo("d/"); + assertThat(entries.nextElement().getName()).isEqualTo("d/9.dat"); + assertThat(entries.nextElement().getName()).isEqualTo("special/"); + assertThat(entries.nextElement().getName()).isEqualTo("special/\u00EB.dat"); + assertThat(entries.nextElement().getName()).isEqualTo("nested.jar"); + assertThat(entries.nextElement().getName()).isEqualTo("another-nested.jar"); + assertThat(entries.nextElement().getName()).isEqualTo("space nested.jar"); + assertThat(entries.nextElement().getName()).isEqualTo("multi-release.jar"); + assertThat(entries.hasMoreElements()).isFalse(); + } + + @Test + void getSpecialResourceViaClassLoader() throws Exception { + URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { this.jarFile.getUrl() }); + assertThat(urlClassLoader.getResource("special/\u00EB.dat")).isNotNull(); + urlClassLoader.close(); + } + + @Test + void getJarEntry() { + java.util.jar.JarEntry entry = this.jarFile.getJarEntry("1.dat"); + assertThat(entry).isNotNull(); + assertThat(entry.getName()).isEqualTo("1.dat"); + } + + @Test + void getJarEntryWhenClosed() throws Exception { + this.jarFile.close(); + assertThatZipFileClosedIsThrownBy(() -> this.jarFile.getJarEntry("1.dat")); + } + + @Test + void getInputStream() throws Exception { + InputStream inputStream = this.jarFile.getInputStream(this.jarFile.getEntry("1.dat")); + assertThat(inputStream.available()).isOne(); + assertThat(inputStream.read()).isOne(); + assertThat(inputStream.available()).isZero(); + assertThat(inputStream.read()).isEqualTo(-1); + } + + @Test + void getInputStreamWhenClosed() throws Exception { + ZipEntry entry = this.jarFile.getEntry("1.dat"); + this.jarFile.close(); + assertThatZipFileClosedIsThrownBy(() -> this.jarFile.getInputStream(entry)); + } + + @Test + void getComment() { + assertThat(this.jarFile.getComment()).isEqualTo("outer"); + } + + @Test + void getCommentWhenClosed() throws Exception { + this.jarFile.close(); + assertThatZipFileClosedIsThrownBy(() -> this.jarFile.getComment()); + } + + @Test + void getName() { + assertThat(this.jarFile.getName()).isEqualTo(this.rootJarFile.getPath()); + } + + @Test + void size() throws Exception { + try (ZipFile zip = new ZipFile(this.rootJarFile)) { + assertThat(this.jarFile).hasSize(zip.size()); + } + } + + @Test + void sizeWhenClosed() throws Exception { + this.jarFile.close(); + assertThatZipFileClosedIsThrownBy(() -> this.jarFile.size()); + } + + @Test + void getEntryTime() throws Exception { + java.util.jar.JarFile jdkJarFile = new java.util.jar.JarFile(this.rootJarFile); + assertThat(this.jarFile.getEntry("META-INF/MANIFEST.MF").getTime()) + .isEqualTo(jdkJarFile.getEntry("META-INF/MANIFEST.MF").getTime()); + jdkJarFile.close(); + } + + @Test + void close() throws Exception { + RandomAccessDataFile randomAccessDataFile = spy(new RandomAccessDataFile(this.rootJarFile)); + JarFile jarFile = new JarFile(randomAccessDataFile); + jarFile.close(); + then(randomAccessDataFile).should().close(); + } + + @Test + void getUrl() throws Exception { + URL url = this.jarFile.getUrl(); + assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/"); + JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection(); + assertThat(JarFileWrapper.unwrap(jarURLConnection.getJarFile())).isSameAs(this.jarFile); + assertThat(jarURLConnection.getJarEntry()).isNull(); + assertThat(jarURLConnection.getContentLength()).isGreaterThan(1); + assertThat(JarFileWrapper.unwrap((java.util.jar.JarFile) jarURLConnection.getContent())).isSameAs(this.jarFile); + assertThat(jarURLConnection.getContentType()).isEqualTo("x-java/jar"); + assertThat(jarURLConnection.getJarFileURL().toURI()).isEqualTo(this.rootJarFile.toURI()); + } + + @Test + void createEntryUrl() throws Exception { + URL url = new URL(this.jarFile.getUrl(), "1.dat"); + assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/1.dat"); + JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection(); + assertThat(JarFileWrapper.unwrap(jarURLConnection.getJarFile())).isSameAs(this.jarFile); + assertThat(jarURLConnection.getJarEntry()).isSameAs(this.jarFile.getJarEntry("1.dat")); + assertThat(jarURLConnection.getContentLength()).isOne(); + assertThat(jarURLConnection.getContent()).isInstanceOf(InputStream.class); + assertThat(jarURLConnection.getContentType()).isEqualTo("content/unknown"); + assertThat(jarURLConnection.getPermission()).isInstanceOf(FilePermission.class); + FilePermission permission = (FilePermission) jarURLConnection.getPermission(); + assertThat(permission.getActions()).isEqualTo("read"); + assertThat(permission.getName()).isEqualTo(this.rootJarFile.getPath()); + } + + @Test + void getMissingEntryUrl() throws Exception { + URL url = new URL(this.jarFile.getUrl(), "missing.dat"); + assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/missing.dat"); + assertThatExceptionOfType(FileNotFoundException.class) + .isThrownBy(((JarURLConnection) url.openConnection())::getJarEntry); + } + + @Test + void getUrlStream() throws Exception { + URL url = this.jarFile.getUrl(); + url.openConnection(); + assertThatIOException().isThrownBy(url::openStream); + } + + @Test + void getEntryUrlStream() throws Exception { + URL url = new URL(this.jarFile.getUrl(), "1.dat"); + url.openConnection(); + try (InputStream stream = url.openStream()) { + assertThat(stream.read()).isOne(); + assertThat(stream.read()).isEqualTo(-1); + } + } + + @Test + void getNestedJarFile() throws Exception { + try (JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { + assertThat(nestedJarFile.getComment()).isEqualTo("nested"); + Enumeration entries = nestedJarFile.entries(); + assertThat(entries.nextElement().getName()).isEqualTo("META-INF/"); + assertThat(entries.nextElement().getName()).isEqualTo("META-INF/MANIFEST.MF"); + assertThat(entries.nextElement().getName()).isEqualTo("3.dat"); + assertThat(entries.nextElement().getName()).isEqualTo("4.dat"); + assertThat(entries.nextElement().getName()).isEqualTo("\u00E4.dat"); + assertThat(entries.hasMoreElements()).isFalse(); + + InputStream inputStream = nestedJarFile.getInputStream(nestedJarFile.getEntry("3.dat")); + assertThat(inputStream.read()).isEqualTo(3); + assertThat(inputStream.read()).isEqualTo(-1); + + URL url = nestedJarFile.getUrl(); + assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/nested.jar!/"); + JarURLConnection conn = (JarURLConnection) url.openConnection(); + assertThat(JarFileWrapper.unwrap(conn.getJarFile())).isSameAs(nestedJarFile); + assertThat(conn.getJarFileURL()).hasToString("jar:" + this.rootJarFile.toURI() + "!/nested.jar"); + assertThat(conn.getInputStream()).isNotNull(); + JarInputStream jarInputStream = new JarInputStream(conn.getInputStream()); + assertThat(jarInputStream.getNextJarEntry().getName()).isEqualTo("3.dat"); + assertThat(jarInputStream.getNextJarEntry().getName()).isEqualTo("4.dat"); + assertThat(jarInputStream.getNextJarEntry().getName()).isEqualTo("\u00E4.dat"); + jarInputStream.close(); + assertThat(conn.getPermission()).isInstanceOf(FilePermission.class); + FilePermission permission = (FilePermission) conn.getPermission(); + assertThat(permission.getActions()).isEqualTo("read"); + assertThat(permission.getName()).isEqualTo(this.rootJarFile.getPath()); + } + } + + @Test + void getNestedJarDirectory() throws Exception { + try (JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile.getEntry("d/"))) { + Enumeration entries = nestedJarFile.entries(); + assertThat(entries.nextElement().getName()).isEqualTo("9.dat"); + assertThat(entries.hasMoreElements()).isFalse(); + + try (InputStream inputStream = nestedJarFile.getInputStream(nestedJarFile.getEntry("9.dat"))) { + assertThat(inputStream.read()).isEqualTo(9); + assertThat(inputStream.read()).isEqualTo(-1); + } + + URL url = nestedJarFile.getUrl(); + assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/d!/"); + JarURLConnection connection = (JarURLConnection) url.openConnection(); + assertThat(JarFileWrapper.unwrap(connection.getJarFile())).isSameAs(nestedJarFile); + } + } + + @Test + void getNestedJarEntryUrl() throws Exception { + try (JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { + URL url = nestedJarFile.getJarEntry("3.dat").getUrl(); + assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/nested.jar!/3.dat"); + try (InputStream inputStream = url.openStream()) { + assertThat(inputStream).isNotNull(); + assertThat(inputStream.read()).isEqualTo(3); + } + } + } + + @Test + void createUrlFromString() throws Exception { + String spec = "jar:" + this.rootJarFile.toURI() + "!/nested.jar!/3.dat"; + URL url = new URL(spec); + assertThat(url).hasToString(spec); + JarURLConnection connection = (JarURLConnection) url.openConnection(); + try (InputStream inputStream = connection.getInputStream()) { + assertThat(inputStream).isNotNull(); + assertThat(inputStream.read()).isEqualTo(3); + assertThat(connection.getURL()).hasToString(spec); + assertThat(connection.getJarFileURL()).hasToString("jar:" + this.rootJarFile.toURI() + "!/nested.jar"); + assertThat(connection.getEntryName()).isEqualTo("3.dat"); + connection.getJarFile().close(); + } + } + + @Test + void createNonNestedUrlFromString() throws Exception { + nonNestedJarFileFromString("jar:" + this.rootJarFile.toURI() + "!/2.dat"); + } + + @Test + void createNonNestedUrlFromPathString() throws Exception { + nonNestedJarFileFromString("jar:" + this.rootJarFile.toPath().toUri() + "!/2.dat"); + } + + private void nonNestedJarFileFromString(String spec) throws Exception { + JarFile.registerUrlProtocolHandler(); + URL url = new URL(spec); + assertThat(url).hasToString(spec); + JarURLConnection connection = (JarURLConnection) url.openConnection(); + try (InputStream inputStream = connection.getInputStream()) { + assertThat(inputStream).isNotNull(); + assertThat(inputStream.read()).isEqualTo(2); + assertThat(connection.getURL()).hasToString(spec); + assertThat(connection.getJarFileURL().toURI()).isEqualTo(this.rootJarFile.toURI()); + assertThat(connection.getEntryName()).isEqualTo("2.dat"); + } + connection.getJarFile().close(); + } + + @Test + void getDirectoryInputStream() throws Exception { + InputStream inputStream = this.jarFile.getInputStream(this.jarFile.getEntry("d/")); + assertThat(inputStream).isNotNull(); + assertThat(inputStream.read()).isEqualTo(-1); + } + + @Test + void getDirectoryInputStreamWithoutSlash() throws Exception { + InputStream inputStream = this.jarFile.getInputStream(this.jarFile.getEntry("d")); + assertThat(inputStream).isNotNull(); + assertThat(inputStream.read()).isEqualTo(-1); + } + + @Test + void sensibleToString() throws Exception { + assertThat(this.jarFile).hasToString(this.rootJarFile.getPath()); + try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { + assertThat(nested).hasToString(this.rootJarFile.getPath() + "!/nested.jar"); + } + } + + @Test + void verifySignedJar() throws Exception { + File signedJarFile = getSignedJarFile(); + assertThat(signedJarFile).exists(); + try (java.util.jar.JarFile expected = new java.util.jar.JarFile(signedJarFile)) { + try (JarFile actual = new JarFile(signedJarFile)) { + StopWatch stopWatch = new StopWatch(); + Enumeration actualEntries = actual.entries(); + while (actualEntries.hasMoreElements()) { + JarEntry actualEntry = actualEntries.nextElement(); + java.util.jar.JarEntry expectedEntry = expected.getJarEntry(actualEntry.getName()); + StreamUtils.drain(expected.getInputStream(expectedEntry)); + if (!actualEntry.getName().equals("META-INF/MANIFEST.MF")) { + assertThat(actualEntry.getCertificates()).as(actualEntry.getName()) + .isEqualTo(expectedEntry.getCertificates()); + assertThat(actualEntry.getCodeSigners()).as(actualEntry.getName()) + .isEqualTo(expectedEntry.getCodeSigners()); + } + } + assertThat(stopWatch.getTotalTimeSeconds()).isLessThan(3.0); + } + } + } + + private File getSignedJarFile() { + String[] entries = System.getProperty("java.class.path").split(System.getProperty("path.separator")); + for (String entry : entries) { + if (entry.contains("bcprov")) { + return new File(entry); + } + } + return null; + } + + @Test + void jarFileWithScriptAtTheStart() throws Exception { + File file = new File(this.tempDir, "test.jar"); + InputStream sourceJarContent = new FileInputStream(this.rootJarFile); + FileOutputStream outputStream = new FileOutputStream(file); + StreamUtils.copy("#/bin/bash", Charset.defaultCharset(), outputStream); + FileCopyUtils.copy(sourceJarContent, outputStream); + this.rootJarFile = file; + this.jarFile.close(); + this.jarFile = new JarFile(file); + // Call some other tests to verify + getEntries(); + getNestedJarFile(); + } + + @Test + void cannotLoadMissingJar() throws Exception { + // relates to gh-1070 + try (JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { + URL nestedUrl = nestedJarFile.getUrl(); + URL url = new URL(nestedUrl, nestedJarFile.getUrl() + "missing.jar!/3.dat"); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(url.openConnection()::getInputStream); + } + } + + @Test + void registerUrlProtocolHandlerWithNoExistingRegistration() { + String original = System.getProperty(PROTOCOL_HANDLER); + try { + System.clearProperty(PROTOCOL_HANDLER); + JarFile.registerUrlProtocolHandler(); + String protocolHandler = System.getProperty(PROTOCOL_HANDLER); + assertThat(protocolHandler).isEqualTo(HANDLERS_PACKAGE); + } + finally { + if (original == null) { + System.clearProperty(PROTOCOL_HANDLER); + } + else { + System.setProperty(PROTOCOL_HANDLER, original); + } + } + } + + @Test + void registerUrlProtocolHandlerAddsToExistingRegistration() { + String original = System.getProperty(PROTOCOL_HANDLER); + try { + System.setProperty(PROTOCOL_HANDLER, "com.example"); + JarFile.registerUrlProtocolHandler(); + String protocolHandler = System.getProperty(PROTOCOL_HANDLER); + assertThat(protocolHandler).isEqualTo("com.example|" + HANDLERS_PACKAGE); + } + finally { + if (original == null) { + System.clearProperty(PROTOCOL_HANDLER); + } + else { + System.setProperty(PROTOCOL_HANDLER, original); + } + } + } + + @Test + void jarFileCanBeDeletedOnceItHasBeenClosed() throws Exception { + File jar = new File(this.tempDir, "test.jar"); + TestJarCreator.createTestJar(jar); + JarFile jf = new JarFile(jar); + jf.close(); + assertThat(jar.delete()).isTrue(); + } + + @Test + void createUrlFromStringWithContextWhenNotFound() throws Exception { + // gh-12483 + JarURLConnection.setUseFastExceptions(true); + try { + try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { + URL context = nested.getUrl(); + new URL(context, "jar:" + this.rootJarFile.toURI() + "!/nested.jar!/3.dat").openConnection() + .getInputStream() + .close(); + assertThatExceptionOfType(FileNotFoundException.class) + .isThrownBy(new URL(context, "jar:" + this.rootJarFile.toURI() + "!/no.dat") + .openConnection()::getInputStream); + } + } + finally { + JarURLConnection.setUseFastExceptions(false); + } + } + + @Test + void multiReleaseEntry() throws Exception { + try (JarFile multiRelease = this.jarFile.getNestedJarFile(this.jarFile.getEntry("multi-release.jar"))) { + ZipEntry entry = multiRelease.getEntry("multi-release.dat"); + assertThat(entry.getName()).isEqualTo("multi-release.dat"); + InputStream inputStream = multiRelease.getInputStream(entry); + assertThat(inputStream.available()).isOne(); + assertThat(inputStream.read()).isEqualTo(Runtime.version().feature()); + } + } + + @Test + void zip64JarThatExceedsZipEntryLimitCanBeRead() throws Exception { + File zip64Jar = new File(this.tempDir, "zip64.jar"); + FileCopyUtils.copy(zip64Jar(), zip64Jar); + try (JarFile zip64JarFile = new JarFile(zip64Jar)) { + List entries = Collections.list(zip64JarFile.entries()); + assertThat(entries).hasSize(65537); + for (int i = 0; i < entries.size(); i++) { + JarEntry entry = entries.get(i); + InputStream entryInput = zip64JarFile.getInputStream(entry); + assertThat(entryInput).hasContent("Entry " + (i + 1)); + } + } + } + + @Test + void zip64JarThatExceedsZipSizeLimitCanBeRead() throws Exception { + Assumptions.assumeTrue(this.tempDir.getFreeSpace() > 6 * 1024 * 1024 * 1024, "Insufficient disk space"); + File zip64Jar = new File(this.tempDir, "zip64.jar"); + File entry = new File(this.tempDir, "entry.dat"); + CRC32 crc32 = new CRC32(); + try (FileOutputStream entryOut = new FileOutputStream(entry)) { + byte[] data = new byte[1024 * 1024]; + new Random().nextBytes(data); + for (int i = 0; i < 1024; i++) { + entryOut.write(data); + crc32.update(data); + } + } + try (JarOutputStream jarOutput = new JarOutputStream(new FileOutputStream(zip64Jar))) { + for (int i = 0; i < 6; i++) { + JarEntry storedEntry = new JarEntry("huge-" + i); + storedEntry.setSize(entry.length()); + storedEntry.setCompressedSize(entry.length()); + storedEntry.setCrc(crc32.getValue()); + storedEntry.setMethod(ZipEntry.STORED); + jarOutput.putNextEntry(storedEntry); + try (FileInputStream entryIn = new FileInputStream(entry)) { + StreamUtils.copy(entryIn, jarOutput); + } + jarOutput.closeEntry(); + } + } + try (JarFile zip64JarFile = new JarFile(zip64Jar)) { + assertThat(Collections.list(zip64JarFile.entries())).hasSize(6); + } + } + + @Test + void nestedZip64JarCanBeRead() throws Exception { + File outer = new File(this.tempDir, "outer.jar"); + try (JarOutputStream jarOutput = new JarOutputStream(new FileOutputStream(outer))) { + JarEntry nestedEntry = new JarEntry("nested-zip64.jar"); + byte[] contents = zip64Jar(); + nestedEntry.setSize(contents.length); + nestedEntry.setCompressedSize(contents.length); + CRC32 crc32 = new CRC32(); + crc32.update(contents); + nestedEntry.setCrc(crc32.getValue()); + nestedEntry.setMethod(ZipEntry.STORED); + jarOutput.putNextEntry(nestedEntry); + jarOutput.write(contents); + jarOutput.closeEntry(); + } + try (JarFile outerJarFile = new JarFile(outer)) { + try (JarFile nestedZip64JarFile = outerJarFile + .getNestedJarFile(outerJarFile.getJarEntry("nested-zip64.jar"))) { + List entries = Collections.list(nestedZip64JarFile.entries()); + assertThat(entries).hasSize(65537); + for (int i = 0; i < entries.size(); i++) { + JarEntry entry = entries.get(i); + InputStream entryInput = nestedZip64JarFile.getInputStream(entry); + assertThat(entryInput).hasContent("Entry " + (i + 1)); + } + } + } + } + + private byte[] zip64Jar() throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + JarOutputStream jarOutput = new JarOutputStream(bytes); + for (int i = 0; i < 65537; i++) { + jarOutput.putNextEntry(new JarEntry(i + ".dat")); + jarOutput.write(("Entry " + (i + 1)).getBytes(StandardCharsets.UTF_8)); + jarOutput.closeEntry(); + } + jarOutput.close(); + return bytes.toByteArray(); + } + + @Test + void jarFileEntryWithEpochTimeOfZeroShouldNotFail() throws Exception { + File file = createJarFileWithEpochTimeOfZero(); + try (JarFile jar = new JarFile(file)) { + Enumeration entries = jar.entries(); + JarEntry entry = entries.nextElement(); + assertThat(entry.getLastModifiedTime().toInstant()).isEqualTo(Instant.EPOCH); + assertThat(entry.getName()).isEqualTo("1.dat"); + } + } + + private File createJarFileWithEpochTimeOfZero() throws Exception { + File jarFile = new File(this.tempDir, "temp.jar"); + FileOutputStream fileOutputStream = new FileOutputStream(jarFile); + String comment = "outer"; + try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) { + jarOutputStream.setComment(comment); + JarEntry entry = new JarEntry("1.dat"); + entry.setLastModifiedTime(FileTime.from(Instant.EPOCH)); + jarOutputStream.putNextEntry(entry); + jarOutputStream.write(new byte[] { (byte) 1 }); + jarOutputStream.closeEntry(); + } + + byte[] data = Files.readAllBytes(jarFile.toPath()); + int headerPosition = data.length - ZipFile.ENDHDR - comment.getBytes().length; + int centralHeaderPosition = (int) Bytes.littleEndianValue(data, headerPosition + ZipFile.ENDOFF, 1); + int localHeaderPosition = (int) Bytes.littleEndianValue(data, centralHeaderPosition + ZipFile.CENOFF, 1); + writeTimeBlock(data, centralHeaderPosition + ZipFile.CENTIM, 0); + writeTimeBlock(data, localHeaderPosition + ZipFile.LOCTIM, 0); + + File jar = new File(this.tempDir, "zerotimed.jar"); + Files.write(jar.toPath(), data); + return jar; + } + + private static void writeTimeBlock(byte[] data, int pos, int value) { + data[pos] = (byte) (value & 0xff); + data[pos + 1] = (byte) ((value >> 8) & 0xff); + data[pos + 2] = (byte) ((value >> 16) & 0xff); + data[pos + 3] = (byte) ((value >> 24) & 0xff); + } + + @Test + void iterator() { + Iterator iterator = this.jarFile.iterator(); + List names = new ArrayList<>(); + while (iterator.hasNext()) { + names.add(iterator.next().getName()); + } + assertThat(names).hasSize(12).contains("1.dat"); + } + + @Test + void iteratorWhenClosed() throws IOException { + this.jarFile.close(); + assertThatZipFileClosedIsThrownBy(() -> this.jarFile.iterator()); + } + + @Test + void iteratorWhenClosedLater() throws IOException { + Iterator iterator = this.jarFile.iterator(); + iterator.next(); + this.jarFile.close(); + assertThatZipFileClosedIsThrownBy(() -> iterator.hasNext()); + } + + @Test + void stream() { + Stream stream = this.jarFile.stream().map(JarEntry::getName); + assertThat(stream).hasSize(12).contains("1.dat"); + + } + + private void assertThatZipFileClosedIsThrownBy(ThrowingCallable throwingCallable) { + assertThatIllegalStateException().isThrownBy(throwingCallable).withMessage("zip file closed"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java new file mode 100644 index 000000000000..8ae25b72e17a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java @@ -0,0 +1,281 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.Permission; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.Set; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.jar.JarFileWrapperTests.SpyJarFile.Call; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link JarFileWrapper}. + * + * @author Phillip Webb + */ +class JarFileWrapperTests { + + private SpyJarFile parent; + + private JarFileWrapper wrapper; + + @BeforeEach + void setup(@TempDir File temp) throws Exception { + this.parent = new SpyJarFile(createTempJar(temp)); + this.wrapper = new JarFileWrapper(this.parent); + } + + @AfterEach + void cleanup() throws Exception { + this.parent.close(); + } + + private File createTempJar(File temp) throws IOException { + File file = new File(temp, "temp.jar"); + new JarOutputStream(new FileOutputStream(file)).close(); + return file; + } + + @Test + void getUrlDelegatesToParent() throws MalformedURLException { + this.wrapper.getUrl(); + this.parent.verify(Call.GET_URL); + } + + @Test + void getTypeDelegatesToParent() { + this.wrapper.getType(); + this.parent.verify(Call.GET_TYPE); + } + + @Test + void getPermissionDelegatesToParent() { + this.wrapper.getPermission(); + this.parent.verify(Call.GET_PERMISSION); + } + + @Test + void getManifestDelegatesToParent() throws IOException { + this.wrapper.getManifest(); + this.parent.verify(Call.GET_MANIFEST); + } + + @Test + void entriesDelegatesToParent() { + this.wrapper.entries(); + this.parent.verify(Call.ENTRIES); + } + + @Test + void getJarEntryDelegatesToParent() { + this.wrapper.getJarEntry("test"); + this.parent.verify(Call.GET_JAR_ENTRY); + } + + @Test + void getEntryDelegatesToParent() { + this.wrapper.getEntry("test"); + this.parent.verify(Call.GET_ENTRY); + } + + @Test + void getInputStreamDelegatesToParent() throws IOException { + this.wrapper.getInputStream(); + this.parent.verify(Call.GET_INPUT_STREAM); + } + + @Test + void getEntryInputStreamDelegatesToParent() throws IOException { + ZipEntry entry = new ZipEntry("test"); + this.wrapper.getInputStream(entry); + this.parent.verify(Call.GET_ENTRY_INPUT_STREAM); + } + + @Test + void getCommentDelegatesToParent() { + this.wrapper.getComment(); + this.parent.verify(Call.GET_COMMENT); + } + + @Test + void sizeDelegatesToParent() { + this.wrapper.size(); + this.parent.verify(Call.SIZE); + } + + @Test + void toStringDelegatesToParent() { + assertThat(this.wrapper.toString()).endsWith("temp.jar"); + } + + @Test // gh-22991 + void wrapperMustNotImplementClose() { + // If the wrapper overrides close then on Java 11 a FinalizableResource + // instance will be used to perform cleanup. This can result in a lot + // of additional memory being used since cleanup only occurs when the + // finalizer thread runs. See gh-22991 + assertThatExceptionOfType(NoSuchMethodException.class) + .isThrownBy(() -> JarFileWrapper.class.getDeclaredMethod("close")); + } + + @Test + void streamDelegatesToParent() { + this.wrapper.stream(); + this.parent.verify(Call.STREAM); + } + + /** + * {@link JarFile} that we can spy (even on Java 11+) + */ + static class SpyJarFile extends JarFile { + + private final Set calls = EnumSet.noneOf(Call.class); + + SpyJarFile(File file) throws IOException { + super(file); + } + + @Override + Permission getPermission() { + mark(Call.GET_PERMISSION); + return super.getPermission(); + } + + @Override + public Manifest getManifest() throws IOException { + mark(Call.GET_MANIFEST); + return super.getManifest(); + } + + @Override + public Enumeration entries() { + mark(Call.ENTRIES); + return super.entries(); + } + + @Override + public Stream stream() { + mark(Call.STREAM); + return super.stream(); + } + + @Override + public JarEntry getJarEntry(String name) { + mark(Call.GET_JAR_ENTRY); + return super.getJarEntry(name); + } + + @Override + public ZipEntry getEntry(String name) { + mark(Call.GET_ENTRY); + return super.getEntry(name); + } + + @Override + InputStream getInputStream() throws IOException { + mark(Call.GET_INPUT_STREAM); + return super.getInputStream(); + } + + @Override + InputStream getInputStream(String name) throws IOException { + mark(Call.GET_ENTRY_INPUT_STREAM); + return super.getInputStream(name); + } + + @Override + public String getComment() { + mark(Call.GET_COMMENT); + return super.getComment(); + } + + @Override + public int size() { + mark(Call.SIZE); + return super.size(); + } + + @Override + public URL getUrl() throws MalformedURLException { + mark(Call.GET_URL); + return super.getUrl(); + } + + @Override + JarFileType getType() { + mark(Call.GET_TYPE); + return super.getType(); + } + + private void mark(Call call) { + this.calls.add(call); + } + + void verify(Call call) { + assertThat(call).matches(this.calls::contains); + } + + enum Call { + + GET_URL, + + GET_TYPE, + + GET_PERMISSION, + + GET_MANIFEST, + + ENTRIES, + + GET_JAR_ENTRY, + + GET_ENTRY, + + GET_INPUT_STREAM, + + GET_ENTRY_INPUT_STREAM, + + GET_COMMENT, + + SIZE, + + STREAM + + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java new file mode 100644 index 000000000000..d962a72fc575 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java @@ -0,0 +1,246 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.URL; +import java.util.List; +import java.util.jar.JarEntry; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.TestJarCreator; +import org.springframework.boot.loader.jar.JarURLConnection.JarEntryName; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link JarURLConnection}. + * + * @author Andy Wilkinson + * @author Phillip Webb + * @author Rostyslav Dudka + */ +class JarURLConnectionTests { + + private File rootJarFile; + + private JarFile jarFile; + + @BeforeEach + void setup(@TempDir File tempDir) throws Exception { + this.rootJarFile = new File(tempDir, "root.jar"); + TestJarCreator.createTestJar(this.rootJarFile); + this.jarFile = new JarFile(this.rootJarFile); + } + + @AfterEach + void tearDown() throws Exception { + this.jarFile.close(); + } + + @Test + void connectionToRootUsingAbsoluteUrl() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/"); + Object content = JarURLConnection.get(url, this.jarFile).getContent(); + assertThat(JarFileWrapper.unwrap((java.util.jar.JarFile) content)).isSameAs(this.jarFile); + } + + @Test + void connectionToRootUsingRelativeUrl() throws Exception { + URL url = new URL("jar:file:" + getRelativePath() + "!/"); + Object content = JarURLConnection.get(url, this.jarFile).getContent(); + assertThat(JarFileWrapper.unwrap((java.util.jar.JarFile) content)).isSameAs(this.jarFile); + } + + @Test + void connectionToEntryUsingAbsoluteUrl() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/1.dat"); + try (InputStream input = JarURLConnection.get(url, this.jarFile).getInputStream()) { + assertThat(input).hasBinaryContent(new byte[] { 1 }); + } + } + + @Test + void connectionToEntryUsingRelativeUrl() throws Exception { + URL url = new URL("jar:file:" + getRelativePath() + "!/1.dat"); + try (InputStream input = JarURLConnection.get(url, this.jarFile).getInputStream()) { + assertThat(input).hasBinaryContent(new byte[] { 1 }); + } + } + + @Test + void connectionToEntryUsingAbsoluteUrlWithFileColonSlashSlashPrefix() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/1.dat"); + try (InputStream input = JarURLConnection.get(url, this.jarFile).getInputStream()) { + assertThat(input).hasBinaryContent(new byte[] { 1 }); + } + } + + @Test + void connectionToEntryUsingAbsoluteUrlForNestedEntry() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/nested.jar!/3.dat"); + JarURLConnection connection = JarURLConnection.get(url, this.jarFile); + try (InputStream input = connection.getInputStream()) { + assertThat(input).hasBinaryContent(new byte[] { 3 }); + } + connection.getJarFile().close(); + } + + @Test + void connectionToEntryUsingRelativeUrlForNestedEntry() throws Exception { + URL url = new URL("jar:file:" + getRelativePath() + "!/nested.jar!/3.dat"); + JarURLConnection connection = JarURLConnection.get(url, this.jarFile); + try (InputStream input = connection.getInputStream()) { + assertThat(input).hasBinaryContent(new byte[] { 3 }); + } + connection.getJarFile().close(); + } + + @Test + void connectionToEntryUsingAbsoluteUrlForEntryFromNestedJarFile() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/nested.jar!/3.dat"); + try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { + try (InputStream input = JarURLConnection.get(url, nested).getInputStream()) { + assertThat(input).hasBinaryContent(new byte[] { 3 }); + } + } + } + + @Test + void connectionToEntryUsingRelativeUrlForEntryFromNestedJarFile() throws Exception { + URL url = new URL("jar:file:" + getRelativePath() + "!/nested.jar!/3.dat"); + try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { + try (InputStream input = JarURLConnection.get(url, nested).getInputStream()) { + assertThat(input).hasBinaryContent(new byte[] { 3 }); + } + } + } + + @Test + void connectionToEntryInNestedJarFromUrlThatUsesExistingUrlAsContext() throws Exception { + URL url = new URL(new URL("jar", null, -1, this.rootJarFile.toURI().toURL() + "!/nested.jar!/", new Handler()), + "/3.dat"); + try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { + try (InputStream input = JarURLConnection.get(url, nested).getInputStream()) { + assertThat(input).hasBinaryContent(new byte[] { 3 }); + } + } + } + + @Test + void connectionToEntryWithSpaceNestedEntry() throws Exception { + URL url = new URL("jar:file:" + getRelativePath() + "!/space nested.jar!/3.dat"); + JarURLConnection connection = JarURLConnection.get(url, this.jarFile); + try (InputStream input = connection.getInputStream()) { + assertThat(input).hasBinaryContent(new byte[] { 3 }); + } + connection.getJarFile().close(); + } + + @Test + void connectionToEntryWithEncodedSpaceNestedEntry() throws Exception { + URL url = new URL("jar:file:" + getRelativePath() + "!/space%20nested.jar!/3.dat"); + JarURLConnection connection = JarURLConnection.get(url, this.jarFile); + try (InputStream input = connection.getInputStream()) { + assertThat(input).hasBinaryContent(new byte[] { 3 }); + } + connection.getJarFile().close(); + } + + @Test + void connectionToEntryUsingWrongAbsoluteUrlForEntryFromNestedJarFile() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/w.jar!/3.dat"); + try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { + assertThatExceptionOfType(FileNotFoundException.class) + .isThrownBy(JarURLConnection.get(url, nested)::getInputStream); + } + } + + @Test + void getContentLengthReturnsLengthOfUnderlyingEntry() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/nested.jar!/3.dat"); + try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { + JarURLConnection connection = JarURLConnection.get(url, nested); + assertThat(connection.getContentLength()).isOne(); + } + } + + @Test + void getContentLengthLongReturnsLengthOfUnderlyingEntry() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/nested.jar!/3.dat"); + try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { + JarURLConnection connection = JarURLConnection.get(url, nested); + assertThat(connection.getContentLengthLong()).isOne(); + } + } + + @Test + void getLastModifiedReturnsLastModifiedTimeOfJarEntry() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/1.dat"); + JarURLConnection connection = JarURLConnection.get(url, this.jarFile); + assertThat(connection.getLastModified()).isEqualTo(connection.getJarEntry().getTime()); + } + + @Test + void entriesCanBeStreamedFromJarFileOfConnection() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/"); + JarURLConnection connection = JarURLConnection.get(url, this.jarFile); + List entryNames = connection.getJarFile().stream().map(JarEntry::getName).toList(); + assertThat(entryNames).hasSize(12); + } + + @Test + void jarEntryBasicName() { + assertThat(new JarEntryName(new StringSequence("a/b/C.class"))).hasToString("a/b/C.class"); + } + + @Test + void jarEntryNameWithSingleByteEncodedCharacters() { + assertThat(new JarEntryName(new StringSequence("%61/%62/%43.class"))).hasToString("a/b/C.class"); + } + + @Test + void jarEntryNameWithDoubleByteEncodedCharacters() { + assertThat(new JarEntryName(new StringSequence("%c3%a1/b/C.class"))).hasToString("\u00e1/b/C.class"); + } + + @Test + void jarEntryNameWithMixtureOfEncodedAndUnencodedDoubleByteCharacters() { + assertThat(new JarEntryName(new StringSequence("%c3%a1/b/\u00c7.class"))).hasToString("\u00e1/b/\u00c7.class"); + } + + @Test + void openConnectionCanBeClosedWithoutClosingSourceJar() throws Exception { + URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/"); + JarURLConnection connection = JarURLConnection.get(url, this.jarFile); + java.util.jar.JarFile connectionJarFile = connection.getJarFile(); + connectionJarFile.close(); + assertThat(this.jarFile.isClosed()).isFalse(); + } + + private String getRelativePath() { + return this.rootJarFile.getPath().replace('\\', '/'); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarUrlProtocolHandler.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarUrlProtocolHandler.java new file mode 100644 index 000000000000..d9e5eb281420 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarUrlProtocolHandler.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.io.File; +import java.lang.ref.SoftReference; +import java.util.Map; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; + +import org.springframework.test.util.ReflectionTestUtils; + +/** + * JUnit 5 {@link Extension} for tests that interact with Spring Boot's {@link Handler} + * for {@code jar:} URLs. Ensures that the handler is registered prior to test execution + * and cleans up the handler's root file cache afterwards. + * + * @author Andy Wilkinson + */ +class JarUrlProtocolHandler implements BeforeEachCallback, AfterEachCallback { + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + JarFile.registerUrlProtocolHandler(); + } + + @Override + @SuppressWarnings("unchecked") + public void afterEach(ExtensionContext context) throws Exception { + Map rootFileCache = ((SoftReference>) ReflectionTestUtils + .getField(Handler.class, "rootFileCache")).get(); + if (rootFileCache != null) { + for (JarFile rootJarFile : rootFileCache.values()) { + rootJarFile.close(); + } + rootFileCache.clear(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java new file mode 100644 index 000000000000..ee7170f08c25 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java @@ -0,0 +1,220 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +/** + * Tests for {@link StringSequence}. + * + * @author Phillip Webb + */ +class StringSequenceTests { + + @Test + void createWhenSourceIsNullShouldThrowException() { + assertThatNullPointerException().isThrownBy(() -> new StringSequence(null)) + .withMessage("Source must not be null"); + } + + @Test + void createWithIndexWhenSourceIsNullShouldThrowException() { + assertThatNullPointerException().isThrownBy(() -> new StringSequence(null, 0, 0)) + .withMessage("Source must not be null"); + } + + @Test + void createWhenStartIsLessThanZeroShouldThrowException() { + assertThatExceptionOfType(StringIndexOutOfBoundsException.class) + .isThrownBy(() -> new StringSequence("x", -1, 0)); + } + + @Test + void createWhenEndIsGreaterThanLengthShouldThrowException() { + assertThatExceptionOfType(StringIndexOutOfBoundsException.class) + .isThrownBy(() -> new StringSequence("x", 0, 2)); + } + + @Test + void createFromString() { + assertThat(new StringSequence("test")).hasToString("test"); + } + + @Test + void subSequenceWithJustStartShouldReturnSubSequence() { + assertThat(new StringSequence("smiles").subSequence(1)).hasToString("miles"); + } + + @Test + void subSequenceShouldReturnSubSequence() { + assertThat(new StringSequence("hamburger").subSequence(4, 8)).hasToString("urge"); + assertThat(new StringSequence("smiles").subSequence(1, 5)).hasToString("mile"); + } + + @Test + void subSequenceWhenCalledMultipleTimesShouldReturnSubSequence() { + assertThat(new StringSequence("hamburger").subSequence(4, 8).subSequence(1, 3)).hasToString("rg"); + } + + @Test + void subSequenceWhenEndPastExistingEndShouldThrowException() { + StringSequence sequence = new StringSequence("abcde").subSequence(1, 4); + assertThat(sequence).hasToString("bcd"); + assertThat(sequence.subSequence(2, 3)).hasToString("d"); + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> sequence.subSequence(3, 4)); + } + + @Test + void subSequenceWhenStartPastExistingEndShouldThrowException() { + StringSequence sequence = new StringSequence("abcde").subSequence(1, 4); + assertThat(sequence).hasToString("bcd"); + assertThat(sequence.subSequence(2, 3)).hasToString("d"); + assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> sequence.subSequence(4, 3)); + } + + @Test + void isEmptyWhenEmptyShouldReturnTrue() { + assertThat(new StringSequence("").isEmpty()).isTrue(); + } + + @Test + void isEmptyWhenNotEmptyShouldReturnFalse() { + assertThat(new StringSequence("x").isEmpty()).isFalse(); + } + + @Test + void lengthShouldReturnLength() { + StringSequence sequence = new StringSequence("hamburger"); + assertThat(sequence).hasSize(9); + assertThat(sequence.subSequence(4, 8)).hasSize(4); + } + + @Test + void charAtShouldReturnChar() { + StringSequence sequence = new StringSequence("hamburger"); + assertThat(sequence.charAt(0)).isEqualTo('h'); + assertThat(sequence.charAt(1)).isEqualTo('a'); + assertThat(sequence.subSequence(4, 8).charAt(0)).isEqualTo('u'); + assertThat(sequence.subSequence(4, 8).charAt(1)).isEqualTo('r'); + } + + @Test + void indexOfCharShouldReturnIndexOf() { + StringSequence sequence = new StringSequence("aabbaacc"); + assertThat(sequence.indexOf('a')).isZero(); + assertThat(sequence.indexOf('b')).isEqualTo(2); + assertThat(sequence.subSequence(2).indexOf('a')).isEqualTo(2); + } + + @Test + void indexOfStringShouldReturnIndexOf() { + StringSequence sequence = new StringSequence("aabbaacc"); + assertThat(sequence.indexOf('a')).isZero(); + assertThat(sequence.indexOf('b')).isEqualTo(2); + assertThat(sequence.subSequence(2).indexOf('a')).isEqualTo(2); + } + + @Test + void indexOfStringFromIndexShouldReturnIndexOf() { + StringSequence sequence = new StringSequence("aabbaacc"); + assertThat(sequence.indexOf("a", 2)).isEqualTo(4); + assertThat(sequence.indexOf("b", 3)).isEqualTo(3); + assertThat(sequence.subSequence(2).indexOf("a", 3)).isEqualTo(3); + } + + @Test + void hashCodeShouldBeSameAsString() { + assertThat(new StringSequence("hamburger")).hasSameHashCodeAs("hamburger"); + assertThat(new StringSequence("hamburger").subSequence(4, 8)).hasSameHashCodeAs("urge"); + } + + @Test + void equalsWhenSameContentShouldMatch() { + StringSequence a = new StringSequence("hamburger").subSequence(4, 8); + StringSequence b = new StringSequence("urge"); + StringSequence c = new StringSequence("urgh"); + assertThat(a).isEqualTo(b).isNotEqualTo(c); + } + + @Test + void notEqualsWhenSequencesOfDifferentLength() { + StringSequence a = new StringSequence("abcd"); + StringSequence b = new StringSequence("ef"); + assertThat(a).isNotEqualTo(b); + } + + @Test + void startsWithWhenExactMatch() { + assertThat(new StringSequence("abc").startsWith("abc")).isTrue(); + } + + @Test + void startsWithWhenLongerAndStartsWith() { + assertThat(new StringSequence("abcd").startsWith("abc")).isTrue(); + } + + @Test + void startsWithWhenLongerAndDoesNotStartWith() { + assertThat(new StringSequence("abcd").startsWith("abx")).isFalse(); + } + + @Test + void startsWithWhenShorterAndDoesNotStartWith() { + assertThat(new StringSequence("ab").startsWith("abc")).isFalse(); + assertThat(new StringSequence("ab").startsWith("c")).isFalse(); + } + + @Test + void startsWithOffsetWhenExactMatch() { + assertThat(new StringSequence("xabc").startsWith("abc", 1)).isTrue(); + } + + @Test + void startsWithOffsetWhenLongerAndStartsWith() { + assertThat(new StringSequence("xabcd").startsWith("abc", 1)).isTrue(); + } + + @Test + void startsWithOffsetWhenLongerAndDoesNotStartWith() { + assertThat(new StringSequence("xabcd").startsWith("abx", 1)).isFalse(); + } + + @Test + void startsWithOffsetWhenShorterAndDoesNotStartWith() { + assertThat(new StringSequence("xab").startsWith("abc", 1)).isFalse(); + assertThat(new StringSequence("xab").startsWith("c", 1)).isFalse(); + } + + @Test + void startsWithOnSubstringTailWhenMatch() { + StringSequence subSequence = new StringSequence("xabc").subSequence(1); + assertThat(subSequence.startsWith("abc")).isTrue(); + assertThat(subSequence.startsWith("abcd")).isFalse(); + } + + @Test + void startsWithOnSubstringMiddleWhenMatch() { + StringSequence subSequence = new StringSequence("xabc").subSequence(1, 3); + assertThat(subSequence.startsWith("ab")).isTrue(); + assertThat(subSequence.startsWith("abc")).isFalse(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java new file mode 100644 index 000000000000..dec587e18bb2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2023 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.loader.jarmode; + +import java.util.Collections; +import java.util.Iterator; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.loader.Launcher; +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.testsupport.system.CapturedOutput; +import org.springframework.boot.testsupport.system.OutputCaptureExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Launcher} with jar mode support. + * + * @author Phillip Webb + */ +@ExtendWith(OutputCaptureExtension.class) +class LauncherJarModeTests { + + @BeforeEach + void setup() { + System.setProperty(JarModeLauncher.DISABLE_SYSTEM_EXIT, "true"); + } + + @AfterEach + void cleanup() { + System.clearProperty("jarmode"); + System.clearProperty(JarModeLauncher.DISABLE_SYSTEM_EXIT); + } + + @Test + void launchWhenJarModePropertyIsSetLaunchesJarMode(CapturedOutput out) throws Exception { + System.setProperty("jarmode", "test"); + new TestLauncher().launch(new String[] { "boot" }); + assertThat(out).contains("running in test jar mode [boot]"); + } + + @Test + void launchWhenJarModePropertyIsNotAcceptedThrowsException(CapturedOutput out) throws Exception { + System.setProperty("jarmode", "idontexist"); + new TestLauncher().launch(new String[] { "boot" }); + assertThat(out).contains("Unsupported jarmode 'idontexist'"); + } + + private static class TestLauncher extends Launcher { + + @Override + protected String getMainClass() throws Exception { + throw new IllegalStateException("Should not be called"); + } + + @Override + protected Iterator getClassPathArchivesIterator() throws Exception { + return Collections.emptyIterator(); + } + + @Override + protected void launch(String[] args) throws Exception { + super.launch(args); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java new file mode 100644 index 000000000000..802a762e79dd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2023 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.loader.util; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SystemPropertyUtils}. + * + * @author Dave Syer + */ +class SystemPropertyUtilsTests { + + @BeforeEach + void init() { + System.setProperty("foo", "bar"); + } + + @AfterEach + void close() { + System.clearProperty("foo"); + } + + @Test + void testVanillaPlaceholder() { + assertThat(SystemPropertyUtils.resolvePlaceholders("${foo}")).isEqualTo("bar"); + } + + @Test + void testDefaultValue() { + assertThat(SystemPropertyUtils.resolvePlaceholders("${bar:foo}")).isEqualTo("foo"); + } + + @Test + void testNestedPlaceholder() { + assertThat(SystemPropertyUtils.resolvePlaceholders("${bar:${spam:foo}}")).isEqualTo("foo"); + } + + @Test + void testEnvVar() { + assertThat(SystemPropertyUtils.getProperty("lang")).isEqualTo(System.getenv("LANG")); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/application.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/application.properties new file mode 100644 index 000000000000..85a390f4d4e0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/application.properties @@ -0,0 +1 @@ +loader.main: demo.Application diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/bar.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/bar.properties new file mode 100644 index 000000000000..6b37480f8b99 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/bar.properties @@ -0,0 +1 @@ +loader.main: my.BootInfBarApplication diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/foo.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/foo.properties new file mode 100644 index 000000000000..36bd211df41b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/foo.properties @@ -0,0 +1,3 @@ +foo: Application +loader.main: my.${foo} +loader.path: etc diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/loader.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/loader.properties new file mode 100644 index 000000000000..85a390f4d4e0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/BOOT-INF/classes/loader.properties @@ -0,0 +1 @@ +loader.main: demo.Application diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/META-INF/spring.factories new file mode 100644 index 000000000000..c45c87d76f45 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Jar Modes +org.springframework.boot.loader.jarmode.JarMode=\ +org.springframework.boot.loader.jarmode.TestJarMode \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/bar.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/bar.properties new file mode 100644 index 000000000000..8301c2649f3c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/bar.properties @@ -0,0 +1 @@ +loader.main: my.BarApplication diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/explodedsample/ExampleClass.txt b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/explodedsample/ExampleClass.txt new file mode 100644 index 000000000000..c53100f90fa1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/explodedsample/ExampleClass.txt @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2020 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 explodedsample; + +/** + * Example class used to test class loading. + * + * @author Phillip Webb + */ +public class ExampleClass { + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/home/loader.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/home/loader.properties new file mode 100644 index 000000000000..7a134969b766 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/home/loader.properties @@ -0,0 +1 @@ +loader.main: demo.HomeApplication diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/jars/app.jar b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/jars/app.jar new file mode 100644 index 0000000000000000000000000000000000000000..fb02c027012d66154056f7e7df17ba836ee6d2b8 GIT binary patch literal 2213 zcmWIWW@Zs#;Nak3&?ukh!hi%g8CV#6T|*poJ^kGD|D9rBU}gyLX6FE@V1ge|^G~JD{Zqb94n||$D z(D61fRetw|w~lFAvv#y+uRVA|;M5l0_^WGPN-h_^_Iq;SBBlGxlb4vEKK)8y_N^&Z zjG*Y)@p4|!I-oBk86hD7iA!a)kl;n~XL3$radB>-t^Z*I5u4=~mMBiSbm>#%0_LE^ z47OuRDlJ!5GPwwHPi9_Ll=&InLf~u=Nl}uDr0I+-r-AUK+P~A1r*5WP5;Z7Eh|s z4K?R3GwZp1-Der1Z=@AePHHhpS@GwlO|RRdTa)gY?DWW<&3!cH?vv?i`@Lq=|892w zBofSW=)!`3!VOm*N*YF?0binLOYR)_7$Oe~u{kG-Fg+n^F{ERQx%B(8!x2pY> z9h4f9{{7y8c`ut2`_4V`7xw2c_|JWJVQ72SOIE+FAG&7R@35Dw5RPMi+IfmeYrjkP z0dr7Xy-`}Rst_1Y8o>O6NVQjL=7Ewj9IygeDXF>nNXhmLni4UX632pqoXq6JlFa-( zJxII;#CkJ1ir6k*z4-OCWtsDr*|J=7=H4si=yI_+B;ks)|6{8YC*GXfv@XzowH#N~ z3-&LI7wwi#bz&~IKWF>=+}_V0e}3b8z>q6lsUsTby0#%zy41uX%py$kQ@d8cM?vRp zo2w5e%$>0?MdN#jg8s694sO?z7I1E?S;*Ap_I#bW*!M$Ml_YllwFo-6Vm8Oy%k2Jb z8;uUOB=>&WH`8sNO>pOqBAK?RN+r$r>P74_tbL{I(l=5x=6bL7{e8`)>cqv84Xt7; z+b)^Elh<_Iyj`C?`Q=fbt&(4?56Nu&Z_&BbpL>?(#n<;AYd?;=_{K0-q4oaXOk;lI z4+bYqOxKkEyDfeD#Kr4s>de!0T(@~dPPS2=f4iEsdQn4NOO73@v5V$GPumTXy&4}G z8(n?Fy+-oxGNFXP>oQ`OIua-5S>^`)WCF#K;p18cdtfX*K*>?3(V{>UTNHqb5Z`WF zzC#8)Zr|q|dUZu_uY)?9$~Qiq4n>u~j)ElZGH#Bq?^uo;xV>-w@oRT;cpf+}JNcpL z;FALjMH^*{xx<7-D8O6o$=K@uYc;A_o?&x7rk|KPx_xb?|s@w zU)Squ@mX)5Q=X+PU6#&Ag}jYEf6htXA;it~<<6(iJHh32T-xN*pfcJM>=Z^O5eC${ z23C=P>Kar4cN(bX3GhbMid_7IY8nKv1v24Uk?I|g30w@gs|kb&hk#5-rGc;$RBa#^ zw4iDO0iFPvARVwu1lhHqf*3j5g9>5YbK)R1Eyl++O5KF|nw6rm@) T0B=?{kWx+{R0XDvEHDoMzsJ<_ literal 0 HcmV?d00001 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/more-jars/app.jar b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/more-jars/app.jar new file mode 100644 index 0000000000000000000000000000000000000000..3945fd020d3455db0bef268d12ae67809c90c2b7 GIT binary patch literal 1150 zcmWIWW@Zs#;Nak3u<&^2#()Gk8CV#6T|*poJ^kGD|D9rBU}gyLX6FE@V1gH`sg4aoHjkb7SsI^Urdhzdio^ zR`vknEVEB5L;_vcHn>WcnpjM;m?rtDT`S~ShvMy9d($@feQ_|FVx=`jeDVVQ$J!Ua zb8ci>=+x%+dYiG(_d{P@ElR4@!X9pzEAaL*yT6;;%Yri-ol5=vllBb!Q;M%t z%6zY0!Y;$w7s{R;k-lP%_xkRnoCy*|Nz%7y(GX8RxH={&pGt!ir!ua zbv6~VHa?z?gDQbi6OV5ZrSyRTh7ejpQ?{1NU2WdP(T0c8LLcm!mEwP0iiWQT(y j4>>$Qk%s_Tz@We|0~D13-mGjOWvoDG4onS(%pe{B`N?6@ literal 0 HcmV?d00001 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/app.jar b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/app.jar new file mode 100644 index 0000000000000000000000000000000000000000..5600ed279efb99189791ed00b3bbb4203efaa208 GIT binary patch literal 3313 zcmc&$&u`pB6n@^_WV6nuNq;15Xxh2~q)k!0O}QYpMNPK}O_Q`y6M{s6%GsR+x4Rz8 z_NFZmp^5|Q2_a4hlmkM5I3U3R5E5MA)b@n<11fPqNN_+1aX}T|vmM*JF%=R*cr~_n z{NBv_@qO>j!fZCD67>*`oW6OM5?6t8G(WX8IX-i4x_s{z(L$T(z|Qwary$N_4@M+9 zKY4CudTMb=pP!yLEOS-3-uSX`TvJ+)YZLm>KDFuy!xQ!KQ`dRoG5zSJiDMI5>AYjq z8bYf|$Ci%aL4K@H?Nj4z4`1CpoiGsg5gE2!jwK$1#LuH2epJd3@#qUBrlr(t4a0SB z-TU%u%sonnRqCc)1=>w}dT3CkA=;a#eR&$r(*p{1A2%)2dqSaXX>3`cTvgVELj7~5 zCC;^)E5cbaRvMVeIxS0~q0+^<%f`AY{8pH8E|nY!4g3VhRmk zXjz`w6w9V-Vx39LlAiCdtI&}->8zDqn-^JijHb9MohxP66ONGHbz79HvS~|8SYD-w zv=#ESzepo=K%pJjK!ZURG<99i)4?JgB2A$@2MHe)8i>t2zj9gBJnWaY-HMbRZ`3rb zIy((}E#b83FJ6aDO@u%=_%Ij*z?9_$815kk!e~|$+U;-HJ`ID|JFkJz4VKa(M~A5k z>46|}mBAB!;trhB0X!8vyN-NL$|rbHs26t@#w6XKB9b16J`7c0fC`^dW>@wz%EjWU z|DnfUH$yuhl%aka2p8I!#5#lSi=Q?x3}XWxMF@rQ^s{LjwJXM&&@fj~O{B*0G=x;| z$e_+4)whN8zAACjSusqydNO)f?bb@etZ6>7HO@ZvZ-^$a;etjT+0$N93n$O7BVA@) zsD)5>U7B?UO(}paaHAQ{nmYz*EpY|e)}gG2xhsvf!)WM8>zNP8PLWrCy@zlf)V><3mpDW*vIK&?!hVC3;379J*H&9y$nQp!47^BG53(h zPVTXIz`Yz+?egn$4uU%{`tdD812HCU0~1ev@N463eBd#N(HXrq3i8KFJ1G)@4iO#V z-sOT@(|q_d3PSu}d@h8)_kts@x>^+d1Rp{!Ao4L661mHEN!8OyT}*w=!CutVwItTx&T( zpR)MOGl{usIOkX86VIxKZR^ugqTqMi@x+a4uW$&@%{p*nb$!8 literal 0 HcmV?d00001 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/nested-jar-app.jar b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/nested-jar-app.jar new file mode 100644 index 0000000000000000000000000000000000000000..4c2254f6352b5443de1396397423068a792b2eb5 GIT binary patch literal 1408 zcmWIWW@h1H0D(E)T7h5&l;C8LVeoYgan$wnbJGtE;bdSw{x~uogi9;985mio! z6LHmP3{$f|6@4sX1Um@gryeYRa`N{NLHKDo&;=lj#ZM3;`q7LK1o{bXL~>4IadB!f zBzV>VjaiS+s1`J%#IPBalA4gB1ED*-xG)QotshQ_&__&tA9vBl3fLI-;d5#4I zIho0cC7JnodSEw$W1wL2!5|PuvmiGh)|<&u#CGxO#jmF=%bdT=mgSl=_g*PSmy69I z30Iu`A6uO`@#fs7b%FM)<+!R|uzy*+Xt#8#6LYcsIos#w_J02O^Bdm-hFs}N9nnD7 zwGFA#r6v|(7GaW~+O+~c3OaAwTzxoU?u>;g8s9?{^q2i}aJ!zgfOBKbLZ&vi=j+VH zz8|`(B(d|aMbN<&vpL>gX7_K~Xmqe8x%bn)nQrrJf;)E<$+SgPDrvq~FJhNr?JH%M zzLBCa*L$t+?`tkqCoYz3Xcb%8cFFvmyr$#k?fUG=FOTwUmHc9TNM_@Ii_WF~+_N+< zzP|rh`*GaGH-@Xy zJS73l79b$N@YWGTBjqqwNDf1b4Ty2L@)yK7VB9k-X$0juxN#`C4QMhbw_!CIS4Kg0 z=oO&J@H8J90yGY5mO=O%Gt(d&Ck}Kk+&DxQ0vZd-LU>$>nSqdvO~Pg@G&=#!#+sEt qPR5m)U}l5Db`wssv1BcvWmvLRfHx}}FdP}!fKY;ofgy+)!~+16@N`rF literal 0 HcmV?d00001 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx new file mode 100644 index 000000000000..b84b99a6b47e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx @@ -0,0 +1,5 @@ +- "BOOT-INF/layers/one/lib/a.jar" +- "BOOT-INF/layers/one/lib/b.jar" +- "BOOT-INF/layers/one/lib/c.jar" +- "BOOT-INF/layers/two/lib/d.jar" +- "BOOT-INF/layers/two/lib/e.jar" diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/META-INF/MANIFEST.MF b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/META-INF/MANIFEST.MF new file mode 100644 index 000000000000..d95a13c5284e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Start-Class: ${foo.main} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/loader.properties b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/loader.properties new file mode 100644 index 000000000000..32f7d00f2d01 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/placeholders/loader.properties @@ -0,0 +1 @@ +foo.main: demo.FooApplication diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/MANIFEST.MF b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/MANIFEST.MF new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/MANIFEST.MF @@ -0,0 +1 @@ + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/spring/application.xml b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/spring/application.xml new file mode 100644 index 000000000000..cf04aa4fbe43 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/spring/application.xml @@ -0,0 +1,6 @@ + + + + diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/build.gradle new file mode 100644 index 000000000000..7c4095f73b1a --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/build.gradle @@ -0,0 +1,44 @@ +plugins { + id "java" + id "org.springframework.boot.conventions" + id "org.springframework.boot.integration-test" +} + +description = "Spring Boot Loader Integration Tests" + +configurations { + app +} + +dependencies { + app project(path: ":spring-boot-project:spring-boot-dependencies", configuration: "mavenRepository") + app project(path: ":spring-boot-project:spring-boot-tools:spring-boot-gradle-plugin", configuration: "mavenRepository") + app project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter-web", configuration: "mavenRepository") + + intTestImplementation(enforcedPlatform(project(":spring-boot-project:spring-boot-parent"))) + intTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + intTestImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) + intTestImplementation("org.testcontainers:junit-jupiter") + intTestImplementation("org.testcontainers:testcontainers") +} + +task syncMavenRepository(type: Sync) { + from configurations.app + into "${buildDir}/int-test-maven-repository" +} + +task syncAppSource(type: org.springframework.boot.build.SyncAppSource) { + sourceDirectory = file("spring-boot-loader-tests-app") + destinationDirectory = file("${buildDir}/spring-boot-loader-tests-app") +} + +task buildApp(type: GradleBuild) { + dependsOn syncAppSource, syncMavenRepository + dir = "${buildDir}/spring-boot-loader-tests-app" + startParameter.buildCacheEnabled = false + tasks = ["build"] +} + +intTest { + dependsOn buildApp +} \ No newline at end of file diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/build.gradle new file mode 100644 index 000000000000..37596c620634 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/build.gradle @@ -0,0 +1,18 @@ +plugins { + id "java" + id "org.springframework.boot" +} + +apply plugin: "io.spring.dependency-management" + +repositories { + maven { url "file:${rootDir}/../int-test-maven-repository"} + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.webjars:jquery:3.5.0") +} diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/settings.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/settings.gradle new file mode 100644 index 000000000000..06d9554ad0d6 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/settings.gradle @@ -0,0 +1,15 @@ +pluginManagement { + repositories { + maven { url "file:${rootDir}/../int-test-maven-repository"} + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } + } + resolutionStrategy { + eachPlugin { + if (requested.id.id == "org.springframework.boot") { + useModule "org.springframework.boot:spring-boot-gradle-plugin:${requested.version}" + } + } + } +} \ No newline at end of file diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java new file mode 100644 index 000000000000..0c9d429350d8 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2023 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.loaderapp; + +import java.io.File; +import java.net.JarURLConnection; +import java.net.URL; +import java.util.Arrays; + +import jakarta.servlet.ServletContext; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.util.FileCopyUtils; + +@SpringBootApplication +public class LoaderTestApplication { + + @Bean + public CommandLineRunner commandLineRunner(ServletContext servletContext) { + return (args) -> { + File temp = new File(System.getProperty("java.io.tmpdir")); + URL resourceUrl = servletContext.getResource("webjars/jquery/3.5.0/jquery.js"); + JarURLConnection connection = (JarURLConnection) resourceUrl.openConnection(); + String jarName = connection.getJarFile().getName(); + System.out.println(">>>>> jar file " + jarName); + if(jarName.contains(temp.getAbsolutePath())) { + System.out.println(">>>>> jar written to temp"); + } + byte[] resourceContent = FileCopyUtils.copyToByteArray(resourceUrl.openStream()); + URL directUrl = new URL(resourceUrl.toExternalForm()); + byte[] directContent = FileCopyUtils.copyToByteArray(directUrl.openStream()); + String message = (!Arrays.equals(resourceContent, directContent)) ? "NO MATCH" + : directContent.length + " BYTES"; + System.out.println(">>>>> " + message + " from " + resourceUrl); + }; + } + + public static void main(String[] args) { + SpringApplication.run(LoaderTestApplication.class, args).close(); + } + +} diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java new file mode 100644 index 000000000000..a2d6db7c7fde --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2012-2023 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.loader; + +import java.io.File; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.ToStringConsumer; +import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; +import org.testcontainers.images.builder.ImageFromDockerfile; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import org.springframework.boot.system.JavaVersion; +import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable; +import org.springframework.util.Assert; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests loader that supports fat jars. + * + * @author Phillip Webb + * @author Moritz Halbritter + */ +@DisabledIfDockerUnavailable +class LoaderIntegrationTests { + + private final ToStringConsumer output = new ToStringConsumer(); + + @ParameterizedTest + @MethodSource("javaRuntimes") + void readUrlsWithoutWarning(JavaRuntime javaRuntime) { + try (GenericContainer container = createContainer(javaRuntime)) { + container.start(); + System.out.println(this.output.toUtf8String()); + assertThat(this.output.toUtf8String()).contains(">>>>> 287649 BYTES from") + .doesNotContain("WARNING:") + .doesNotContain("illegal") + .doesNotContain("jar written to temp"); + } + } + + private GenericContainer createContainer(JavaRuntime javaRuntime) { + return javaRuntime.getContainer() + .withLogConsumer(this.output) + .withCopyFileToContainer(MountableFile.forHostPath(findApplication().toPath()), "/app.jar") + .withStartupCheckStrategy(new OneShotStartupCheckStrategy().withTimeout(Duration.ofMinutes(5))) + .withCommand("java", "-jar", "app.jar"); + } + + private File findApplication() { + String name = String.format("build/%1$s/build/libs/%1$s.jar", "spring-boot-loader-tests-app"); + File jar = new File(name); + Assert.state(jar.isFile(), () -> "Could not find " + name + ". Have you built it?"); + return jar; + } + + static Stream javaRuntimes() { + List javaRuntimes = new ArrayList<>(); + javaRuntimes.add(JavaRuntime.openJdk(JavaVersion.SEVENTEEN)); + javaRuntimes.add(JavaRuntime.openJdk(JavaVersion.TWENTY)); + javaRuntimes.add(JavaRuntime.oracleJdk17()); + javaRuntimes.add(JavaRuntime.openJdkEarlyAccess(JavaVersion.TWENTY_ONE)); + return javaRuntimes.stream().filter(JavaRuntime::isCompatible); + } + + static final class JavaRuntime { + + private final String name; + + private final JavaVersion version; + + private final Supplier> container; + + private JavaRuntime(String name, JavaVersion version, Supplier> container) { + this.name = name; + this.version = version; + this.container = container; + } + + private boolean isCompatible() { + return this.version.isEqualOrNewerThan(JavaVersion.getJavaVersion()); + } + + GenericContainer getContainer() { + return this.container.get(); + } + + @Override + public String toString() { + return this.name; + } + + static JavaRuntime openJdkEarlyAccess(JavaVersion version) { + String imageVersion = version.toString(); + DockerImageName image = DockerImageName.parse("openjdk:%s-ea-jdk".formatted(imageVersion)); + return new JavaRuntime("OpenJDK Early Access " + imageVersion, version, + () -> new GenericContainer<>(image)); + } + + static JavaRuntime openJdk(JavaVersion version) { + String imageVersion = version.toString(); + DockerImageName image = DockerImageName.parse("bellsoft/liberica-openjdk-debian:" + imageVersion); + return new JavaRuntime("OpenJDK " + imageVersion, version, () -> new GenericContainer<>(image)); + } + + static JavaRuntime oracleJdk17() { + String arch = System.getProperty("os.arch"); + String dockerFile = ("aarch64".equals(arch)) ? "Dockerfile-aarch64" : "Dockerfile"; + ImageFromDockerfile image = new ImageFromDockerfile("spring-boot-loader/oracle-jdk-17") + .withFileFromFile("Dockerfile", new File("src/intTest/resources/conf/oracle-jdk-17/" + dockerFile)); + return new JavaRuntime("Oracle JDK 17", JavaVersion.SEVENTEEN, () -> new GenericContainer<>(image)); + } + + } + +} diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile new file mode 100644 index 000000000000..2a50709dc5a5 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile @@ -0,0 +1,8 @@ +FROM ubuntu:jammy-20230624 +RUN apt-get update && \ + apt-get install -y software-properties-common curl && \ + mkdir -p /opt/oraclejdk && \ + cd /opt/oraclejdk && \ + curl -L https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.tar.gz | tar zx --strip-components=1 +ENV JAVA_HOME /opt/oraclejdk +ENV PATH $JAVA_HOME/bin:$PATH diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile-aarch64 b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile-aarch64 new file mode 100644 index 000000000000..3f8614c7a219 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile-aarch64 @@ -0,0 +1,8 @@ +FROM ubuntu:jammy-20230624 +RUN apt-get update && \ + apt-get install -y software-properties-common curl && \ + mkdir -p /opt/oraclejdk && \ + cd /opt/oraclejdk && \ + curl -L https://download.oracle.com/java/17/archive/jdk-17.0.8_linux-aarch64_bin.tar.gz | tar zx --strip-components=1 +ENV JAVA_HOME /opt/oraclejdk +ENV PATH $JAVA_HOME/bin:$PATH diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/README.adoc b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/README.adoc new file mode 100644 index 000000000000..28704af225f5 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/README.adoc @@ -0,0 +1,5 @@ +This folder contains a Dockerfile that will create an Oracle JDK instance for use in integration tests. +The resulting Docker image should not be published. + +Oracle JDK is subject to the https://www.oracle.com/downloads/licenses/no-fee-license.html["Oracle No-Fee Terms and Conditions" License (NFTC)] license. +We are specifically using the unmodified JDK for the purposes of developing and testing. diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/logback.xml b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/logback.xml new file mode 100644 index 000000000000..b8a41480d7d6 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/logback.xml @@ -0,0 +1,4 @@ + + + + From 55b5610dd9d5929b55c60442b92a2cc0f8fb83b2 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 18 Sep 2023 23:12:19 -0700 Subject: [PATCH 0523/1215] Add Maven and Gradle option for the loader implementation to use Add properties to the Maven and Gradle plugins so that users can switch between the two loader modules. See gh-37669 --- .../gradle/tasks/bundling/BootArchive.java | 11 ++++ .../tasks/bundling/BootArchiveSupport.java | 13 +++-- .../boot/gradle/tasks/bundling/BootJar.java | 8 ++- .../boot/gradle/tasks/bundling/BootWar.java | 8 ++- .../tasks/bundling/BootZipCopyAction.java | 10 +++- .../tasks/bundling/LoaderZipEntries.java | 9 ++- .../AbstractBootArchiveIntegrationTests.java | 10 ++++ .../bundling/AbstractBootArchiveTests.java | 12 ++++ ...otJarIntegrationTests-classicLoader.gradle | 9 +++ ...otWarIntegrationTests-classicLoader.gradle | 9 +++ .../spring-boot-loader-tools/build.gradle | 24 +++++++- .../boot/loader/tools/AbstractJarWriter.java | 14 ++--- .../boot/loader/tools/Layouts.java | 2 +- .../loader/tools/LoaderClassesWriter.java | 10 +++- .../loader/tools/LoaderImplementation.java | 51 ++++++++++++++++ .../boot/loader/tools/Packager.java | 12 +++- .../boot/maven/JarIntegrationTests.java | 20 +++++++ .../projects/jar-with-classic-loader/pom.xml | 58 +++++++++++++++++++ .../main/java/org/test/SampleApplication.java | 24 ++++++++ .../boot/maven/AbstractPackagerMojo.java | 11 ++++ .../boot/maven/BuildImageMojo.java | 13 +++++ .../boot/maven/RepackageMojo.java | 13 +++++ .../build.gradle | 4 ++ 23 files changed, 330 insertions(+), 25 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-classicLoader.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-classicLoader.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderImplementation.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/pom.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/src/main/java/org/test/SampleApplication.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchive.java index 1f3875a45aab..2da97a470c8d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchive.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchive.java @@ -33,6 +33,8 @@ import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.Optional; +import org.springframework.boot.loader.tools.LoaderImplementation; + /** * A Spring Boot "fat" archive task. * @@ -133,4 +135,13 @@ public interface BootArchive extends Task { */ void resolvedArtifacts(Provider> resolvedArtifacts); + /** + * The loader implementation that should be used with the archive. + * @return the loader implementation + * @since 3.2.0 + */ + @Input + @Optional + Property getLoaderImplementation(); + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java index 921e9f3d4856..22b086248ac7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java @@ -42,6 +42,8 @@ import org.gradle.api.tasks.bundling.Jar; import org.gradle.api.tasks.util.PatternSet; +import org.springframework.boot.loader.tools.LoaderImplementation; + /** * Support class for implementations of {@link BootArchive}. * @@ -116,12 +118,13 @@ private String determineSpringBootVersion() { return (version != null) ? version : "unknown"; } - CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies) { - return createCopyAction(jar, resolvedDependencies, null, null); + CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies, + LoaderImplementation loaderImplementation) { + return createCopyAction(jar, resolvedDependencies, loaderImplementation, null, null); } - CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies, LayerResolver layerResolver, - String layerToolsLocation) { + CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies, + LoaderImplementation loaderImplementation, LayerResolver layerResolver, String layerToolsLocation) { File output = jar.getArchiveFile().get().getAsFile(); Manifest manifest = jar.getManifest(); boolean preserveFileTimestamps = jar.isPreserveFileTimestamps(); @@ -136,7 +139,7 @@ CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies, String encoding = jar.getMetadataCharset(); CopyAction action = new BootZipCopyAction(output, manifest, preserveFileTimestamps, dirMode, fileMode, includeDefaultLoader, layerToolsLocation, requiresUnpack, exclusions, launchScript, librarySpec, - compressionResolver, encoding, resolvedDependencies, layerResolver); + compressionResolver, encoding, resolvedDependencies, layerResolver, loaderImplementation); return jar.isReproducibleFileOrder() ? new ReproducibleOrderingCopyAction(action) : action; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java index 5cf51bb85075..c76a95f1d6cc 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java @@ -37,6 +37,8 @@ import org.gradle.api.tasks.bundling.Jar; import org.gradle.work.DisableCachingByDefault; +import org.springframework.boot.loader.tools.LoaderImplementation; + /** * A custom {@link Jar} task that produces a Spring Boot executable jar. * @@ -141,12 +143,14 @@ private boolean isLayeredDisabled() { @Override protected CopyAction createCopyAction() { + LoaderImplementation loaderImplementation = getLoaderImplementation().getOrElse(LoaderImplementation.DEFAULT); if (!isLayeredDisabled()) { LayerResolver layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary); String layerToolsLocation = this.layered.getIncludeLayerTools().get() ? LIB_DIRECTORY : null; - return this.support.createCopyAction(this, this.resolvedDependencies, layerResolver, layerToolsLocation); + return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, layerResolver, + layerToolsLocation); } - return this.support.createCopyAction(this, this.resolvedDependencies); + return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation); } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java index d697f00a1e54..d3aa0eab860c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java @@ -37,6 +37,8 @@ import org.gradle.api.tasks.bundling.War; import org.gradle.work.DisableCachingByDefault; +import org.springframework.boot.loader.tools.LoaderImplementation; + /** * A custom {@link War} task that produces a Spring Boot executable war. * @@ -115,12 +117,14 @@ private boolean isLayeredDisabled() { @Override protected CopyAction createCopyAction() { + LoaderImplementation loaderImplementation = getLoaderImplementation().getOrElse(LoaderImplementation.DEFAULT); if (!isLayeredDisabled()) { LayerResolver layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary); String layerToolsLocation = this.layered.getIncludeLayerTools().get() ? LIB_DIRECTORY : null; - return this.support.createCopyAction(this, this.resolvedDependencies, layerResolver, layerToolsLocation); + return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, layerResolver, + layerToolsLocation); } - return this.support.createCopyAction(this, this.resolvedDependencies); + return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation); } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java index 8e3a57e4357f..1f35d482fb14 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java @@ -57,6 +57,7 @@ import org.springframework.boot.loader.tools.Layer; import org.springframework.boot.loader.tools.LayersIndex; import org.springframework.boot.loader.tools.LibraryCoordinates; +import org.springframework.boot.loader.tools.LoaderImplementation; import org.springframework.boot.loader.tools.NativeImageArgFile; import org.springframework.boot.loader.tools.ReachabilityMetadataProperties; import org.springframework.util.Assert; @@ -111,11 +112,14 @@ class BootZipCopyAction implements CopyAction { private final LayerResolver layerResolver; + private final LoaderImplementation loaderImplementation; + BootZipCopyAction(File output, Manifest manifest, boolean preserveFileTimestamps, Integer dirMode, Integer fileMode, boolean includeDefaultLoader, String layerToolsLocation, Spec requiresUnpack, Spec exclusions, LaunchScriptConfiguration launchScript, Spec librarySpec, Function compressionResolver, String encoding, - ResolvedDependencies resolvedDependencies, LayerResolver layerResolver) { + ResolvedDependencies resolvedDependencies, LayerResolver layerResolver, + LoaderImplementation loaderImplementation) { this.output = output; this.manifest = manifest; this.preserveFileTimestamps = preserveFileTimestamps; @@ -131,6 +135,7 @@ class BootZipCopyAction implements CopyAction { this.encoding = encoding; this.resolvedDependencies = resolvedDependencies; this.layerResolver = layerResolver; + this.loaderImplementation = loaderImplementation; } @Override @@ -310,7 +315,8 @@ private void writeLoaderEntriesIfNecessary(FileCopyDetails details) throws IOExc // Always write loader entries after META-INF directory (see gh-16698) return; } - LoaderZipEntries loaderEntries = new LoaderZipEntries(getTime(), getDirMode(), getFileMode()); + LoaderZipEntries loaderEntries = new LoaderZipEntries(getTime(), getDirMode(), getFileMode(), + BootZipCopyAction.this.loaderImplementation); this.writtenLoaderEntries = loaderEntries.writeTo(this.out); if (BootZipCopyAction.this.layerResolver != null) { for (String name : this.writtenLoaderEntries.getFiles()) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java index 7606a3d66a2b..8a7851f07c0c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java @@ -28,6 +28,7 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; import org.gradle.api.file.FileTreeElement; +import org.springframework.boot.loader.tools.LoaderImplementation; import org.springframework.util.StreamUtils; /** @@ -39,22 +40,26 @@ */ class LoaderZipEntries { + private final LoaderImplementation loaderImplementation; + private final Long entryTime; private final int dirMode; private final int fileMode; - LoaderZipEntries(Long entryTime, int dirMode, int fileMode) { + LoaderZipEntries(Long entryTime, int dirMode, int fileMode, LoaderImplementation loaderImplementation) { this.entryTime = entryTime; this.dirMode = dirMode; this.fileMode = fileMode; + this.loaderImplementation = (loaderImplementation != null) ? loaderImplementation + : LoaderImplementation.DEFAULT; } WrittenEntries writeTo(ZipArchiveOutputStream out) throws IOException { WrittenEntries written = new WrittenEntries(); try (ZipInputStream loaderJar = new ZipInputStream( - getClass().getResourceAsStream("/META-INF/loader/spring-boot-loader-classic.jar"))) { + getClass().getResourceAsStream("/" + this.loaderImplementation.getJarResourceName()))) { java.util.zip.ZipEntry entry = loaderJar.getNextEntry(); while (entry != null) { if (entry.isDirectory() && !entry.getName().equals("META-INF/")) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java index a01d3bd4157e..45c10a127db4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java @@ -106,6 +106,16 @@ void reproducibleArchive() throws IOException, InterruptedException { assertThat(firstHash).isEqualTo(secondHash); } + @TestTemplate + void classicLoader() throws IOException { + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) + .isEqualTo(TaskOutcome.SUCCESS); + File jar = new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0]; + try (JarFile jarFile = new JarFile(jar)) { + assertThat(jarFile.getEntry("org/springframework/boot/loader/LaunchedURLClassLoader.class")).isNotNull(); + } + } + @TestTemplate void upToDateWhenBuiltTwice() { assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java index 17623f9ed415..3dffc0075201 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java @@ -65,6 +65,7 @@ import org.springframework.boot.gradle.junit.GradleProjectBuilder; import org.springframework.boot.loader.tools.DefaultLaunchScript; import org.springframework.boot.loader.tools.JarModeLibrary; +import org.springframework.boot.loader.tools.LoaderImplementation; import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -279,6 +280,17 @@ void loaderIsWrittenToTheRootOfTheJarWhenUsingThePropertiesLauncher() throws IOE } } + @Test + void loaderIsWrittenToTheRootOfTheJarWhenUsingClassicLoader() throws IOException { + this.task.getMainClass().set("com.example.Main"); + this.task.getLoaderImplementation().set(LoaderImplementation.CLASSIC); + executeTask(); + try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { + assertThat(jarFile.getEntry("org/springframework/boot/loader/LaunchedURLClassLoader.class")).isNotNull(); + assertThat(jarFile.getEntry("org/springframework/boot/loader/")).isNotNull(); + } + } + @Test void unpackCommentIsAddedToEntryIdentifiedByAPattern() throws IOException { this.task.getMainClass().set("com.example.Main"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-classicLoader.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-classicLoader.gradle new file mode 100644 index 000000000000..2e9e26c99cad --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-classicLoader.gradle @@ -0,0 +1,9 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' + loaderImplementation = org.springframework.boot.loader.tools.LoaderImplementation.CLASSIC +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-classicLoader.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-classicLoader.gradle new file mode 100644 index 000000000000..fd14cc1a64af --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-classicLoader.gradle @@ -0,0 +1,9 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{version}' +} + +bootWar { + mainClass = 'com.example.Application' + loaderImplementation = org.springframework.boot.loader.tools.LoaderImplementation.CLASSIC +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle index 80311be0acb7..f7968f659d51 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle @@ -13,6 +13,10 @@ configurations { extendsFrom dependencyManagement transitive = false } + loaderClassic { + extendsFrom dependencyManagement + transitive = false + } jarmode { extendsFrom dependencyManagement transitive = false @@ -36,7 +40,8 @@ dependencies { compileOnly("ch.qos.logback:logback-classic") - loader(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-classic")) + loader(project(":spring-boot-project:spring-boot-tools:spring-boot-loader")) + loaderClassic(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-classic")) jarmode(project(":spring-boot-project:spring-boot-tools:spring-boot-jarmode-layertools")) @@ -57,6 +62,21 @@ task reproducibleLoaderJar(type: Jar) { } reproducibleFileOrder = true preserveFileTimestamps = false + archiveFileName = "spring-boot-loader.jar" + destinationDirectory = file("${generatedResources}/META-INF/loader") +} + +task reproducibleLoaderClassicJar(type: Jar) { + dependsOn configurations.loaderClassic + from { + zipTree(configurations.loaderClassic.incoming.files.singleFile).matching { + exclude "META-INF/LICENSE.txt" + exclude "META-INF/NOTICE.txt" + exclude "META-INF/spring-boot.properties" + } + } + reproducibleFileOrder = true + preserveFileTimestamps = false archiveFileName = "spring-boot-loader-classic.jar" destinationDirectory = file("${generatedResources}/META-INF/loader") } @@ -72,7 +92,7 @@ task layerToolsJar(type: Sync) { sourceSets { main { - output.dir(generatedResources, builtBy: [layerToolsJar, reproducibleLoaderJar]) + output.dir(generatedResources, builtBy: [layerToolsJar, reproducibleLoaderJar, reproducibleLoaderClassicJar]) } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java index 23282dbea729..7e807b1f9d82 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java @@ -51,8 +51,6 @@ */ public abstract class AbstractJarWriter implements LoaderClassesWriter { - private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader-classic.jar"; - private static final int BUFFER_SIZE = 32 * 1024; private static final int UNIX_FILE_MODE = UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM; @@ -199,13 +197,15 @@ private long getNestedLibraryTime(Library library) { return library.getLastModified(); } - /** - * Write the required spring-boot-loader classes to the JAR. - * @throws IOException if the classes cannot be written - */ @Override public void writeLoaderClasses() throws IOException { - writeLoaderClasses(NESTED_LOADER_JAR); + writeLoaderClasses(LoaderImplementation.DEFAULT); + } + + @Override + public void writeLoaderClasses(LoaderImplementation loaderImplementation) throws IOException { + writeLoaderClasses((loaderImplementation != null) ? loaderImplementation.getJarResourceName() + : LoaderImplementation.DEFAULT.getJarResourceName()); } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java index 82c73ef85987..e6f99282a717 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Layouts.java @@ -23,7 +23,7 @@ import java.util.Map; /** - * Common {@link Layout}s. + * Common {@link Layout layouts}. * * @author Phillip Webb * @author Dave Syer diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderClassesWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderClassesWriter.java index 187ff0b90292..864992279d35 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderClassesWriter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderClassesWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2023 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. @@ -34,6 +34,14 @@ public interface LoaderClassesWriter { */ void writeLoaderClasses() throws IOException; + /** + * Write the default required spring-boot-loader classes to the JAR. + * @param loaderImplementation the specific implementation to write + * @throws IOException if the classes cannot be written + * @since 3.2.0 + */ + void writeLoaderClasses(LoaderImplementation loaderImplementation) throws IOException; + /** * Write custom required spring-boot-loader classes to the JAR. * @param loaderJarResourceName the name of the resource containing the loader classes diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderImplementation.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderImplementation.java new file mode 100644 index 000000000000..6414a3cfbbf8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/LoaderImplementation.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2023 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.loader.tools; + +/** + * Supported loader implementations. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public enum LoaderImplementation { + + /** + * The default recommended loader implementation. + */ + DEFAULT("META-INF/loader/spring-boot-loader.jar"), + + /** + * The classic loader implementation as used with Spring Boot 3.1 and earlier. + */ + CLASSIC("META-INF/loader/spring-boot-loader-classic.jar"); + + private final String jarResourceName; + + LoaderImplementation(String jarResourceName) { + this.jarResourceName = jarResourceName; + } + + /** + * Return the name of the nested resource that can be loaded from the tools jar. + * @return the jar resource name + */ + public String getJarResourceName() { + return this.jarResourceName; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java index 815e0ee4a935..af4dff233012 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java @@ -88,6 +88,8 @@ public abstract class Packager { private Layout layout; + private LoaderImplementation loaderImplementation; + private LayoutFactory layoutFactory; private Layers layers; @@ -135,6 +137,14 @@ public void setLayout(Layout layout) { this.layout = layout; } + /** + * Sets the loader implementation to use. + * @param loaderImplementation the loaderImplementation to set + */ + public void setLoaderImplementation(LoaderImplementation loaderImplementation) { + this.loaderImplementation = loaderImplementation; + } + /** * Sets the layout factory for the jar. The factory can be used when no specific * layout is specified. @@ -215,7 +225,7 @@ private void writeLoaderClasses(AbstractJarWriter writer) throws IOException { customLoaderLayout.writeLoadedClasses(writer); } else if (layout.isExecutable()) { - writer.writeLoaderClasses(); + writer.writeLoaderClasses(this.loaderImplementation); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java index 1d37949cfaa9..c903da23a2cd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java @@ -75,6 +75,26 @@ void whenJarIsRepackagedInPlaceOnlyRepackagedJarIsInstalled(MavenBuild mavenBuil }); } + @TestTemplate + void whenJarWithClassicLoaderIsRepackagedInPlaceOnlyRepackagedJarIsInstalled(MavenBuild mavenBuild) { + mavenBuild.project("jar-with-classic-loader").goals("install").execute((project) -> { + File original = new File(project, "target/jar-with-classic-loader-0.0.1.BUILD-SNAPSHOT.jar.original"); + assertThat(original).isFile(); + File repackaged = new File(project, "target/jar-with-classic-loader-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(launchScript(repackaged)).isEmpty(); + assertThat(jar(repackaged)).manifest((manifest) -> { + manifest.hasMainClass("org.springframework.boot.loader.launch.JarLauncher"); + manifest.hasStartClass("some.random.Main"); + manifest.hasAttribute("Not-Used", "Foo"); + }).hasEntryWithName("org/springframework/boot/loader/launch/JarLauncher.class"); + assertThat(buildLog(project)) + .contains("Replacing main artifact " + repackaged + " with repackaged archive,") + .contains("The original artifact has been renamed to " + original) + .contains("Installing " + repackaged + " to") + .doesNotContain("Installing " + original + " to"); + }); + } + @TestTemplate void whenAttachIsDisabledOnlyTheOriginalJarIsInstalled(MavenBuild mavenBuild) { mavenBuild.project("jar-attach-disabled").goals("install").execute((project) -> { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/pom.xml new file mode 100644 index 000000000000..64d9d04f9949 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-with-classic-loader + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + + + + org.apache.maven.plugins + maven-jar-plugin + @maven-jar-plugin.version@ + + + + some.random.Main + + + Foo + + + CLASSIC + + + + + + + org.springframework + spring-context + @spring-framework.version@ + + + jakarta.servlet + jakarta.servlet-api + @jakarta-servlet.version@ + provided + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..5e51546d4e0d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2023 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.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java index b6dfdc04bb08..28d55d213a16 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java @@ -47,6 +47,7 @@ import org.springframework.boot.loader.tools.Layouts.None; import org.springframework.boot.loader.tools.Layouts.War; import org.springframework.boot.loader.tools.Libraries; +import org.springframework.boot.loader.tools.LoaderImplementation; import org.springframework.boot.loader.tools.Packager; import org.springframework.boot.loader.tools.layer.CustomLayers; @@ -128,6 +129,15 @@ protected LayoutType getLayout() { return null; } + /** + * Return the loader implementation that should be used. + * @return the loader implementation or {@code null} + * @since 3.2.0 + */ + protected LoaderImplementation getLoaderImplementation() { + return null; + } + /** * Return the layout factory that will be used to determine the {@link LayoutType} if * no explicit layout is set. @@ -145,6 +155,7 @@ protected LayoutFactory getLayoutFactory() { */ protected

    P getConfiguredPackager(Supplier

    supplier) { P packager = supplier.get(); + packager.setLoaderImplementation(getLoaderImplementation()); packager.setLayoutFactory(getLayoutFactory()); packager.addMainClassTimeoutWarningListener(new LoggingMainClassTimeoutWarningListener(this::getLog)); packager.setMainClass(this.mainClass); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java index 84589c01891f..79b62bf53030 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java @@ -48,6 +48,7 @@ import org.springframework.boot.loader.tools.ImagePackager; import org.springframework.boot.loader.tools.LayoutFactory; import org.springframework.boot.loader.tools.Libraries; +import org.springframework.boot.loader.tools.LoaderImplementation; import org.springframework.util.StringUtils; /** @@ -187,6 +188,13 @@ public abstract class BuildImageMojo extends AbstractPackagerMojo { @Parameter private LayoutType layout; + /** + * The loader implementation that should be used. + * @since 3.2.0 + */ + @Parameter + private LoaderImplementation loaderImplementation; + /** * The layout factory that will be used to create the executable archive if no * explicit layout is set. Alternative layouts implementations can be provided by 3rd @@ -206,6 +214,11 @@ protected LayoutType getLayout() { return this.layout; } + @Override + protected LoaderImplementation getLoaderImplementation() { + return this.loaderImplementation; + } + /** * Return the layout factory that will be used to determine the * {@link AbstractPackagerMojo.LayoutType} if no explicit layout is set. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java index 5ca2ecac5a90..13a16c2a144a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RepackageMojo.java @@ -36,6 +36,7 @@ import org.springframework.boot.loader.tools.LaunchScript; import org.springframework.boot.loader.tools.LayoutFactory; import org.springframework.boot.loader.tools.Libraries; +import org.springframework.boot.loader.tools.LoaderImplementation; import org.springframework.boot.loader.tools.Repackager; /** @@ -161,6 +162,13 @@ public class RepackageMojo extends AbstractPackagerMojo { @Parameter(property = "spring-boot.repackage.layout") private LayoutType layout; + /** + * The loader implementation that should be used. + * @since 3.2.0 + */ + @Parameter + private LoaderImplementation loaderImplementation; + /** * The layout factory that will be used to create the executable archive if no * explicit layout is set. Alternative layouts implementations can be provided by 3rd @@ -180,6 +188,11 @@ protected LayoutType getLayout() { return this.layout; } + @Override + protected LoaderImplementation getLoaderImplementation() { + return this.loaderImplementation; + } + /** * Return the layout factory that will be used to determine the * {@link AbstractPackagerMojo.LayoutType} if no explicit layout is set. diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/build.gradle index 37596c620634..16f7a57ebe55 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/build.gradle +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-classic-tests/spring-boot-loader-classic-tests-app/build.gradle @@ -16,3 +16,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.webjars:jquery:3.5.0") } + +bootJar { + loaderImplementation = org.springframework.boot.loader.tools.LoaderImplementation.CLASSIC +} \ No newline at end of file From 75ddb9fa47b4dcd7f47dc011606ef2738c6c1af4 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 3 Oct 2023 17:26:29 -0700 Subject: [PATCH 0524/1215] Fix test failure caused by PropertiesLoader class reference See gh-37667 --- .../java/org/springframework/boot/maven/BuildImageTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java index 12ca33cdb26a..b4d0fffeb5be 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java @@ -307,7 +307,7 @@ void whenBuildImageIsInvokedWithZipPackaging(MavenBuild mavenBuild) { assertThat(jar).isFile(); assertThat(buildLog(project)).contains("Building image") .contains("docker.io/library/build-image-zip-packaging:0.0.1.BUILD-SNAPSHOT") - .contains("Main-Class: org.springframework.boot.loader.PropertiesLauncher") + .contains("Main-Class: org.springframework.boot.loader.launch.PropertiesLauncher") .contains("Successfully built image"); removeImage("build-image-zip-packaging", "0.0.1.BUILD-SNAPSHOT"); }); From 7ad4a9817da4b04c1a743cc52a7585dd30b25ef9 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 18 Sep 2023 12:30:13 -0700 Subject: [PATCH 0525/1215] Rewrite nested jar support code and remove Java 8 support Rewrite nested jar code to better align with the implementations provided in Java 17. This update makes two fundamental changes to the previous implementation: - Resource cleanup is now handled using the `java.lang.ref.Cleaner` - Jar URLs now use the form `jar:nested:/my.jar/!nested.jar!/entry` Unlike the previous `jar:jar:/my,jar!/nested.jar!/entry` URL format, the new format is compatible with Java's default Jar URL handler. Specifically, it now only uses a single `jar:` prefix and it no longer includes multiple `!/` separators. In addition to the changes above, many of the ancillary classes have also been refactored and updated to create cleaner APIs. Closes gh-37668 --- .../executable-jar/jarfile-class.adoc | 12 +- .../asciidoc/executable-jar/launching.adoc | 7 +- .../bundling/AbstractBootArchiveTests.java | 6 +- .../loader/ExecutableArchiveLauncher.java | 207 ----- .../boot/loader/JarLauncher.java | 68 -- .../boot/loader/LaunchedURLClassLoader.java | 366 -------- .../springframework/boot/loader/Launcher.java | 159 ---- .../boot/loader/MainMethodRunner.java | 52 -- .../boot/loader/PropertiesLauncher.java | 726 ---------------- .../boot/loader/WarLauncher.java | 62 -- .../boot/loader/archive/Archive.java | 115 --- .../boot/loader/archive/ExplodedArchive.java | 342 -------- .../boot/loader/archive/JarFileArchive.java | 310 ------- .../boot/loader/data/RandomAccessData.java | 74 -- .../loader/data/RandomAccessDataFile.java | 262 ------ .../boot/loader/jar/AbstractJarFile.java | 78 -- .../boot/loader/jar/AsciiBytes.java | 255 ------ .../loader/jar/CentralDirectoryEndRecord.java | 258 ------ .../jar/CentralDirectoryFileHeader.java | 222 ----- .../loader/jar/CentralDirectoryParser.java | 101 --- .../boot/loader/jar/FileHeader.java | 64 -- .../boot/loader/jar/Handler.java | 466 ---------- .../boot/loader/jar/JarEntry.java | 120 --- .../loader/jar/JarEntryCertification.java | 58 -- .../boot/loader/jar/JarFile.java | 475 ---------- .../boot/loader/jar/JarFileEntries.java | 491 ----------- .../boot/loader/jar/JarFileWrapper.java | 126 --- .../boot/loader/jar/JarURLConnection.java | 393 --------- .../boot/loader/jar/ManifestInfo.java | 79 ++ .../boot/loader/jar/MetaInfVersionsInfo.java | 101 +++ .../boot/loader/jar/NestedJarFile.java | 801 +++++++++++++++++ .../loader/jar/NestedJarFileResources.java | 206 +++++ .../boot/loader/jar/SecurityInfo.java | 110 +++ .../boot/loader/jar/StringSequence.java | 157 ---- .../loader/jar/ZipInflaterInputStream.java | 34 +- .../boot/loader/jar/package-info.java | 3 +- .../boot/loader/jarmode/package-info.java | 2 - .../boot/loader/launch/Archive.java | 150 ++++ .../{ => launch}/ClassPathIndexFile.java | 59 +- .../launch/ExecutableArchiveLauncher.java | 136 +++ .../boot/loader/launch/ExplodedArchive.java | 139 +++ .../boot/loader/launch/JarFileArchive.java | 205 +++++ .../boot/loader/launch/JarLauncher.java | 31 +- .../JarModeRunner.java} | 14 +- .../loader/launch/LaunchedClassLoader.java | 189 ++++ .../boot/loader/launch/Launcher.java | 125 +++ .../loader/launch/PropertiesLauncher.java | 574 ++++++++++++- .../loader/launch/SystemPropertyUtils.java | 151 ++++ .../boot/loader/launch/WarLauncher.java | 30 +- .../boot/loader/launch/package-info.java | 5 +- .../boot/loader/log/DebugLogger.java | 152 ++++ .../loader/{data => log}/package-info.java | 6 +- .../boot/loader/net/protocol/Handlers.java | 63 ++ .../net/protocol/jar/Canonicalizer.java | 84 ++ .../boot/loader/net/protocol/jar/Handler.java | 190 ++++ .../net/protocol/jar/JarFileUrlKey.java | 74 ++ .../boot/loader/net/protocol/jar/JarUrl.java | 86 ++ .../net/protocol/jar/JarUrlClassLoader.java | 290 +++++++ .../net/protocol/jar/JarUrlConnection.java | 399 +++++++++ .../jar/LazyDelegatingInputStream.java | 110 +++ .../protocol/jar/Optimizations.java} | 36 +- .../loader/net/protocol/jar/UrlJarEntry.java | 47 + .../loader/net/protocol/jar/UrlJarFile.java | 60 ++ .../net/protocol/jar/UrlJarFileFactory.java | 118 +++ .../loader/net/protocol/jar/UrlJarFiles.java | 217 +++++ .../net/protocol/jar/UrlJarManifest.java | 86 ++ .../net/protocol/jar/UrlNestedJarFile.java | 63 ++ .../loader/net/protocol/jar/package-info.java | 23 + .../loader/net/protocol/nested/Handler.java | 61 ++ .../net/protocol/nested/NestedLocation.java | 98 +++ .../protocol/nested/NestedUrlConnection.java | 155 ++++ .../nested/NestedUrlConnectionResources.java | 128 +++ .../net/protocol/nested/package-info.java | 23 + .../protocol}/package-info.java | 7 +- .../boot/loader/net/util/UrlDecoder.java | 109 +++ .../loader/{ => net}/util/package-info.java | 4 +- .../boot/loader/package-info.java | 26 - .../boot/loader/ref/Cleaner.java | 45 + .../boot/loader/ref/DefaultCleaner.java | 44 + .../boot/loader/ref/package-info.java | 20 + .../boot/loader/util/SystemPropertyUtils.java | 232 ----- .../boot/loader/zip/ByteArrayDataBlock.java | 56 ++ .../CloseableDataBlock.java} | 20 +- .../boot/loader/zip/DataBlock.java | 81 ++ .../boot/loader/zip/DataBlockInputStream.java | 110 +++ .../boot/loader/zip/FileChannelDataBlock.java | 258 ++++++ .../boot/loader/zip/NameOffsetLookups.java | 72 ++ .../boot/loader/zip/VirtualDataBlock.java | 92 ++ .../boot/loader/zip/VirtualZipDataBlock.java | 140 +++ .../Zip64EndOfCentralDirectoryLocator.java | 80 ++ .../zip/Zip64EndOfCentralDirectoryRecord.java | 89 ++ .../ZipCentralDirectoryFileHeaderRecord.java | 211 +++++ .../boot/loader/zip/ZipContent.java | 811 ++++++++++++++++++ .../zip/ZipEndOfCentralDirectoryRecord.java | 160 ++++ .../loader/zip/ZipLocalFileHeaderRecord.java | 124 +++ .../boot/loader/zip/ZipString.java | 320 +++++++ .../boot/loader/zip/package-info.java | 21 + .../loader/LaunchedURLClassLoaderTests.java | 111 --- .../loader/archive/ExplodedArchiveTests.java | 189 ---- .../loader/archive/JarFileArchiveTests.java | 207 ----- .../data/RandomAccessDataFileTests.java | 300 ------- .../boot/loader/jar/AsciiBytesTests.java | 196 ----- .../jar/CentralDirectoryParserTests.java | 139 --- .../boot/loader/jar/HandlerTests.java | 210 ----- .../boot/loader/jar/JarFileTests.java | 736 ---------------- .../boot/loader/jar/JarFileWrapperTests.java | 281 ------ .../loader/jar/JarURLConnectionTests.java | 246 ------ .../loader/jar/JarUrlProtocolHandler.java | 57 -- .../boot/loader/jar/ManifestInfoTests.java | 62 ++ .../loader/jar/MetaInfVersionsInfoTests.java | 84 ++ .../boot/loader/jar/NestedJarFileTests.java | 368 ++++++++ .../boot/loader/jar/SecurityInfoTests.java | 84 ++ .../boot/loader/jar/StringSequenceTests.java | 220 ----- .../boot/loader/jarmode/TestJarMode.java | 0 ...bstractExecutableArchiveLauncherTests.java | 17 +- .../boot/loader/launch/ArchiveTests.java | 111 +++ .../{ => launch}/ClassPathIndexFileTests.java | 16 +- .../loader/launch/ExplodedArchiveTests.java | 159 ++++ .../loader/launch/JarFileArchiveTests.java | 172 ++++ .../loader/{ => launch}/JarLauncherTests.java | 70 +- .../launch/LaunchedClassLoaderTests.java | 49 ++ .../LauncherTests.java} | 71 +- .../{ => launch}/PropertiesLauncherTests.java | 128 +-- .../loader/{ => launch}/WarLauncherTests.java | 62 +- .../net/protocol/jar/CanonicalizerTests.java | 57 ++ .../loader/net/protocol/jar/HandlerTests.java | 203 +++++ .../net/protocol/jar/JarFileUrlKeyTests.java | 88 ++ .../protocol/jar/JarUrlClassLoaderTests.java | 147 ++++ .../protocol/jar/JarUrlConnectionTests.java | 480 +++++++++++ .../loader/net/protocol/jar/JarUrlTests.java | 87 ++ .../jar/LazyDelegatingInputStreamTests.java | 127 +++ .../net/protocol/jar/OptimizationsTests.java | 79 ++ .../net/protocol/jar/UrlJarEntryTests.java | 57 ++ .../protocol/jar/UrlJarFileFactoryTests.java | 114 +++ .../net/protocol/jar/UrlJarFileTests.java | 89 ++ .../net/protocol/jar/UrlJarFilesTests.java | 164 ++++ .../net/protocol/jar/UrlJarManifestTests.java | 89 ++ .../protocol/jar/UrlNestedJarFileTests.java | 87 ++ .../net/protocol/nested/HandlerTests.java | 77 ++ .../protocol/nested/NestedLocationTests.java | 98 +++ .../nested/NestedUrlConnectionTests.java | 151 ++++ .../boot/loader/net/util/UrlDecoderTests.java | 50 ++ .../loader/ref/DefaultCleanerTracking.java} | 18 +- .../TestJar.java} | 35 +- .../loader/util/SystemPropertyUtilsTests.java | 62 -- .../AssertFileChannelDataBlocksClosed.java | 39 + ...tFileChannelDataBlocksClosedExtension.java | 88 ++ .../loader/zip/ByteArrayDataBlockTests.java | 76 ++ .../boot/loader/zip/DataBlockTests.java | 76 ++ .../loader/zip/FileChannelDataBlockTests.java | 238 +++++ .../loader/zip/VirtualDataBlockTests.java | 82 ++ .../loader/zip/VirtualZipDataBlockTests.java | 98 +++ ...ip64EndOfCentralDirectoryLocatorTests.java | 58 ++ ...Zip64EndOfCentralDirectoryRecordTests.java | 76 ++ ...CentralDirectoryFileHeaderRecordTests.java | 213 +++++ .../boot/loader/zip/ZipContentTests.java | 437 ++++++++++ .../ZipEndOfCentralDirectoryRecordTests.java | 110 +++ .../zip/ZipLocalFileHeaderRecordTests.java | 117 +++ .../boot/loader/zip/ZipStringTests.java | 194 +++++ .../{ => launch}/classpath-index-file.idx | 0 .../test/resources/root/META-INF/MANIFEST.MF | 1 - 161 files changed, 13906 insertions(+), 9578 deletions(-) delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java delete mode 100755 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java delete mode 100755 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ManifestInfo.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/MetaInfVersionsInfo.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFileResources.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/SecurityInfo.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/Archive.java rename spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/{ => launch}/ClassPathIndexFile.java (54%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/ExecutableArchiveLauncher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/ExplodedArchive.java create mode 100755 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarFileArchive.java rename spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/{jarmode/JarModeLauncher.java => launch/JarModeRunner.java} (77%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/LaunchedClassLoader.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/Launcher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/SystemPropertyUtils.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/log/DebugLogger.java rename spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/{data => log}/package-info.java (77%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/Handlers.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/Canonicalizer.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/Handler.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarFileUrlKey.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrl.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlClassLoader.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/LazyDelegatingInputStream.java rename spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/{jar/JarEntryFilter.java => net/protocol/jar/Optimizations.java} (54%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarEntry.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFile.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFiles.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarManifest.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlNestedJarFile.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/Handler.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnection.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionResources.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/package-info.java rename spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/{archive => net/protocol}/package-info.java (75%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/util/UrlDecoder.java rename spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/{ => net}/util/package-info.java (87%) delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ref/Cleaner.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ref/DefaultCleaner.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ref/package-info.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ByteArrayDataBlock.java rename spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/{jar/Bytes.java => zip/CloseableDataBlock.java} (65%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/DataBlock.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/DataBlockInputStream.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/FileChannelDataBlock.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/NameOffsetLookups.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualDataBlock.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualZipDataBlock.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/Zip64EndOfCentralDirectoryLocator.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/Zip64EndOfCentralDirectoryRecord.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipCentralDirectoryFileHeaderRecord.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipEndOfCentralDirectoryRecord.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipLocalFileHeaderRecord.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipString.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/package-info.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java delete mode 100755 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java delete mode 100755 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarUrlProtocolHandler.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/ManifestInfoTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/MetaInfVersionsInfoTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/SecurityInfoTests.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java rename spring-boot-project/spring-boot-tools/spring-boot-loader/src/{main => test}/java/org/springframework/boot/loader/jarmode/TestJarMode.java (100%) rename spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/{ => launch}/AbstractExecutableArchiveLauncherTests.java (90%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/ArchiveTests.java rename spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/{ => launch}/ClassPathIndexFileTests.java (82%) create mode 100755 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/ExplodedArchiveTests.java create mode 100755 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/JarFileArchiveTests.java rename spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/{ => launch}/JarLauncherTests.java (69%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/LaunchedClassLoaderTests.java rename spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/{jarmode/LauncherJarModeTests.java => launch/LauncherTests.java} (52%) rename spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/{ => launch}/PropertiesLauncherTests.java (75%) rename spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/{ => launch}/WarLauncherTests.java (64%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/CanonicalizerTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/HandlerTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarFileUrlKeyTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlClassLoaderTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/LazyDelegatingInputStreamTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/OptimizationsTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarEntryTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactoryTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFilesTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarManifestTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlNestedJarFileTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/HandlerTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/util/UrlDecoderTests.java rename spring-boot-project/spring-boot-tools/spring-boot-loader/src/{main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java => test/java/org/springframework/boot/loader/ref/DefaultCleanerTracking.java} (62%) rename spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/{TestJarCreator.java => testsupport/TestJar.java} (83%) delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/AssertFileChannelDataBlocksClosed.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/AssertFileChannelDataBlocksClosedExtension.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ByteArrayDataBlockTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/DataBlockTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/VirtualDataBlockTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/VirtualZipDataBlockTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/Zip64EndOfCentralDirectoryLocatorTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/Zip64EndOfCentralDirectoryRecordTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipCentralDirectoryFileHeaderRecordTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipContentTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipEndOfCentralDirectoryRecordTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipLocalFileHeaderRecordTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipStringTests.java rename spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/{ => launch}/classpath-index-file.idx (100%) delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/MANIFEST.MF diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/jarfile-class.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/jarfile-class.adoc index da7c616fb314..b1db0c87261a 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/jarfile-class.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/jarfile-class.adoc @@ -1,7 +1,7 @@ [[appendix.executable-jar.jarfile-class]] -== Spring Boot's "`JarFile`" Class -The core class used to support loading nested jars is `org.springframework.boot.loader.jar.JarFile`. -It lets you load jar content from a standard jar file or from nested child jar data. +== Spring Boot's "`NestedJarFile`" Class +The core class used to support loading nested jars is `org.springframework.boot.loader.jar.NestedJarFile`. +It lets you load jar content from nested child jar data. When first loaded, the location of each `JarEntry` is mapped to a physical file offset of the outer jar, as shown in the following example: [indent=0] @@ -28,5 +28,7 @@ We do not need to unpack the archive, and we do not need to read all entry data [[appendix.executable-jar.jarfile-class.compatibility]] === Compatibility With the Standard Java "`JarFile`" Spring Boot Loader strives to remain compatible with existing code and libraries. -`org.springframework.boot.loader.jar.JarFile` extends from `java.util.jar.JarFile` and should work as a drop-in replacement. -The `getURL()` method returns a `URL` that opens a connection compatible with `java.net.JarURLConnection` and can be used with Java's `URLClassLoader`. +`org.springframework.boot.loader.jar.NestedJarFile` extends from `java.util.jar.JarFile` and should work as a drop-in replacement. + +Nested JAR URLs of the form `jar:nested:/path/myjar.jar/!BOOT-INF/lib/mylib.jar!/B.class` are supported and open a connection compatible with `java.net.JarURLConnection`. +These can be used with Java's `URLClassLoader`. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/launching.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/launching.adoc index 481145b60a6d..690b85c438d8 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/launching.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/executable-jar/launching.adoc @@ -1,13 +1,14 @@ [[appendix.executable-jar.launching]] == Launching Executable Jars -The `org.springframework.boot.loader.Launcher` class is a special bootstrap class that is used as an executable jar's main entry point. -It is the actual `Main-Class` in your jar file, and it is used to setup an appropriate `URLClassLoader` and ultimately call your `main()` method. +The `org.springframework.boot.loader.launch.Launcher` class is a special bootstrap class that is used as an executable jar's main entry point. +It is the actual `Main-Class` in your jar file, and it is used to setup an appropriate `ClassLoader` and ultimately call your `main()` method. There are three launcher subclasses (`JarLauncher`, `WarLauncher`, and `PropertiesLauncher`). Their purpose is to load resources (`.class` files and so on) from nested jar files or war files in directories (as opposed to those explicitly on the classpath). In the case of `JarLauncher` and `WarLauncher`, the nested paths are fixed. `JarLauncher` looks in `BOOT-INF/lib/`, and `WarLauncher` looks in `WEB-INF/lib/` and `WEB-INF/lib-provided/`. You can add extra jars in those locations if you want more. + The `PropertiesLauncher` looks in `BOOT-INF/lib/` in your application archive by default. You can add additional locations by setting an environment variable called `LOADER_PATH` or `loader.path` in `loader.properties` (which is a comma-separated list of directories, archives, or directories within archives). @@ -30,7 +31,7 @@ For a war file, it would be as follows: [indent=0] ---- - Main-Class: org.springframework.boot.loader.WarLauncher + Main-Class: org.springframework.boot.loader.launch.WarLauncher Start-Class: com.mycompany.project.MyApplication ---- diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java index 3dffc0075201..c5c78eb5a21c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java @@ -256,7 +256,8 @@ void loaderIsWrittenToTheRootOfTheJarAfterManifest() throws IOException { this.task.getMainClass().set("com.example.Main"); executeTask(); try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { - assertThat(jarFile.getEntry("org/springframework/boot/loader/LaunchedURLClassLoader.class")).isNotNull(); + assertThat(jarFile.getEntry("org/springframework/boot/loader/launch/LaunchedClassLoader.class")) + .isNotNull(); assertThat(jarFile.getEntry("org/springframework/boot/loader/")).isNotNull(); } // gh-16698 @@ -275,7 +276,8 @@ void loaderIsWrittenToTheRootOfTheJarWhenUsingThePropertiesLauncher() throws IOE .getAttributes() .put("Main-Class", "org.springframework.boot.loader.launch.PropertiesLauncher"); try (JarFile jarFile = new JarFile(this.task.getArchiveFile().get().getAsFile())) { - assertThat(jarFile.getEntry("org/springframework/boot/loader/LaunchedURLClassLoader.class")).isNotNull(); + assertThat(jarFile.getEntry("org/springframework/boot/loader/launch/LaunchedClassLoader.class")) + .isNotNull(); assertThat(jarFile.getEntry("org/springframework/boot/loader/")).isNotNull(); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java deleted file mode 100644 index d2ceaf61c565..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExecutableArchiveLauncher.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright 2012-2023 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.loader; - -import java.io.IOException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.jar.Attributes; -import java.util.jar.Manifest; - -import org.springframework.boot.loader.archive.Archive; -import org.springframework.boot.loader.archive.ExplodedArchive; - -/** - * Base class for executable archive {@link Launcher}s. - * - * @author Phillip Webb - * @author Andy Wilkinson - * @author Madhura Bhave - * @author Scott Frederick - * @since 1.0.0 - */ -public abstract class ExecutableArchiveLauncher extends Launcher { - - private static final String START_CLASS_ATTRIBUTE = "Start-Class"; - - protected static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index"; - - protected static final String DEFAULT_CLASSPATH_INDEX_FILE_NAME = "classpath.idx"; - - private final Archive archive; - - private final ClassPathIndexFile classPathIndex; - - public ExecutableArchiveLauncher() { - try { - this.archive = createArchive(); - this.classPathIndex = getClassPathIndex(this.archive); - } - catch (Exception ex) { - throw new IllegalStateException(ex); - } - } - - protected ExecutableArchiveLauncher(Archive archive) { - try { - this.archive = archive; - this.classPathIndex = getClassPathIndex(this.archive); - } - catch (Exception ex) { - throw new IllegalStateException(ex); - } - } - - protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException { - // Only needed for exploded archives, regular ones already have a defined order - if (archive instanceof ExplodedArchive) { - String location = getClassPathIndexFileLocation(archive); - return ClassPathIndexFile.loadIfPossible(archive.getUrl(), location); - } - return null; - } - - private String getClassPathIndexFileLocation(Archive archive) throws IOException { - Manifest manifest = archive.getManifest(); - Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null; - String location = (attributes != null) ? attributes.getValue(BOOT_CLASSPATH_INDEX_ATTRIBUTE) : null; - return (location != null) ? location : getArchiveEntryPathPrefix() + DEFAULT_CLASSPATH_INDEX_FILE_NAME; - } - - @Override - protected String getMainClass() throws Exception { - Manifest manifest = this.archive.getManifest(); - String mainClass = null; - if (manifest != null) { - mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE); - } - if (mainClass == null) { - throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this); - } - return mainClass; - } - - @Override - protected ClassLoader createClassLoader(Iterator archives) throws Exception { - List urls = new ArrayList<>(guessClassPathSize()); - while (archives.hasNext()) { - urls.add(archives.next().getUrl()); - } - if (this.classPathIndex != null) { - urls.addAll(this.classPathIndex.getUrls()); - } - return createClassLoader(urls.toArray(new URL[0])); - } - - private int guessClassPathSize() { - if (this.classPathIndex != null) { - return this.classPathIndex.size() + 10; - } - return 50; - } - - @Override - protected Iterator getClassPathArchivesIterator() throws Exception { - Archive.EntryFilter searchFilter = this::isSearchCandidate; - Iterator archives = this.archive.getNestedArchives(searchFilter, - (entry) -> isNestedArchive(entry) && !isEntryIndexed(entry)); - if (isPostProcessingClassPathArchives()) { - archives = applyClassPathArchivePostProcessing(archives); - } - return archives; - } - - private boolean isEntryIndexed(Archive.Entry entry) { - if (this.classPathIndex != null) { - return this.classPathIndex.containsEntry(entry.getName()); - } - return false; - } - - private Iterator applyClassPathArchivePostProcessing(Iterator archives) throws Exception { - List list = new ArrayList<>(); - while (archives.hasNext()) { - list.add(archives.next()); - } - postProcessClassPathArchives(list); - return list.iterator(); - } - - /** - * Determine if the specified entry is a candidate for further searching. - * @param entry the entry to check - * @return {@code true} if the entry is a candidate for further searching - * @since 2.3.0 - */ - protected boolean isSearchCandidate(Archive.Entry entry) { - if (getArchiveEntryPathPrefix() == null) { - return true; - } - return entry.getName().startsWith(getArchiveEntryPathPrefix()); - } - - /** - * Determine if the specified entry is a nested item that should be added to the - * classpath. - * @param entry the entry to check - * @return {@code true} if the entry is a nested item (jar or directory) - */ - protected abstract boolean isNestedArchive(Archive.Entry entry); - - /** - * Return if post-processing needs to be applied to the archives. For back - * compatibility this method returns {@code true}, but subclasses that don't override - * {@link #postProcessClassPathArchives(List)} should provide an implementation that - * returns {@code false}. - * @return if the {@link #postProcessClassPathArchives(List)} method is implemented - * @since 2.3.0 - */ - protected boolean isPostProcessingClassPathArchives() { - return true; - } - - /** - * Called to post-process archive entries before they are used. Implementations can - * add and remove entries. - * @param archives the archives - * @throws Exception if the post-processing fails - * @see #isPostProcessingClassPathArchives() - */ - protected void postProcessClassPathArchives(List archives) throws Exception { - } - - /** - * Return the path prefix for entries in the archive. - * @return the path prefix - */ - protected String getArchiveEntryPathPrefix() { - return null; - } - - @Override - protected boolean isExploded() { - return this.archive.isExploded(); - } - - @Override - protected final Archive getArchive() { - return this.archive; - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java deleted file mode 100644 index 5061573e2460..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2012-2023 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.loader; - -import org.springframework.boot.loader.archive.Archive; -import org.springframework.boot.loader.archive.Archive.EntryFilter; - -/** - * {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are - * included inside a {@code /BOOT-INF/lib} directory and that application classes are - * included inside a {@code /BOOT-INF/classes} directory. - * - * @author Phillip Webb - * @author Andy Wilkinson - * @author Madhura Bhave - * @author Scott Frederick - * @since 1.0.0 - */ -public class JarLauncher extends ExecutableArchiveLauncher { - - static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> { - if (entry.isDirectory()) { - return entry.getName().equals("BOOT-INF/classes/"); - } - return entry.getName().startsWith("BOOT-INF/lib/"); - }; - - public JarLauncher() { - } - - protected JarLauncher(Archive archive) { - super(archive); - } - - @Override - protected boolean isPostProcessingClassPathArchives() { - return false; - } - - @Override - protected boolean isNestedArchive(Archive.Entry entry) { - return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry); - } - - @Override - protected String getArchiveEntryPathPrefix() { - return "BOOT-INF/"; - } - - public static void main(String[] args) throws Exception { - new JarLauncher().launch(args); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java deleted file mode 100644 index 7e3e2fa22392..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java +++ /dev/null @@ -1,366 +0,0 @@ -/* - * Copyright 2012-2023 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.loader; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.JarURLConnection; -import java.net.URL; -import java.net.URLClassLoader; -import java.net.URLConnection; -import java.util.Enumeration; -import java.util.function.Supplier; -import java.util.jar.JarFile; -import java.util.jar.Manifest; - -import org.springframework.boot.loader.archive.Archive; -import org.springframework.boot.loader.jar.Handler; - -/** - * {@link ClassLoader} used by the {@link Launcher}. - * - * @author Phillip Webb - * @author Dave Syer - * @author Andy Wilkinson - * @since 1.0.0 - */ -public class LaunchedURLClassLoader extends URLClassLoader { - - private static final int BUFFER_SIZE = 4096; - - static { - ClassLoader.registerAsParallelCapable(); - } - - private final boolean exploded; - - private final Archive rootArchive; - - private final Object packageLock = new Object(); - - private volatile DefinePackageCallType definePackageCallType; - - /** - * Create a new {@link LaunchedURLClassLoader} instance. - * @param urls the URLs from which to load classes and resources - * @param parent the parent class loader for delegation - */ - public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) { - this(false, urls, parent); - } - - /** - * Create a new {@link LaunchedURLClassLoader} instance. - * @param exploded if the underlying archive is exploded - * @param urls the URLs from which to load classes and resources - * @param parent the parent class loader for delegation - */ - public LaunchedURLClassLoader(boolean exploded, URL[] urls, ClassLoader parent) { - this(exploded, null, urls, parent); - } - - /** - * Create a new {@link LaunchedURLClassLoader} instance. - * @param exploded if the underlying archive is exploded - * @param rootArchive the root archive or {@code null} - * @param urls the URLs from which to load classes and resources - * @param parent the parent class loader for delegation - * @since 2.3.1 - */ - public LaunchedURLClassLoader(boolean exploded, Archive rootArchive, URL[] urls, ClassLoader parent) { - super(urls, parent); - this.exploded = exploded; - this.rootArchive = rootArchive; - } - - @Override - public URL findResource(String name) { - if (this.exploded) { - return super.findResource(name); - } - Handler.setUseFastConnectionExceptions(true); - try { - return super.findResource(name); - } - finally { - Handler.setUseFastConnectionExceptions(false); - } - } - - @Override - public Enumeration findResources(String name) throws IOException { - if (this.exploded) { - return super.findResources(name); - } - Handler.setUseFastConnectionExceptions(true); - try { - return new UseFastConnectionExceptionsEnumeration(super.findResources(name)); - } - finally { - Handler.setUseFastConnectionExceptions(false); - } - } - - @Override - protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { - if (name.startsWith("org.springframework.boot.loader.jarmode.")) { - try { - Class result = loadClassInLaunchedClassLoader(name); - if (resolve) { - resolveClass(result); - } - return result; - } - catch (ClassNotFoundException ex) { - } - } - if (this.exploded) { - return super.loadClass(name, resolve); - } - Handler.setUseFastConnectionExceptions(true); - try { - try { - definePackageIfNecessary(name); - } - catch (IllegalArgumentException ex) { - // Tolerate race condition due to being parallel capable - if (getDefinedPackage(name) == null) { - // This should never happen as the IllegalArgumentException indicates - // that the package has already been defined and, therefore, - // getDefinedPackage(name) should not return null. - throw new AssertionError("Package " + name + " has already been defined but it could not be found"); - } - } - return super.loadClass(name, resolve); - } - finally { - Handler.setUseFastConnectionExceptions(false); - } - } - - private Class loadClassInLaunchedClassLoader(String name) throws ClassNotFoundException { - String internalName = name.replace('.', '/') + ".class"; - InputStream inputStream = getParent().getResourceAsStream(internalName); - if (inputStream == null) { - throw new ClassNotFoundException(name); - } - try { - try { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - byte[] buffer = new byte[BUFFER_SIZE]; - int bytesRead = -1; - while ((bytesRead = inputStream.read(buffer)) != -1) { - outputStream.write(buffer, 0, bytesRead); - } - inputStream.close(); - byte[] bytes = outputStream.toByteArray(); - Class definedClass = defineClass(name, bytes, 0, bytes.length); - definePackageIfNecessary(name); - return definedClass; - } - finally { - inputStream.close(); - } - } - catch (IOException ex) { - throw new ClassNotFoundException("Cannot load resource for class [" + name + "]", ex); - } - } - - /** - * Define a package before a {@code findClass} call is made. This is necessary to - * ensure that the appropriate manifest for nested JARs is associated with the - * package. - * @param className the class name being found - */ - private void definePackageIfNecessary(String className) { - int lastDot = className.lastIndexOf('.'); - if (lastDot >= 0) { - String packageName = className.substring(0, lastDot); - if (getDefinedPackage(packageName) == null) { - try { - definePackage(className, packageName); - } - catch (IllegalArgumentException ex) { - // Tolerate race condition due to being parallel capable - if (getDefinedPackage(packageName) == null) { - // This should never happen as the IllegalArgumentException - // indicates that the package has already been defined and, - // therefore, getDefinedPackage(name) should not have returned - // null. - throw new AssertionError( - "Package " + packageName + " has already been defined but it could not be found"); - } - } - } - } - } - - private void definePackage(String className, String packageName) { - String packageEntryName = packageName.replace('.', '/') + "/"; - String classEntryName = className.replace('.', '/') + ".class"; - for (URL url : getURLs()) { - try { - URLConnection connection = url.openConnection(); - if (connection instanceof JarURLConnection jarURLConnection) { - JarFile jarFile = jarURLConnection.getJarFile(); - if (jarFile.getEntry(classEntryName) != null && jarFile.getEntry(packageEntryName) != null - && jarFile.getManifest() != null) { - definePackage(packageName, jarFile.getManifest(), url); - return; - } - } - } - catch (IOException ex) { - // Ignore - } - } - } - - @Override - protected Package definePackage(String name, Manifest man, URL url) throws IllegalArgumentException { - if (!this.exploded) { - return super.definePackage(name, man, url); - } - synchronized (this.packageLock) { - return doDefinePackage(DefinePackageCallType.MANIFEST, () -> super.definePackage(name, man, url)); - } - } - - @Override - protected Package definePackage(String name, String specTitle, String specVersion, String specVendor, - String implTitle, String implVersion, String implVendor, URL sealBase) throws IllegalArgumentException { - if (!this.exploded) { - return super.definePackage(name, specTitle, specVersion, specVendor, implTitle, implVersion, implVendor, - sealBase); - } - synchronized (this.packageLock) { - if (this.definePackageCallType == null) { - // We're not part of a call chain which means that the URLClassLoader - // is trying to define a package for our exploded JAR. We use the - // manifest version to ensure package attributes are set - Manifest manifest = getManifest(this.rootArchive); - if (manifest != null) { - return definePackage(name, manifest, sealBase); - } - } - return doDefinePackage(DefinePackageCallType.ATTRIBUTES, () -> super.definePackage(name, specTitle, - specVersion, specVendor, implTitle, implVersion, implVendor, sealBase)); - } - } - - private Manifest getManifest(Archive archive) { - try { - return (archive != null) ? archive.getManifest() : null; - } - catch (IOException ex) { - return null; - } - } - - private T doDefinePackage(DefinePackageCallType type, Supplier call) { - DefinePackageCallType existingType = this.definePackageCallType; - try { - this.definePackageCallType = type; - return call.get(); - } - finally { - this.definePackageCallType = existingType; - } - } - - /** - * Clear URL caches. - */ - public void clearCache() { - if (this.exploded) { - return; - } - for (URL url : getURLs()) { - try { - URLConnection connection = url.openConnection(); - if (connection instanceof JarURLConnection) { - clearCache(connection); - } - } - catch (IOException ex) { - // Ignore - } - } - - } - - private void clearCache(URLConnection connection) throws IOException { - Object jarFile = ((JarURLConnection) connection).getJarFile(); - if (jarFile instanceof org.springframework.boot.loader.jar.JarFile) { - ((org.springframework.boot.loader.jar.JarFile) jarFile).clearCache(); - } - } - - private static class UseFastConnectionExceptionsEnumeration implements Enumeration { - - private final Enumeration delegate; - - UseFastConnectionExceptionsEnumeration(Enumeration delegate) { - this.delegate = delegate; - } - - @Override - public boolean hasMoreElements() { - Handler.setUseFastConnectionExceptions(true); - try { - return this.delegate.hasMoreElements(); - } - finally { - Handler.setUseFastConnectionExceptions(false); - } - - } - - @Override - public URL nextElement() { - Handler.setUseFastConnectionExceptions(true); - try { - return this.delegate.nextElement(); - } - finally { - Handler.setUseFastConnectionExceptions(false); - } - } - - } - - /** - * The different types of call made to define a package. We track these for exploded - * jars so that we can detect packages that should have manifest attributes applied. - */ - private enum DefinePackageCallType { - - /** - * A define package call from a resource that has a manifest. - */ - MANIFEST, - - /** - * A define package call with a direct set of attributes. - */ - ATTRIBUTES - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java deleted file mode 100644 index 2f4cac944408..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2012-2023 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.loader; - -import java.io.File; -import java.net.URI; -import java.net.URL; -import java.security.CodeSource; -import java.security.ProtectionDomain; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import org.springframework.boot.loader.archive.Archive; -import org.springframework.boot.loader.archive.ExplodedArchive; -import org.springframework.boot.loader.archive.JarFileArchive; -import org.springframework.boot.loader.jar.JarFile; - -/** - * Base class for launchers that can start an application with a fully configured - * classpath backed by one or more {@link Archive}s. - * - * @author Phillip Webb - * @author Dave Syer - * @since 1.0.0 - */ -public abstract class Launcher { - - private static final String JAR_MODE_LAUNCHER = "org.springframework.boot.loader.jarmode.JarModeLauncher"; - - /** - * Launch the application. This method is the initial entry point that should be - * called by a subclass {@code public static void main(String[] args)} method. - * @param args the incoming arguments - * @throws Exception if the application fails to launch - */ - protected void launch(String[] args) throws Exception { - if (!isExploded()) { - JarFile.registerUrlProtocolHandler(); - } - ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator()); - String jarMode = System.getProperty("jarmode"); - String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass(); - launch(args, launchClass, classLoader); - } - - /** - * Create a classloader for the specified archives. - * @param archives the archives - * @return the classloader - * @throws Exception if the classloader cannot be created - * @since 2.3.0 - */ - protected ClassLoader createClassLoader(Iterator archives) throws Exception { - List urls = new ArrayList<>(50); - while (archives.hasNext()) { - urls.add(archives.next().getUrl()); - } - return createClassLoader(urls.toArray(new URL[0])); - } - - /** - * Create a classloader for the specified URLs. - * @param urls the URLs - * @return the classloader - * @throws Exception if the classloader cannot be created - */ - protected ClassLoader createClassLoader(URL[] urls) throws Exception { - return new LaunchedURLClassLoader(isExploded(), getArchive(), urls, getClass().getClassLoader()); - } - - /** - * Launch the application given the archive file and a fully configured classloader. - * @param args the incoming arguments - * @param launchClass the launch class to run - * @param classLoader the classloader - * @throws Exception if the launch fails - */ - protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception { - Thread.currentThread().setContextClassLoader(classLoader); - createMainMethodRunner(launchClass, args, classLoader).run(); - } - - /** - * Create the {@code MainMethodRunner} used to launch the application. - * @param mainClass the main class - * @param args the incoming arguments - * @param classLoader the classloader - * @return the main method runner - */ - protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) { - return new MainMethodRunner(mainClass, args); - } - - /** - * Returns the main class that should be launched. - * @return the name of the main class - * @throws Exception if the main class cannot be obtained - */ - protected abstract String getMainClass() throws Exception; - - /** - * Returns the archives that will be used to construct the class path. - * @return the class path archives - * @throws Exception if the class path archives cannot be obtained - * @since 2.3.0 - */ - protected abstract Iterator getClassPathArchivesIterator() throws Exception; - - protected final Archive createArchive() throws Exception { - ProtectionDomain protectionDomain = getClass().getProtectionDomain(); - CodeSource codeSource = protectionDomain.getCodeSource(); - URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null; - String path = (location != null) ? location.getSchemeSpecificPart() : null; - if (path == null) { - throw new IllegalStateException("Unable to determine code source archive"); - } - File root = new File(path); - if (!root.exists()) { - throw new IllegalStateException("Unable to determine code source archive from " + root); - } - return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root)); - } - - /** - * Returns if the launcher is running in an exploded mode. If this method returns - * {@code true} then only regular JARs are supported and the additional URL and - * ClassLoader support infrastructure can be optimized. - * @return if the jar is exploded. - * @since 2.3.0 - */ - protected boolean isExploded() { - return false; - } - - /** - * Return the root archive. - * @return the root archive - * @since 2.3.1 - */ - protected Archive getArchive() { - return null; - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java deleted file mode 100644 index 12355a2bef46..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2012-2023 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.loader; - -import java.lang.reflect.Method; - -/** - * Utility class that is used by {@link Launcher}s to call a main method. The class - * containing the main method is loaded using the thread context class loader. - * - * @author Phillip Webb - * @author Andy Wilkinson - * @since 1.0.0 - */ -public class MainMethodRunner { - - private final String mainClassName; - - private final String[] args; - - /** - * Create a new {@link MainMethodRunner} instance. - * @param mainClass the main class - * @param args incoming arguments - */ - public MainMethodRunner(String mainClass, String[] args) { - this.mainClassName = mainClass; - this.args = (args != null) ? args.clone() : null; - } - - public void run() throws Exception { - Class mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader()); - Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); - mainMethod.setAccessible(true); - mainMethod.invoke(null, new Object[] { this.args }); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java deleted file mode 100755 index 3703ac136705..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java +++ /dev/null @@ -1,726 +0,0 @@ -/* - * Copyright 2012-2023 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.loader; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.lang.reflect.Constructor; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLDecoder; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Locale; -import java.util.Properties; -import java.util.Set; -import java.util.jar.Manifest; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.springframework.boot.loader.archive.Archive; -import org.springframework.boot.loader.archive.Archive.Entry; -import org.springframework.boot.loader.archive.Archive.EntryFilter; -import org.springframework.boot.loader.archive.ExplodedArchive; -import org.springframework.boot.loader.archive.JarFileArchive; -import org.springframework.boot.loader.util.SystemPropertyUtils; - -/** - * {@link Launcher} for archives with user-configured classpath and main class through a - * properties file. This model is often more flexible and more amenable to creating - * well-behaved OS-level services than a model based on executable jars. - *

    - * Looks in various places for a properties file to extract loader settings, defaulting to - * {@code loader.properties} either on the current classpath or in the current working - * directory. The name of the properties file can be changed by setting a System property - * {@code loader.config.name} (e.g. {@code -Dloader.config.name=foo} will look for - * {@code foo.properties}. If that file doesn't exist then tries - * {@code loader.config.location} (with allowed prefixes {@code classpath:} and - * {@code file:} or any valid URL). Once that file is located turns it into Properties and - * extracts optional values (which can also be provided overridden as System properties in - * case the file doesn't exist): - *

      - *
    • {@code loader.path}: a comma-separated list of directories (containing file - * resources and/or nested archives in *.jar or *.zip or archives) or archives to append - * to the classpath. {@code BOOT-INF/classes,BOOT-INF/lib} in the application archive are - * always used
    • - *
    • {@code loader.main}: the main method to delegate execution to once the class loader - * is set up. No default, but will fall back to looking for a {@code Start-Class} in a - * {@code MANIFEST.MF}, if there is one in ${loader.home}/META-INF.
    • - *
    - * - * @author Dave Syer - * @author Janne Valkealahti - * @author Andy Wilkinson - * @since 1.0.0 - */ -public class PropertiesLauncher extends Launcher { - - private static final Class[] PARENT_ONLY_PARAMS = new Class[] { ClassLoader.class }; - - private static final Class[] URLS_AND_PARENT_PARAMS = new Class[] { URL[].class, ClassLoader.class }; - - private static final Class[] NO_PARAMS = new Class[] {}; - - private static final URL[] NO_URLS = new URL[0]; - - private static final String DEBUG = "loader.debug"; - - /** - * Properties key for main class. As a manifest entry can also be specified as - * {@code Start-Class}. - */ - public static final String MAIN = "loader.main"; - - /** - * Properties key for classpath entries (directories possibly containing jars or - * jars). Multiple entries can be specified using a comma-separated list. {@code - * BOOT-INF/classes,BOOT-INF/lib} in the application archive are always used. - */ - public static final String PATH = "loader.path"; - - /** - * Properties key for home directory. This is the location of external configuration - * if not on classpath, and also the base path for any relative paths in the - * {@link #PATH loader path}. Defaults to current working directory ( - * ${user.dir}). - */ - public static final String HOME = "loader.home"; - - /** - * Properties key for default command line arguments. These arguments (if present) are - * prepended to the main method arguments before launching. - */ - public static final String ARGS = "loader.args"; - - /** - * Properties key for name of external configuration file (excluding suffix). Defaults - * to "application". Ignored if {@link #CONFIG_LOCATION loader config location} is - * provided instead. - */ - public static final String CONFIG_NAME = "loader.config.name"; - - /** - * Properties key for config file location (including optional classpath:, file: or - * URL prefix). - */ - public static final String CONFIG_LOCATION = "loader.config.location"; - - /** - * Properties key for boolean flag (default false) which, if set, will cause the - * external configuration properties to be copied to System properties (assuming that - * is allowed by Java security). - */ - public static final String SET_SYSTEM_PROPERTIES = "loader.system"; - - private static final Pattern WORD_SEPARATOR = Pattern.compile("\\W+"); - - private static final String NESTED_ARCHIVE_SEPARATOR = "!" + File.separator; - - private final File home; - - private List paths = new ArrayList<>(); - - private final Properties properties = new Properties(); - - private final Archive parent; - - private volatile ClassPathArchives classPathArchives; - - public PropertiesLauncher() { - try { - this.home = getHomeDirectory(); - initializeProperties(); - initializePaths(); - this.parent = createArchive(); - } - catch (Exception ex) { - throw new IllegalStateException(ex); - } - } - - protected File getHomeDirectory() { - try { - return new File(getPropertyWithDefault(HOME, "${user.dir}")); - } - catch (Exception ex) { - throw new IllegalStateException(ex); - } - } - - private void initializeProperties() throws Exception { - List configs = new ArrayList<>(); - if (getProperty(CONFIG_LOCATION) != null) { - configs.add(getProperty(CONFIG_LOCATION)); - } - else { - String[] names = getPropertyWithDefault(CONFIG_NAME, "loader").split(","); - for (String name : names) { - configs.add("file:" + getHomeDirectory() + "/" + name + ".properties"); - configs.add("classpath:" + name + ".properties"); - configs.add("classpath:BOOT-INF/classes/" + name + ".properties"); - } - } - for (String config : configs) { - try (InputStream resource = getResource(config)) { - if (resource != null) { - debug("Found: " + config); - loadResource(resource); - // Load the first one we find - return; - } - else { - debug("Not found: " + config); - } - } - } - } - - private void loadResource(InputStream resource) throws Exception { - this.properties.load(resource); - for (Object key : Collections.list(this.properties.propertyNames())) { - String text = this.properties.getProperty((String) key); - String value = SystemPropertyUtils.resolvePlaceholders(this.properties, text); - if (value != null) { - this.properties.put(key, value); - } - } - if ("true".equals(getProperty(SET_SYSTEM_PROPERTIES))) { - debug("Adding resolved properties to System properties"); - for (Object key : Collections.list(this.properties.propertyNames())) { - String value = this.properties.getProperty((String) key); - System.setProperty((String) key, value); - } - } - } - - private InputStream getResource(String config) throws Exception { - if (config.startsWith("classpath:")) { - return getClasspathResource(config.substring("classpath:".length())); - } - config = handleUrl(config); - if (isUrl(config)) { - return getURLResource(config); - } - return getFileResource(config); - } - - private String handleUrl(String path) throws UnsupportedEncodingException { - if (path.startsWith("jar:file:") || path.startsWith("file:")) { - path = URLDecoder.decode(path, "UTF-8"); - if (path.startsWith("file:")) { - path = path.substring("file:".length()); - if (path.startsWith("//")) { - path = path.substring(2); - } - } - } - return path; - } - - private boolean isUrl(String config) { - return config.contains("://"); - } - - private InputStream getClasspathResource(String config) { - while (config.startsWith("/")) { - config = config.substring(1); - } - config = "/" + config; - debug("Trying classpath: " + config); - return getClass().getResourceAsStream(config); - } - - private InputStream getFileResource(String config) throws Exception { - File file = new File(config); - debug("Trying file: " + config); - if (file.canRead()) { - return new FileInputStream(file); - } - return null; - } - - private InputStream getURLResource(String config) throws Exception { - URL url = new URL(config); - if (exists(url)) { - URLConnection con = url.openConnection(); - try { - return con.getInputStream(); - } - catch (IOException ex) { - // Close the HTTP connection (if applicable). - if (con instanceof HttpURLConnection httpURLConnection) { - httpURLConnection.disconnect(); - } - throw ex; - } - } - return null; - } - - private boolean exists(URL url) throws IOException { - // Try a URL connection content-length header... - URLConnection connection = url.openConnection(); - try { - connection.setUseCaches(connection.getClass().getSimpleName().startsWith("JNLP")); - if (connection instanceof HttpURLConnection httpConnection) { - httpConnection.setRequestMethod("HEAD"); - int responseCode = httpConnection.getResponseCode(); - if (responseCode == HttpURLConnection.HTTP_OK) { - return true; - } - else if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) { - return false; - } - } - return (connection.getContentLength() >= 0); - } - finally { - if (connection instanceof HttpURLConnection httpURLConnection) { - httpURLConnection.disconnect(); - } - } - } - - private void initializePaths() throws Exception { - String path = getProperty(PATH); - if (path != null) { - this.paths = parsePathsProperty(path); - } - debug("Nested archive paths: " + this.paths); - } - - private List parsePathsProperty(String commaSeparatedPaths) { - List paths = new ArrayList<>(); - for (String path : commaSeparatedPaths.split(",")) { - path = cleanupPath(path); - // "" means the user wants root of archive but not current directory - path = (path == null || path.isEmpty()) ? "/" : path; - paths.add(path); - } - if (paths.isEmpty()) { - paths.add("lib"); - } - return paths; - } - - protected String[] getArgs(String... args) throws Exception { - String loaderArgs = getProperty(ARGS); - if (loaderArgs != null) { - String[] defaultArgs = loaderArgs.split("\\s+"); - String[] additionalArgs = args; - args = new String[defaultArgs.length + additionalArgs.length]; - System.arraycopy(defaultArgs, 0, args, 0, defaultArgs.length); - System.arraycopy(additionalArgs, 0, args, defaultArgs.length, additionalArgs.length); - } - return args; - } - - @Override - protected String getMainClass() throws Exception { - String mainClass = getProperty(MAIN, "Start-Class"); - if (mainClass == null) { - throw new IllegalStateException("No '" + MAIN + "' or 'Start-Class' specified"); - } - return mainClass; - } - - @Override - protected ClassLoader createClassLoader(Iterator archives) throws Exception { - String customLoaderClassName = getProperty("loader.classLoader"); - if (customLoaderClassName == null) { - return super.createClassLoader(archives); - } - Set urls = new LinkedHashSet<>(); - while (archives.hasNext()) { - urls.add(archives.next().getUrl()); - } - ClassLoader loader = new LaunchedURLClassLoader(urls.toArray(NO_URLS), getClass().getClassLoader()); - debug("Classpath for custom loader: " + urls); - loader = wrapWithCustomClassLoader(loader, customLoaderClassName); - debug("Using custom class loader: " + customLoaderClassName); - return loader; - } - - @SuppressWarnings("unchecked") - private ClassLoader wrapWithCustomClassLoader(ClassLoader parent, String className) throws Exception { - Class type = (Class) Class.forName(className, true, parent); - ClassLoader classLoader = newClassLoader(type, PARENT_ONLY_PARAMS, parent); - if (classLoader == null) { - classLoader = newClassLoader(type, URLS_AND_PARENT_PARAMS, NO_URLS, parent); - } - if (classLoader == null) { - classLoader = newClassLoader(type, NO_PARAMS); - } - if (classLoader == null) { - throw new IllegalArgumentException("Unable to create class loader for " + className); - } - return classLoader; - } - - private ClassLoader newClassLoader(Class loaderClass, Class[] parameterTypes, Object... initargs) - throws Exception { - try { - Constructor constructor = loaderClass.getDeclaredConstructor(parameterTypes); - constructor.setAccessible(true); - return constructor.newInstance(initargs); - } - catch (NoSuchMethodException ex) { - return null; - } - } - - private String getProperty(String propertyKey) throws Exception { - return getProperty(propertyKey, null, null); - } - - private String getProperty(String propertyKey, String manifestKey) throws Exception { - return getProperty(propertyKey, manifestKey, null); - } - - private String getPropertyWithDefault(String propertyKey, String defaultValue) throws Exception { - return getProperty(propertyKey, null, defaultValue); - } - - private String getProperty(String propertyKey, String manifestKey, String defaultValue) throws Exception { - if (manifestKey == null) { - manifestKey = propertyKey.replace('.', '-'); - manifestKey = toCamelCase(manifestKey); - } - String property = SystemPropertyUtils.getProperty(propertyKey); - if (property != null) { - String value = SystemPropertyUtils.resolvePlaceholders(this.properties, property); - debug("Property '" + propertyKey + "' from environment: " + value); - return value; - } - if (this.properties.containsKey(propertyKey)) { - String value = SystemPropertyUtils.resolvePlaceholders(this.properties, - this.properties.getProperty(propertyKey)); - debug("Property '" + propertyKey + "' from properties: " + value); - return value; - } - try { - if (this.home != null) { - // Prefer home dir for MANIFEST if there is one - try (ExplodedArchive archive = new ExplodedArchive(this.home, false)) { - Manifest manifest = archive.getManifest(); - if (manifest != null) { - String value = manifest.getMainAttributes().getValue(manifestKey); - if (value != null) { - debug("Property '" + manifestKey + "' from home directory manifest: " + value); - return SystemPropertyUtils.resolvePlaceholders(this.properties, value); - } - } - } - } - } - catch (IllegalStateException ex) { - // Ignore - } - // Otherwise try the parent archive - Manifest manifest = createArchive().getManifest(); - if (manifest != null) { - String value = manifest.getMainAttributes().getValue(manifestKey); - if (value != null) { - debug("Property '" + manifestKey + "' from archive manifest: " + value); - return SystemPropertyUtils.resolvePlaceholders(this.properties, value); - } - } - return (defaultValue != null) ? SystemPropertyUtils.resolvePlaceholders(this.properties, defaultValue) - : defaultValue; - } - - @Override - protected Iterator getClassPathArchivesIterator() throws Exception { - ClassPathArchives classPathArchives = this.classPathArchives; - if (classPathArchives == null) { - classPathArchives = new ClassPathArchives(); - this.classPathArchives = classPathArchives; - } - return classPathArchives.iterator(); - } - - public static void main(String[] args) throws Exception { - PropertiesLauncher launcher = new PropertiesLauncher(); - args = launcher.getArgs(args); - launcher.launch(args); - } - - public static String toCamelCase(CharSequence string) { - if (string == null) { - return null; - } - StringBuilder builder = new StringBuilder(); - Matcher matcher = WORD_SEPARATOR.matcher(string); - int pos = 0; - while (matcher.find()) { - builder.append(capitalize(string.subSequence(pos, matcher.end()).toString())); - pos = matcher.end(); - } - builder.append(capitalize(string.subSequence(pos, string.length()).toString())); - return builder.toString(); - } - - private static String capitalize(String str) { - return Character.toUpperCase(str.charAt(0)) + str.substring(1); - } - - private void debug(String message) { - if (Boolean.getBoolean(DEBUG)) { - System.out.println(message); - } - } - - private String cleanupPath(String path) { - path = path.trim(); - // No need for current dir path - if (path.startsWith("./")) { - path = path.substring(2); - } - String lowerCasePath = path.toLowerCase(Locale.ENGLISH); - if (lowerCasePath.endsWith(".jar") || lowerCasePath.endsWith(".zip")) { - return path; - } - if (path.endsWith("/*")) { - path = path.substring(0, path.length() - 1); - } - else { - // It's a directory - if (!path.endsWith("/") && !path.equals(".")) { - path = path + "/"; - } - } - return path; - } - - void close() throws Exception { - if (this.classPathArchives != null) { - this.classPathArchives.close(); - } - if (this.parent != null) { - this.parent.close(); - } - } - - /** - * An iterable collection of the classpath archives. - */ - private class ClassPathArchives implements Iterable { - - private final List classPathArchives; - - private final List jarFileArchives = new ArrayList<>(); - - ClassPathArchives() throws Exception { - this.classPathArchives = new ArrayList<>(); - for (String path : PropertiesLauncher.this.paths) { - for (Archive archive : getClassPathArchives(path)) { - addClassPathArchive(archive); - } - } - addNestedEntries(); - } - - private void addClassPathArchive(Archive archive) throws IOException { - if (!(archive instanceof ExplodedArchive)) { - this.classPathArchives.add(archive); - return; - } - this.classPathArchives.add(archive); - this.classPathArchives.addAll(asList(archive.getNestedArchives(null, new ArchiveEntryFilter()))); - } - - private List getClassPathArchives(String path) throws Exception { - String root = cleanupPath(handleUrl(path)); - List lib = new ArrayList<>(); - File file = new File(root); - if (!"/".equals(root)) { - if (!isAbsolutePath(root)) { - file = new File(PropertiesLauncher.this.home, root); - } - if (file.isDirectory()) { - debug("Adding classpath entries from " + file); - Archive archive = new ExplodedArchive(file, false); - lib.add(archive); - } - } - Archive archive = getArchive(file); - if (archive != null) { - debug("Adding classpath entries from archive " + archive.getUrl() + root); - lib.add(archive); - } - List nestedArchives = getNestedArchives(root); - if (nestedArchives != null) { - debug("Adding classpath entries from nested " + root); - lib.addAll(nestedArchives); - } - return lib; - } - - private boolean isAbsolutePath(String root) { - // Windows contains ":" others start with "/" - return root.contains(":") || root.startsWith("/"); - } - - private Archive getArchive(File file) throws IOException { - if (isNestedArchivePath(file)) { - return null; - } - String name = file.getName().toLowerCase(Locale.ENGLISH); - if (name.endsWith(".jar") || name.endsWith(".zip")) { - return getJarFileArchive(file); - } - return null; - } - - private boolean isNestedArchivePath(File file) { - return file.getPath().contains(NESTED_ARCHIVE_SEPARATOR); - } - - private List getNestedArchives(String path) throws Exception { - Archive parent = PropertiesLauncher.this.parent; - String root = path; - if (!root.equals("/") && root.startsWith("/") - || parent.getUrl().toURI().equals(PropertiesLauncher.this.home.toURI())) { - // If home dir is same as parent archive, no need to add it twice. - return null; - } - int index = root.indexOf('!'); - if (index != -1) { - File file = new File(PropertiesLauncher.this.home, root.substring(0, index)); - if (root.startsWith("jar:file:")) { - file = new File(root.substring("jar:file:".length(), index)); - } - parent = getJarFileArchive(file); - root = root.substring(index + 1); - while (root.startsWith("/")) { - root = root.substring(1); - } - } - if (root.endsWith(".jar")) { - File file = new File(PropertiesLauncher.this.home, root); - if (file.exists()) { - parent = getJarFileArchive(file); - root = ""; - } - } - if (root.equals("/") || root.equals("./") || root.equals(".")) { - // The prefix for nested jars is actually empty if it's at the root - root = ""; - } - EntryFilter filter = new PrefixMatchingArchiveFilter(root); - List archives = asList(parent.getNestedArchives(null, filter)); - if ((root == null || root.isEmpty() || ".".equals(root)) && !path.endsWith(".jar") - && parent != PropertiesLauncher.this.parent) { - // You can't find the root with an entry filter so it has to be added - // explicitly. But don't add the root of the parent archive. - archives.add(parent); - } - return archives; - } - - private void addNestedEntries() { - // The parent archive might have "BOOT-INF/lib/" and "BOOT-INF/classes/" - // directories, meaning we are running from an executable JAR. We add nested - // entries from there with low priority (i.e. at end). - try { - Iterator archives = PropertiesLauncher.this.parent.getNestedArchives(null, - JarLauncher.NESTED_ARCHIVE_ENTRY_FILTER); - while (archives.hasNext()) { - this.classPathArchives.add(archives.next()); - } - } - catch (IOException ex) { - // Ignore - } - } - - private List asList(Iterator iterator) { - List list = new ArrayList<>(); - while (iterator.hasNext()) { - list.add(iterator.next()); - } - return list; - } - - private JarFileArchive getJarFileArchive(File file) throws IOException { - JarFileArchive archive = new JarFileArchive(file); - this.jarFileArchives.add(archive); - return archive; - } - - @Override - public Iterator iterator() { - return this.classPathArchives.iterator(); - } - - void close() throws IOException { - for (JarFileArchive archive : this.jarFileArchives) { - archive.close(); - } - } - - } - - /** - * Convenience class for finding nested archives that have a prefix in their file path - * (e.g. "lib/"). - */ - private static final class PrefixMatchingArchiveFilter implements EntryFilter { - - private final String prefix; - - private final ArchiveEntryFilter filter = new ArchiveEntryFilter(); - - private PrefixMatchingArchiveFilter(String prefix) { - this.prefix = prefix; - } - - @Override - public boolean matches(Entry entry) { - if (entry.isDirectory()) { - return entry.getName().equals(this.prefix); - } - return entry.getName().startsWith(this.prefix) && this.filter.matches(entry); - } - - } - - /** - * Convenience class for finding nested archives (archive entries that can be - * classpath entries). - */ - private static final class ArchiveEntryFilter implements EntryFilter { - - private static final String DOT_JAR = ".jar"; - - private static final String DOT_ZIP = ".zip"; - - @Override - public boolean matches(Entry entry) { - return entry.getName().endsWith(DOT_JAR) || entry.getName().endsWith(DOT_ZIP); - } - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java deleted file mode 100644 index 482832c1f722..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/WarLauncher.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2012-2023 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.loader; - -import org.springframework.boot.loader.archive.Archive; - -/** - * {@link Launcher} for WAR based archives. This launcher for standard WAR archives. - * Supports dependencies in {@code WEB-INF/lib} as well as {@code WEB-INF/lib-provided}, - * classes are loaded from {@code WEB-INF/classes}. - * - * @author Phillip Webb - * @author Andy Wilkinson - * @author Scott Frederick - * @since 1.0.0 - */ -public class WarLauncher extends ExecutableArchiveLauncher { - - public WarLauncher() { - } - - protected WarLauncher(Archive archive) { - super(archive); - } - - @Override - protected boolean isPostProcessingClassPathArchives() { - return false; - } - - @Override - public boolean isNestedArchive(Archive.Entry entry) { - if (entry.isDirectory()) { - return entry.getName().equals("WEB-INF/classes/"); - } - return entry.getName().startsWith("WEB-INF/lib/") || entry.getName().startsWith("WEB-INF/lib-provided/"); - } - - @Override - protected String getArchiveEntryPathPrefix() { - return "WEB-INF/"; - } - - public static void main(String[] args) throws Exception { - new WarLauncher().launch(args); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java deleted file mode 100644 index c1f2bbb2f75b..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/Archive.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.archive; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Iterator; -import java.util.jar.Manifest; - -import org.springframework.boot.loader.Launcher; - -/** - * An archive that can be launched by the {@link Launcher}. - * - * @author Phillip Webb - * @since 1.0.0 - * @see JarFileArchive - */ -public interface Archive extends Iterable, AutoCloseable { - - /** - * Returns a URL that can be used to load the archive. - * @return the archive URL - * @throws MalformedURLException if the URL is malformed - */ - URL getUrl() throws MalformedURLException; - - /** - * Returns the manifest of the archive. - * @return the manifest - * @throws IOException if the manifest cannot be read - */ - Manifest getManifest() throws IOException; - - /** - * Returns nested {@link Archive}s for entries that match the specified filters. - * @param searchFilter filter used to limit when additional sub-entry searching is - * required or {@code null} if all entries should be considered. - * @param includeFilter filter used to determine which entries should be included in - * the result or {@code null} if all entries should be included - * @return the nested archives - * @throws IOException on IO error - * @since 2.3.0 - */ - Iterator getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) throws IOException; - - /** - * Return if the archive is exploded (already unpacked). - * @return if the archive is exploded - * @since 2.3.0 - */ - default boolean isExploded() { - return false; - } - - /** - * Closes the {@code Archive}, releasing any open resources. - * @throws Exception if an error occurs during close processing - * @since 2.2.0 - */ - @Override - default void close() throws Exception { - - } - - /** - * Represents a single entry in the archive. - */ - interface Entry { - - /** - * Returns {@code true} if the entry represents a directory. - * @return if the entry is a directory - */ - boolean isDirectory(); - - /** - * Returns the name of the entry. - * @return the name of the entry - */ - String getName(); - - } - - /** - * Strategy interface to filter {@link Entry Entries}. - */ - @FunctionalInterface - interface EntryFilter { - - /** - * Apply the jar entry filter. - * @param entry the entry to filter - * @return {@code true} if the filter matches - */ - boolean matches(Entry entry); - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java deleted file mode 100644 index f8cd52dc16f1..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/ExplodedArchive.java +++ /dev/null @@ -1,342 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.archive; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.Deque; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.NoSuchElementException; -import java.util.Set; -import java.util.jar.Manifest; - -/** - * {@link Archive} implementation backed by an exploded archive directory. - * - * @author Phillip Webb - * @author Andy Wilkinson - * @author Madhura Bhave - * @since 1.0.0 - */ -public class ExplodedArchive implements Archive { - - private static final Set SKIPPED_NAMES = new HashSet<>(Arrays.asList(".", "..")); - - private final File root; - - private final boolean recursive; - - private final File manifestFile; - - private Manifest manifest; - - /** - * Create a new {@link ExplodedArchive} instance. - * @param root the root directory - */ - public ExplodedArchive(File root) { - this(root, true); - } - - /** - * Create a new {@link ExplodedArchive} instance. - * @param root the root directory - * @param recursive if recursive searching should be used to locate the manifest. - * Defaults to {@code true}, directories with a large tree might want to set this to - * {@code false}. - */ - public ExplodedArchive(File root, boolean recursive) { - if (!root.exists() || !root.isDirectory()) { - throw new IllegalArgumentException("Invalid source directory " + root); - } - this.root = root; - this.recursive = recursive; - this.manifestFile = getManifestFile(root); - } - - private File getManifestFile(File root) { - File metaInf = new File(root, "META-INF"); - return new File(metaInf, "MANIFEST.MF"); - } - - @Override - public URL getUrl() throws MalformedURLException { - return this.root.toURI().toURL(); - } - - @Override - public Manifest getManifest() throws IOException { - if (this.manifest == null && this.manifestFile.exists()) { - try (FileInputStream inputStream = new FileInputStream(this.manifestFile)) { - this.manifest = new Manifest(inputStream); - } - } - return this.manifest; - } - - @Override - public Iterator getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) throws IOException { - return new ArchiveIterator(this.root, this.recursive, searchFilter, includeFilter); - } - - @Override - @Deprecated(since = "2.3.10", forRemoval = false) - public Iterator iterator() { - return new EntryIterator(this.root, this.recursive, null, null); - } - - protected Archive getNestedArchive(Entry entry) { - File file = ((FileEntry) entry).getFile(); - return (file.isDirectory() ? new ExplodedArchive(file) : new SimpleJarFileArchive((FileEntry) entry)); - } - - @Override - public boolean isExploded() { - return true; - } - - @Override - public String toString() { - try { - return getUrl().toString(); - } - catch (Exception ex) { - return "exploded archive"; - } - } - - /** - * File based {@link Entry} {@link Iterator}. - */ - private abstract static class AbstractIterator implements Iterator { - - private static final Comparator entryComparator = Comparator.comparing(File::getAbsolutePath); - - private final File root; - - private final boolean recursive; - - private final EntryFilter searchFilter; - - private final EntryFilter includeFilter; - - private final Deque> stack = new LinkedList<>(); - - private FileEntry current; - - private final String rootUrl; - - AbstractIterator(File root, boolean recursive, EntryFilter searchFilter, EntryFilter includeFilter) { - this.root = root; - this.rootUrl = this.root.toURI().getPath(); - this.recursive = recursive; - this.searchFilter = searchFilter; - this.includeFilter = includeFilter; - this.stack.add(listFiles(root)); - this.current = poll(); - } - - @Override - public boolean hasNext() { - return this.current != null; - } - - @Override - public T next() { - FileEntry entry = this.current; - if (entry == null) { - throw new NoSuchElementException(); - } - this.current = poll(); - return adapt(entry); - } - - private FileEntry poll() { - while (!this.stack.isEmpty()) { - while (this.stack.peek().hasNext()) { - File file = this.stack.peek().next(); - if (SKIPPED_NAMES.contains(file.getName())) { - continue; - } - FileEntry entry = getFileEntry(file); - if (isListable(entry)) { - this.stack.addFirst(listFiles(file)); - } - if (this.includeFilter == null || this.includeFilter.matches(entry)) { - return entry; - } - } - this.stack.poll(); - } - return null; - } - - private FileEntry getFileEntry(File file) { - URI uri = file.toURI(); - String name = uri.getPath().substring(this.rootUrl.length()); - try { - return new FileEntry(name, file, uri.toURL()); - } - catch (MalformedURLException ex) { - throw new IllegalStateException(ex); - } - } - - private boolean isListable(FileEntry entry) { - return entry.isDirectory() && (this.recursive || entry.getFile().getParentFile().equals(this.root)) - && (this.searchFilter == null || this.searchFilter.matches(entry)) - && (this.includeFilter == null || !this.includeFilter.matches(entry)); - } - - private Iterator listFiles(File file) { - File[] files = file.listFiles(); - if (files == null) { - return Collections.emptyIterator(); - } - Arrays.sort(files, entryComparator); - return Arrays.asList(files).iterator(); - } - - @Override - public void remove() { - throw new UnsupportedOperationException("remove"); - } - - protected abstract T adapt(FileEntry entry); - - } - - private static class EntryIterator extends AbstractIterator { - - EntryIterator(File root, boolean recursive, EntryFilter searchFilter, EntryFilter includeFilter) { - super(root, recursive, searchFilter, includeFilter); - } - - @Override - protected Entry adapt(FileEntry entry) { - return entry; - } - - } - - private static class ArchiveIterator extends AbstractIterator { - - ArchiveIterator(File root, boolean recursive, EntryFilter searchFilter, EntryFilter includeFilter) { - super(root, recursive, searchFilter, includeFilter); - } - - @Override - protected Archive adapt(FileEntry entry) { - File file = entry.getFile(); - return (file.isDirectory() ? new ExplodedArchive(file) : new SimpleJarFileArchive(entry)); - } - - } - - /** - * {@link Entry} backed by a File. - */ - private static class FileEntry implements Entry { - - private final String name; - - private final File file; - - private final URL url; - - FileEntry(String name, File file, URL url) { - this.name = name; - this.file = file; - this.url = url; - } - - File getFile() { - return this.file; - } - - @Override - public boolean isDirectory() { - return this.file.isDirectory(); - } - - @Override - public String getName() { - return this.name; - } - - URL getUrl() { - return this.url; - } - - } - - /** - * {@link Archive} implementation backed by a simple JAR file that doesn't itself - * contain nested archives. - */ - private static class SimpleJarFileArchive implements Archive { - - private final URL url; - - SimpleJarFileArchive(FileEntry file) { - this.url = file.getUrl(); - } - - @Override - public URL getUrl() throws MalformedURLException { - return this.url; - } - - @Override - public Manifest getManifest() throws IOException { - return null; - } - - @Override - public Iterator getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) - throws IOException { - return Collections.emptyIterator(); - } - - @Override - @Deprecated(since = "2.3.10", forRemoval = false) - public Iterator iterator() { - return Collections.emptyIterator(); - } - - @Override - public String toString() { - try { - return getUrl().toString(); - } - catch (Exception ex) { - return "jar archive"; - } - } - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java deleted file mode 100755 index 91e7bc53a486..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/JarFileArchive.java +++ /dev/null @@ -1,310 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.archive; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.MalformedURLException; -import java.net.URL; -import java.nio.file.FileSystem; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; -import java.nio.file.attribute.FileAttribute; -import java.nio.file.attribute.PosixFilePermission; -import java.nio.file.attribute.PosixFilePermissions; -import java.util.EnumSet; -import java.util.Iterator; -import java.util.UUID; -import java.util.jar.JarEntry; -import java.util.jar.Manifest; - -import org.springframework.boot.loader.jar.JarFile; - -/** - * {@link Archive} implementation backed by a {@link JarFile}. - * - * @author Phillip Webb - * @author Andy Wilkinson - * @since 1.0.0 - */ -public class JarFileArchive implements Archive { - - private static final String UNPACK_MARKER = "UNPACK:"; - - private static final int BUFFER_SIZE = 32 * 1024; - - private static final FileAttribute[] NO_FILE_ATTRIBUTES = {}; - - private static final EnumSet DIRECTORY_PERMISSIONS = EnumSet.of(PosixFilePermission.OWNER_READ, - PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE); - - private static final EnumSet FILE_PERMISSIONS = EnumSet.of(PosixFilePermission.OWNER_READ, - PosixFilePermission.OWNER_WRITE); - - private final JarFile jarFile; - - private URL url; - - private Path tempUnpackDirectory; - - public JarFileArchive(File file) throws IOException { - this(file, file.toURI().toURL()); - } - - public JarFileArchive(File file, URL url) throws IOException { - this(new JarFile(file)); - this.url = url; - } - - public JarFileArchive(JarFile jarFile) { - this.jarFile = jarFile; - } - - @Override - public URL getUrl() throws MalformedURLException { - if (this.url != null) { - return this.url; - } - return this.jarFile.getUrl(); - } - - @Override - public Manifest getManifest() throws IOException { - return this.jarFile.getManifest(); - } - - @Override - public Iterator getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter) throws IOException { - return new NestedArchiveIterator(this.jarFile.iterator(), searchFilter, includeFilter); - } - - @Override - @Deprecated(since = "2.3.10", forRemoval = false) - public Iterator iterator() { - return new EntryIterator(this.jarFile.iterator(), null, null); - } - - @Override - public void close() throws IOException { - this.jarFile.close(); - } - - protected Archive getNestedArchive(Entry entry) throws IOException { - JarEntry jarEntry = ((JarFileEntry) entry).getJarEntry(); - if (jarEntry.getComment().startsWith(UNPACK_MARKER)) { - return getUnpackedNestedArchive(jarEntry); - } - try { - JarFile jarFile = this.jarFile.getNestedJarFile(jarEntry); - return new JarFileArchive(jarFile); - } - catch (Exception ex) { - throw new IllegalStateException("Failed to get nested archive for entry " + entry.getName(), ex); - } - } - - private Archive getUnpackedNestedArchive(JarEntry jarEntry) throws IOException { - String name = jarEntry.getName(); - if (name.lastIndexOf('/') != -1) { - name = name.substring(name.lastIndexOf('/') + 1); - } - Path path = getTempUnpackDirectory().resolve(name); - if (!Files.exists(path) || Files.size(path) != jarEntry.getSize()) { - unpack(jarEntry, path); - } - return new JarFileArchive(path.toFile(), path.toUri().toURL()); - } - - private Path getTempUnpackDirectory() { - if (this.tempUnpackDirectory == null) { - Path tempDirectory = Paths.get(System.getProperty("java.io.tmpdir")); - this.tempUnpackDirectory = createUnpackDirectory(tempDirectory); - } - return this.tempUnpackDirectory; - } - - private Path createUnpackDirectory(Path parent) { - int attempts = 0; - while (attempts++ < 1000) { - String fileName = Paths.get(this.jarFile.getName()).getFileName().toString(); - Path unpackDirectory = parent.resolve(fileName + "-spring-boot-libs-" + UUID.randomUUID()); - try { - createDirectory(unpackDirectory); - return unpackDirectory; - } - catch (IOException ex) { - } - } - throw new IllegalStateException("Failed to create unpack directory in directory '" + parent + "'"); - } - - private void unpack(JarEntry entry, Path path) throws IOException { - createFile(path); - path.toFile().deleteOnExit(); - try (InputStream inputStream = this.jarFile.getInputStream(entry); - OutputStream outputStream = Files.newOutputStream(path, StandardOpenOption.WRITE, - StandardOpenOption.TRUNCATE_EXISTING)) { - byte[] buffer = new byte[BUFFER_SIZE]; - int bytesRead; - while ((bytesRead = inputStream.read(buffer)) != -1) { - outputStream.write(buffer, 0, bytesRead); - } - outputStream.flush(); - } - } - - private void createDirectory(Path path) throws IOException { - Files.createDirectory(path, getFileAttributes(path.getFileSystem(), DIRECTORY_PERMISSIONS)); - } - - private void createFile(Path path) throws IOException { - Files.createFile(path, getFileAttributes(path.getFileSystem(), FILE_PERMISSIONS)); - } - - private FileAttribute[] getFileAttributes(FileSystem fileSystem, EnumSet ownerReadWrite) { - if (!fileSystem.supportedFileAttributeViews().contains("posix")) { - return NO_FILE_ATTRIBUTES; - } - return new FileAttribute[] { PosixFilePermissions.asFileAttribute(ownerReadWrite) }; - } - - @Override - public String toString() { - try { - return getUrl().toString(); - } - catch (Exception ex) { - return "jar archive"; - } - } - - /** - * Abstract base class for iterator implementations. - */ - private abstract static class AbstractIterator implements Iterator { - - private final Iterator iterator; - - private final EntryFilter searchFilter; - - private final EntryFilter includeFilter; - - private Entry current; - - AbstractIterator(Iterator iterator, EntryFilter searchFilter, EntryFilter includeFilter) { - this.iterator = iterator; - this.searchFilter = searchFilter; - this.includeFilter = includeFilter; - this.current = poll(); - } - - @Override - public boolean hasNext() { - return this.current != null; - } - - @Override - public T next() { - T result = adapt(this.current); - this.current = poll(); - return result; - } - - private Entry poll() { - while (this.iterator.hasNext()) { - JarFileEntry candidate = new JarFileEntry(this.iterator.next()); - if ((this.searchFilter == null || this.searchFilter.matches(candidate)) - && (this.includeFilter == null || this.includeFilter.matches(candidate))) { - return candidate; - } - } - return null; - } - - protected abstract T adapt(Entry entry); - - } - - /** - * {@link Archive.Entry} iterator implementation backed by {@link JarEntry}. - */ - private static class EntryIterator extends AbstractIterator { - - EntryIterator(Iterator iterator, EntryFilter searchFilter, EntryFilter includeFilter) { - super(iterator, searchFilter, includeFilter); - } - - @Override - protected Entry adapt(Entry entry) { - return entry; - } - - } - - /** - * Nested {@link Archive} iterator implementation backed by {@link JarEntry}. - */ - private class NestedArchiveIterator extends AbstractIterator { - - NestedArchiveIterator(Iterator iterator, EntryFilter searchFilter, EntryFilter includeFilter) { - super(iterator, searchFilter, includeFilter); - } - - @Override - protected Archive adapt(Entry entry) { - try { - return getNestedArchive(entry); - } - catch (IOException ex) { - throw new IllegalStateException(ex); - } - } - - } - - /** - * {@link Archive.Entry} implementation backed by a {@link JarEntry}. - */ - private static class JarFileEntry implements Entry { - - private final JarEntry jarEntry; - - JarFileEntry(JarEntry jarEntry) { - this.jarEntry = jarEntry; - } - - JarEntry getJarEntry() { - return this.jarEntry; - } - - @Override - public boolean isDirectory() { - return this.jarEntry.isDirectory(); - } - - @Override - public String getName() { - return this.jarEntry.getName(); - } - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java deleted file mode 100644 index e96d5ea81a05..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessData.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.data; - -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; - -/** - * Interface that provides read-only random access to some underlying data. - * Implementations must allow concurrent reads in a thread-safe manner. - * - * @author Phillip Webb - * @since 1.0.0 - */ -public interface RandomAccessData { - - /** - * Returns an {@link InputStream} that can be used to read the underlying data. The - * caller is responsible close the underlying stream. - * @return a new input stream that can be used to read the underlying data. - * @throws IOException if the stream cannot be opened - */ - InputStream getInputStream() throws IOException; - - /** - * Returns a new {@link RandomAccessData} for a specific subsection of this data. - * @param offset the offset of the subsection - * @param length the length of the subsection - * @return the subsection data - */ - RandomAccessData getSubsection(long offset, long length); - - /** - * Reads all the data and returns it as a byte array. - * @return the data - * @throws IOException if the data cannot be read - */ - byte[] read() throws IOException; - - /** - * Reads the {@code length} bytes of data starting at the given {@code offset}. - * @param offset the offset from which data should be read - * @param length the number of bytes to be read - * @return the data - * @throws IOException if the data cannot be read - * @throws IndexOutOfBoundsException if offset is beyond the end of the file or - * subsection - * @throws EOFException if offset plus length is greater than the length of the file - * or subsection - */ - byte[] read(long offset, long length) throws IOException; - - /** - * Returns the size of the data. - * @return the size - */ - long getSize(); - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java deleted file mode 100644 index 4bd5d205418c..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/RandomAccessDataFile.java +++ /dev/null @@ -1,262 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.data; - -import java.io.EOFException; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.RandomAccessFile; - -/** - * {@link RandomAccessData} implementation backed by a {@link RandomAccessFile}. - * - * @author Phillip Webb - * @author Andy Wilkinson - * @since 1.0.0 - */ -public class RandomAccessDataFile implements RandomAccessData { - - private final FileAccess fileAccess; - - private final long offset; - - private final long length; - - /** - * Create a new {@link RandomAccessDataFile} backed by the specified file. - * @param file the underlying file - * @throws IllegalArgumentException if the file is null or does not exist - */ - public RandomAccessDataFile(File file) { - if (file == null) { - throw new IllegalArgumentException("File must not be null"); - } - this.fileAccess = new FileAccess(file); - this.offset = 0L; - this.length = file.length(); - } - - /** - * Private constructor used to create a {@link #getSubsection(long, long) subsection}. - * @param fileAccess provides access to the underlying file - * @param offset the offset of the section - * @param length the length of the section - */ - private RandomAccessDataFile(FileAccess fileAccess, long offset, long length) { - this.fileAccess = fileAccess; - this.offset = offset; - this.length = length; - } - - /** - * Returns the underlying File. - * @return the underlying file - */ - public File getFile() { - return this.fileAccess.file; - } - - @Override - public InputStream getInputStream() throws IOException { - return new DataInputStream(); - } - - @Override - public RandomAccessData getSubsection(long offset, long length) { - if (offset < 0 || length < 0 || offset + length > this.length) { - throw new IndexOutOfBoundsException(); - } - return new RandomAccessDataFile(this.fileAccess, this.offset + offset, length); - } - - @Override - public byte[] read() throws IOException { - return read(0, this.length); - } - - @Override - public byte[] read(long offset, long length) throws IOException { - if (offset > this.length) { - throw new IndexOutOfBoundsException(); - } - if (offset + length > this.length) { - throw new EOFException(); - } - byte[] bytes = new byte[(int) length]; - read(bytes, offset, 0, bytes.length); - return bytes; - } - - private int readByte(long position) throws IOException { - if (position >= this.length) { - return -1; - } - return this.fileAccess.readByte(this.offset + position); - } - - private int read(byte[] bytes, long position, int offset, int length) throws IOException { - if (position > this.length) { - return -1; - } - return this.fileAccess.read(bytes, this.offset + position, offset, length); - } - - @Override - public long getSize() { - return this.length; - } - - public void close() throws IOException { - this.fileAccess.close(); - } - - /** - * {@link InputStream} implementation for the {@link RandomAccessDataFile}. - */ - private class DataInputStream extends InputStream { - - private int position; - - @Override - public int read() throws IOException { - int read = RandomAccessDataFile.this.readByte(this.position); - if (read > -1) { - moveOn(1); - } - return read; - } - - @Override - public int read(byte[] b) throws IOException { - return read(b, 0, (b != null) ? b.length : 0); - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - if (b == null) { - throw new NullPointerException("Bytes must not be null"); - } - return doRead(b, off, len); - } - - /** - * Perform the actual read. - * @param b the bytes to read or {@code null} when reading a single byte - * @param off the offset of the byte array - * @param len the length of data to read - * @return the number of bytes read into {@code b} or the actual read byte if - * {@code b} is {@code null}. Returns -1 when the end of the stream is reached - * @throws IOException in case of I/O errors - */ - int doRead(byte[] b, int off, int len) throws IOException { - if (len == 0) { - return 0; - } - int cappedLen = cap(len); - if (cappedLen <= 0) { - return -1; - } - return (int) moveOn(RandomAccessDataFile.this.read(b, this.position, off, cappedLen)); - } - - @Override - public long skip(long n) throws IOException { - return (n <= 0) ? 0 : moveOn(cap(n)); - } - - @Override - public int available() throws IOException { - return (int) RandomAccessDataFile.this.length - this.position; - } - - /** - * Cap the specified value such that it cannot exceed the number of bytes - * remaining. - * @param n the value to cap - * @return the capped value - */ - private int cap(long n) { - return (int) Math.min(RandomAccessDataFile.this.length - this.position, n); - } - - /** - * Move the stream position forwards the specified amount. - * @param amount the amount to move - * @return the amount moved - */ - private long moveOn(int amount) { - this.position += amount; - return amount; - } - - } - - private static final class FileAccess { - - private final Object monitor = new Object(); - - private final File file; - - private RandomAccessFile randomAccessFile; - - private FileAccess(File file) { - this.file = file; - openIfNecessary(); - } - - private int read(byte[] bytes, long position, int offset, int length) throws IOException { - synchronized (this.monitor) { - openIfNecessary(); - this.randomAccessFile.seek(position); - return this.randomAccessFile.read(bytes, offset, length); - } - } - - private void openIfNecessary() { - if (this.randomAccessFile == null) { - try { - this.randomAccessFile = new RandomAccessFile(this.file, "r"); - } - catch (FileNotFoundException ex) { - throw new IllegalArgumentException( - String.format("File %s must exist", this.file.getAbsolutePath())); - } - } - } - - private void close() throws IOException { - synchronized (this.monitor) { - if (this.randomAccessFile != null) { - this.randomAccessFile.close(); - this.randomAccessFile = null; - } - } - } - - private int readByte(long position) throws IOException { - synchronized (this.monitor) { - openIfNecessary(); - this.randomAccessFile.seek(position); - return this.randomAccessFile.read(); - } - } - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java deleted file mode 100644 index 6a98ef682189..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AbstractJarFile.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.jar; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.MalformedURLException; -import java.net.URL; -import java.security.Permission; - -/** - * Base class for extended variants of {@link java.util.jar.JarFile}. - * - * @author Phillip Webb - */ -abstract class AbstractJarFile extends java.util.jar.JarFile { - - /** - * Create a new {@link AbstractJarFile}. - * @param file the root jar file. - * @throws IOException on IO error - */ - AbstractJarFile(File file) throws IOException { - super(file); - } - - /** - * Return a URL that can be used to access this JAR file. NOTE: the specified URL - * cannot be serialized and or cloned. - * @return the URL - * @throws MalformedURLException if the URL is malformed - */ - abstract URL getUrl() throws MalformedURLException; - - /** - * Return the {@link JarFileType} of this instance. - * @return the jar file type - */ - abstract JarFileType getType(); - - /** - * Return the security permission for this JAR. - * @return the security permission. - */ - abstract Permission getPermission(); - - /** - * Return an {@link InputStream} for the entire jar contents. - * @return the contents input stream - * @throws IOException on IO error - */ - abstract InputStream getInputStream() throws IOException; - - /** - * The type of a {@link JarFile}. - */ - enum JarFileType { - - DIRECT, NESTED_DIRECTORY, NESTED_JAR - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java deleted file mode 100644 index cfe121b68996..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/AsciiBytes.java +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.jar; - -import java.nio.charset.StandardCharsets; - -/** - * Simple wrapper around a byte array that represents an ASCII. Used for performance - * reasons to save constructing Strings for ZIP data. - * - * @author Phillip Webb - * @author Andy Wilkinson - */ -final class AsciiBytes { - - private static final String EMPTY_STRING = ""; - - private static final int[] INITIAL_BYTE_BITMASK = { 0x7F, 0x1F, 0x0F, 0x07 }; - - private static final int SUBSEQUENT_BYTE_BITMASK = 0x3F; - - private final byte[] bytes; - - private final int offset; - - private final int length; - - private String string; - - private int hash; - - /** - * Create a new {@link AsciiBytes} from the specified String. - * @param string the source string - */ - AsciiBytes(String string) { - this(string.getBytes(StandardCharsets.UTF_8)); - this.string = string; - } - - /** - * Create a new {@link AsciiBytes} from the specified bytes. NOTE: underlying bytes - * are not expected to change. - * @param bytes the source bytes - */ - AsciiBytes(byte[] bytes) { - this(bytes, 0, bytes.length); - } - - /** - * Create a new {@link AsciiBytes} from the specified bytes. NOTE: underlying bytes - * are not expected to change. - * @param bytes the source bytes - * @param offset the offset - * @param length the length - */ - AsciiBytes(byte[] bytes, int offset, int length) { - if (offset < 0 || length < 0 || (offset + length) > bytes.length) { - throw new IndexOutOfBoundsException(); - } - this.bytes = bytes; - this.offset = offset; - this.length = length; - } - - int length() { - return this.length; - } - - boolean startsWith(AsciiBytes prefix) { - if (this == prefix) { - return true; - } - if (prefix.length > this.length) { - return false; - } - for (int i = 0; i < prefix.length; i++) { - if (this.bytes[i + this.offset] != prefix.bytes[i + prefix.offset]) { - return false; - } - } - return true; - } - - boolean endsWith(AsciiBytes postfix) { - if (this == postfix) { - return true; - } - if (postfix.length > this.length) { - return false; - } - for (int i = 0; i < postfix.length; i++) { - if (this.bytes[this.offset + (this.length - 1) - i] != postfix.bytes[postfix.offset + (postfix.length - 1) - - i]) { - return false; - } - } - return true; - } - - AsciiBytes substring(int beginIndex) { - return substring(beginIndex, this.length); - } - - AsciiBytes substring(int beginIndex, int endIndex) { - int length = endIndex - beginIndex; - if (this.offset + length > this.bytes.length) { - throw new IndexOutOfBoundsException(); - } - return new AsciiBytes(this.bytes, this.offset + beginIndex, length); - } - - boolean matches(CharSequence name, char suffix) { - int charIndex = 0; - int nameLen = name.length(); - int totalLen = nameLen + ((suffix != 0) ? 1 : 0); - for (int i = this.offset; i < this.offset + this.length; i++) { - int b = this.bytes[i]; - int remainingUtfBytes = getNumberOfUtfBytes(b) - 1; - b &= INITIAL_BYTE_BITMASK[remainingUtfBytes]; - for (int j = 0; j < remainingUtfBytes; j++) { - b = (b << 6) + (this.bytes[++i] & SUBSEQUENT_BYTE_BITMASK); - } - char c = getChar(name, suffix, charIndex++); - if (b <= 0xFFFF) { - if (c != b) { - return false; - } - } - else { - if (c != ((b >> 0xA) + 0xD7C0)) { - return false; - } - c = getChar(name, suffix, charIndex++); - if (c != ((b & 0x3FF) + 0xDC00)) { - return false; - } - } - } - return charIndex == totalLen; - } - - private char getChar(CharSequence name, char suffix, int index) { - if (index < name.length()) { - return name.charAt(index); - } - if (index == name.length()) { - return suffix; - } - return 0; - } - - private int getNumberOfUtfBytes(int b) { - if ((b & 0x80) == 0) { - return 1; - } - int numberOfUtfBytes = 0; - while ((b & 0x80) != 0) { - b <<= 1; - numberOfUtfBytes++; - } - return numberOfUtfBytes; - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (this == obj) { - return true; - } - if (obj.getClass() == AsciiBytes.class) { - AsciiBytes other = (AsciiBytes) obj; - if (this.length == other.length) { - for (int i = 0; i < this.length; i++) { - if (this.bytes[this.offset + i] != other.bytes[other.offset + i]) { - return false; - } - } - return true; - } - } - return false; - } - - @Override - public int hashCode() { - int hash = this.hash; - if (hash == 0 && this.bytes.length > 0) { - for (int i = this.offset; i < this.offset + this.length; i++) { - int b = this.bytes[i]; - int remainingUtfBytes = getNumberOfUtfBytes(b) - 1; - b &= INITIAL_BYTE_BITMASK[remainingUtfBytes]; - for (int j = 0; j < remainingUtfBytes; j++) { - b = (b << 6) + (this.bytes[++i] & SUBSEQUENT_BYTE_BITMASK); - } - if (b <= 0xFFFF) { - hash = 31 * hash + b; - } - else { - hash = 31 * hash + ((b >> 0xA) + 0xD7C0); - hash = 31 * hash + ((b & 0x3FF) + 0xDC00); - } - } - this.hash = hash; - } - return hash; - } - - @Override - public String toString() { - if (this.string == null) { - if (this.length == 0) { - this.string = EMPTY_STRING; - } - else { - this.string = new String(this.bytes, this.offset, this.length, StandardCharsets.UTF_8); - } - } - return this.string; - } - - static String toString(byte[] bytes) { - return new String(bytes, StandardCharsets.UTF_8); - } - - static int hashCode(CharSequence charSequence) { - // We're compatible with String's hashCode() - if (charSequence instanceof StringSequence) { - // ... but save making an unnecessary String for StringSequence - return charSequence.hashCode(); - } - return charSequence.toString().hashCode(); - } - - static int hashCode(int hash, char suffix) { - return (suffix != 0) ? (31 * hash + suffix) : hash; - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java deleted file mode 100644 index 61db0b73f422..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryEndRecord.java +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.jar; - -import java.io.IOException; - -import org.springframework.boot.loader.data.RandomAccessData; - -/** - * A ZIP File "End of central directory record" (EOCD). - * - * @author Phillip Webb - * @author Andy Wilkinson - * @author Camille Vienot - * @see Zip File Format - */ -class CentralDirectoryEndRecord { - - private static final int MINIMUM_SIZE = 22; - - private static final int MAXIMUM_COMMENT_LENGTH = 0xFFFF; - - private static final int MAXIMUM_SIZE = MINIMUM_SIZE + MAXIMUM_COMMENT_LENGTH; - - private static final int SIGNATURE = 0x06054b50; - - private static final int COMMENT_LENGTH_OFFSET = 20; - - private static final int READ_BLOCK_SIZE = 256; - - private final Zip64End zip64End; - - private byte[] block; - - private int offset; - - private int size; - - /** - * Create a new {@link CentralDirectoryEndRecord} instance from the specified - * {@link RandomAccessData}, searching backwards from the end until a valid block is - * located. - * @param data the source data - * @throws IOException in case of I/O errors - */ - CentralDirectoryEndRecord(RandomAccessData data) throws IOException { - this.block = createBlockFromEndOfData(data, READ_BLOCK_SIZE); - this.size = MINIMUM_SIZE; - this.offset = this.block.length - this.size; - while (!isValid()) { - this.size++; - if (this.size > this.block.length) { - if (this.size >= MAXIMUM_SIZE || this.size > data.getSize()) { - throw new IOException( - "Unable to find ZIP central directory records after reading " + this.size + " bytes"); - } - this.block = createBlockFromEndOfData(data, this.size + READ_BLOCK_SIZE); - } - this.offset = this.block.length - this.size; - } - long startOfCentralDirectoryEndRecord = data.getSize() - this.size; - Zip64Locator zip64Locator = Zip64Locator.find(data, startOfCentralDirectoryEndRecord); - this.zip64End = (zip64Locator != null) ? new Zip64End(data, zip64Locator) : null; - } - - private byte[] createBlockFromEndOfData(RandomAccessData data, int size) throws IOException { - int length = (int) Math.min(data.getSize(), size); - return data.read(data.getSize() - length, length); - } - - private boolean isValid() { - if (this.block.length < MINIMUM_SIZE || Bytes.littleEndianValue(this.block, this.offset + 0, 4) != SIGNATURE) { - return false; - } - // Total size must be the structure size + comment - long commentLength = Bytes.littleEndianValue(this.block, this.offset + COMMENT_LENGTH_OFFSET, 2); - return this.size == MINIMUM_SIZE + commentLength; - } - - /** - * Returns the location in the data that the archive actually starts. For most files - * the archive data will start at 0, however, it is possible to have prefixed bytes - * (often used for startup scripts) at the beginning of the data. - * @param data the source data - * @return the offset within the data where the archive begins - */ - long getStartOfArchive(RandomAccessData data) { - long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4); - long specifiedOffset = (this.zip64End != null) ? this.zip64End.centralDirectoryOffset - : Bytes.littleEndianValue(this.block, this.offset + 16, 4); - long zip64EndSize = (this.zip64End != null) ? this.zip64End.getSize() : 0L; - int zip64LocSize = (this.zip64End != null) ? Zip64Locator.ZIP64_LOCSIZE : 0; - long actualOffset = data.getSize() - this.size - length - zip64EndSize - zip64LocSize; - return actualOffset - specifiedOffset; - } - - /** - * Return the bytes of the "Central directory" based on the offset indicated in this - * record. - * @param data the source data - * @return the central directory data - */ - RandomAccessData getCentralDirectory(RandomAccessData data) { - if (this.zip64End != null) { - return this.zip64End.getCentralDirectory(data); - } - long offset = Bytes.littleEndianValue(this.block, this.offset + 16, 4); - long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4); - return data.getSubsection(offset, length); - } - - /** - * Return the number of ZIP entries in the file. - * @return the number of records in the zip - */ - int getNumberOfRecords() { - if (this.zip64End != null) { - return this.zip64End.getNumberOfRecords(); - } - long numberOfRecords = Bytes.littleEndianValue(this.block, this.offset + 10, 2); - return (int) numberOfRecords; - } - - String getComment() { - int commentLength = (int) Bytes.littleEndianValue(this.block, this.offset + COMMENT_LENGTH_OFFSET, 2); - AsciiBytes comment = new AsciiBytes(this.block, this.offset + COMMENT_LENGTH_OFFSET + 2, commentLength); - return comment.toString(); - } - - boolean isZip64() { - return this.zip64End != null; - } - - /** - * A Zip64 end of central directory record. - * - * @see Chapter - * 4.3.14 of Zip64 specification - */ - private static final class Zip64End { - - private static final int ZIP64_ENDTOT = 32; // total number of entries - - private static final int ZIP64_ENDSIZ = 40; // central directory size in bytes - - private static final int ZIP64_ENDOFF = 48; // offset of first CEN header - - private final Zip64Locator locator; - - private final long centralDirectoryOffset; - - private final long centralDirectoryLength; - - private final int numberOfRecords; - - private Zip64End(RandomAccessData data, Zip64Locator locator) throws IOException { - this.locator = locator; - byte[] block = data.read(locator.getZip64EndOffset(), 56); - this.centralDirectoryOffset = Bytes.littleEndianValue(block, ZIP64_ENDOFF, 8); - this.centralDirectoryLength = Bytes.littleEndianValue(block, ZIP64_ENDSIZ, 8); - this.numberOfRecords = (int) Bytes.littleEndianValue(block, ZIP64_ENDTOT, 8); - } - - /** - * Return the size of this zip 64 end of central directory record. - * @return size of this zip 64 end of central directory record - */ - private long getSize() { - return this.locator.getZip64EndSize(); - } - - /** - * Return the bytes of the "Central directory" based on the offset indicated in - * this record. - * @param data the source data - * @return the central directory data - */ - private RandomAccessData getCentralDirectory(RandomAccessData data) { - return data.getSubsection(this.centralDirectoryOffset, this.centralDirectoryLength); - } - - /** - * Return the number of entries in the zip64 archive. - * @return the number of records in the zip - */ - private int getNumberOfRecords() { - return this.numberOfRecords; - } - - } - - /** - * A Zip64 end of central directory locator. - * - * @see Chapter - * 4.3.15 of Zip64 specification - */ - private static final class Zip64Locator { - - static final int SIGNATURE = 0x07064b50; - - static final int ZIP64_LOCSIZE = 20; // locator size - - static final int ZIP64_LOCOFF = 8; // offset of zip64 end - - private final long zip64EndOffset; - - private final long offset; - - private Zip64Locator(long offset, byte[] block) { - this.offset = offset; - this.zip64EndOffset = Bytes.littleEndianValue(block, ZIP64_LOCOFF, 8); - } - - /** - * Return the size of the zip 64 end record located by this zip64 end locator. - * @return size of the zip 64 end record located by this zip64 end locator - */ - private long getZip64EndSize() { - return this.offset - this.zip64EndOffset; - } - - /** - * Return the offset to locate {@link Zip64End}. - * @return offset of the Zip64 end of central directory record - */ - private long getZip64EndOffset() { - return this.zip64EndOffset; - } - - private static Zip64Locator find(RandomAccessData data, long centralDirectoryEndOffset) throws IOException { - long offset = centralDirectoryEndOffset - ZIP64_LOCSIZE; - if (offset >= 0) { - byte[] block = data.read(offset, ZIP64_LOCSIZE); - if (Bytes.littleEndianValue(block, 0, 4) == SIGNATURE) { - return new Zip64Locator(offset, block); - } - } - return null; - } - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java deleted file mode 100644 index 19c88dda5241..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryFileHeader.java +++ /dev/null @@ -1,222 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.jar; - -import java.io.IOException; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.temporal.ChronoField; -import java.time.temporal.ChronoUnit; -import java.time.temporal.ValueRange; - -import org.springframework.boot.loader.data.RandomAccessData; - -/** - * A ZIP File "Central directory file header record" (CDFH). - * - * @author Phillip Webb - * @author Andy Wilkinson - * @author Dmytro Nosan - * @see Zip File Format - */ - -final class CentralDirectoryFileHeader implements FileHeader { - - private static final AsciiBytes SLASH = new AsciiBytes("/"); - - private static final byte[] NO_EXTRA = {}; - - private static final AsciiBytes NO_COMMENT = new AsciiBytes(""); - - private byte[] header; - - private int headerOffset; - - private AsciiBytes name; - - private byte[] extra; - - private AsciiBytes comment; - - private long localHeaderOffset; - - CentralDirectoryFileHeader() { - } - - CentralDirectoryFileHeader(byte[] header, int headerOffset, AsciiBytes name, byte[] extra, AsciiBytes comment, - long localHeaderOffset) { - this.header = header; - this.headerOffset = headerOffset; - this.name = name; - this.extra = extra; - this.comment = comment; - this.localHeaderOffset = localHeaderOffset; - } - - void load(byte[] data, int dataOffset, RandomAccessData variableData, long variableOffset, JarEntryFilter filter) - throws IOException { - // Load fixed part - this.header = data; - this.headerOffset = dataOffset; - long compressedSize = Bytes.littleEndianValue(data, dataOffset + 20, 4); - long uncompressedSize = Bytes.littleEndianValue(data, dataOffset + 24, 4); - long nameLength = Bytes.littleEndianValue(data, dataOffset + 28, 2); - long extraLength = Bytes.littleEndianValue(data, dataOffset + 30, 2); - long commentLength = Bytes.littleEndianValue(data, dataOffset + 32, 2); - long localHeaderOffset = Bytes.littleEndianValue(data, dataOffset + 42, 4); - // Load variable part - dataOffset += 46; - if (variableData != null) { - data = variableData.read(variableOffset + 46, nameLength + extraLength + commentLength); - dataOffset = 0; - } - this.name = new AsciiBytes(data, dataOffset, (int) nameLength); - if (filter != null) { - this.name = filter.apply(this.name); - } - this.extra = NO_EXTRA; - this.comment = NO_COMMENT; - if (extraLength > 0) { - this.extra = new byte[(int) extraLength]; - System.arraycopy(data, (int) (dataOffset + nameLength), this.extra, 0, this.extra.length); - } - this.localHeaderOffset = getLocalHeaderOffset(compressedSize, uncompressedSize, localHeaderOffset, this.extra); - if (commentLength > 0) { - this.comment = new AsciiBytes(data, (int) (dataOffset + nameLength + extraLength), (int) commentLength); - } - } - - private long getLocalHeaderOffset(long compressedSize, long uncompressedSize, long localHeaderOffset, byte[] extra) - throws IOException { - if (localHeaderOffset != 0xFFFFFFFFL) { - return localHeaderOffset; - } - int extraOffset = 0; - while (extraOffset < extra.length - 2) { - int id = (int) Bytes.littleEndianValue(extra, extraOffset, 2); - int length = (int) Bytes.littleEndianValue(extra, extraOffset, 2); - extraOffset += 4; - if (id == 1) { - int localHeaderExtraOffset = 0; - if (compressedSize == 0xFFFFFFFFL) { - localHeaderExtraOffset += 4; - } - if (uncompressedSize == 0xFFFFFFFFL) { - localHeaderExtraOffset += 4; - } - return Bytes.littleEndianValue(extra, extraOffset + localHeaderExtraOffset, 8); - } - extraOffset += length; - } - throw new IOException("Zip64 Extended Information Extra Field not found"); - } - - AsciiBytes getName() { - return this.name; - } - - @Override - public boolean hasName(CharSequence name, char suffix) { - return this.name.matches(name, suffix); - } - - boolean isDirectory() { - return this.name.endsWith(SLASH); - } - - @Override - public int getMethod() { - return (int) Bytes.littleEndianValue(this.header, this.headerOffset + 10, 2); - } - - long getTime() { - long datetime = Bytes.littleEndianValue(this.header, this.headerOffset + 12, 4); - return decodeMsDosFormatDateTime(datetime); - } - - /** - * Decode MS-DOS Date Time details. See - * Microsoft's documentation for more details of the format. - * @param datetime the date and time - * @return the date and time as milliseconds since the epoch - */ - private long decodeMsDosFormatDateTime(long datetime) { - int year = getChronoValue(((datetime >> 25) & 0x7f) + 1980, ChronoField.YEAR); - int month = getChronoValue((datetime >> 21) & 0x0f, ChronoField.MONTH_OF_YEAR); - int day = getChronoValue((datetime >> 16) & 0x1f, ChronoField.DAY_OF_MONTH); - int hour = getChronoValue((datetime >> 11) & 0x1f, ChronoField.HOUR_OF_DAY); - int minute = getChronoValue((datetime >> 5) & 0x3f, ChronoField.MINUTE_OF_HOUR); - int second = getChronoValue((datetime << 1) & 0x3e, ChronoField.SECOND_OF_MINUTE); - return ZonedDateTime.of(year, month, day, hour, minute, second, 0, ZoneId.systemDefault()) - .toInstant() - .truncatedTo(ChronoUnit.SECONDS) - .toEpochMilli(); - } - - long getCrc() { - return Bytes.littleEndianValue(this.header, this.headerOffset + 16, 4); - } - - @Override - public long getCompressedSize() { - return Bytes.littleEndianValue(this.header, this.headerOffset + 20, 4); - } - - @Override - public long getSize() { - return Bytes.littleEndianValue(this.header, this.headerOffset + 24, 4); - } - - byte[] getExtra() { - return this.extra; - } - - boolean hasExtra() { - return this.extra.length > 0; - } - - AsciiBytes getComment() { - return this.comment; - } - - @Override - public long getLocalHeaderOffset() { - return this.localHeaderOffset; - } - - @Override - public CentralDirectoryFileHeader clone() { - byte[] header = new byte[46]; - System.arraycopy(this.header, this.headerOffset, header, 0, header.length); - return new CentralDirectoryFileHeader(header, 0, this.name, header, this.comment, this.localHeaderOffset); - } - - static CentralDirectoryFileHeader fromRandomAccessData(RandomAccessData data, long offset, JarEntryFilter filter) - throws IOException { - CentralDirectoryFileHeader fileHeader = new CentralDirectoryFileHeader(); - byte[] bytes = data.read(offset, 46); - fileHeader.load(bytes, 0, data, offset, filter); - return fileHeader; - } - - private static int getChronoValue(long value, ChronoField field) { - ValueRange range = field.range(); - return Math.toIntExact(Math.min(Math.max(value, range.getMinimum()), range.getMaximum())); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java deleted file mode 100644 index eff96a56e2cc..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryParser.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.jar; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import org.springframework.boot.loader.data.RandomAccessData; - -/** - * Parses the central directory from a JAR file. - * - * @author Phillip Webb - * @author Andy Wilkinson - * @see CentralDirectoryVisitor - */ -class CentralDirectoryParser { - - private static final int CENTRAL_DIRECTORY_HEADER_BASE_SIZE = 46; - - private final List visitors = new ArrayList<>(); - - T addVisitor(T visitor) { - this.visitors.add(visitor); - return visitor; - } - - /** - * Parse the source data, triggering {@link CentralDirectoryVisitor visitors}. - * @param data the source data - * @param skipPrefixBytes if prefix bytes should be skipped - * @return the actual archive data without any prefix bytes - * @throws IOException on error - */ - RandomAccessData parse(RandomAccessData data, boolean skipPrefixBytes) throws IOException { - CentralDirectoryEndRecord endRecord = new CentralDirectoryEndRecord(data); - if (skipPrefixBytes) { - data = getArchiveData(endRecord, data); - } - RandomAccessData centralDirectoryData = endRecord.getCentralDirectory(data); - visitStart(endRecord, centralDirectoryData); - parseEntries(endRecord, centralDirectoryData); - visitEnd(); - return data; - } - - private void parseEntries(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) - throws IOException { - byte[] bytes = centralDirectoryData.read(0, centralDirectoryData.getSize()); - CentralDirectoryFileHeader fileHeader = new CentralDirectoryFileHeader(); - int dataOffset = 0; - for (int i = 0; i < endRecord.getNumberOfRecords(); i++) { - fileHeader.load(bytes, dataOffset, null, 0, null); - visitFileHeader(dataOffset, fileHeader); - dataOffset += CENTRAL_DIRECTORY_HEADER_BASE_SIZE + fileHeader.getName().length() - + fileHeader.getComment().length() + fileHeader.getExtra().length; - } - } - - private RandomAccessData getArchiveData(CentralDirectoryEndRecord endRecord, RandomAccessData data) { - long offset = endRecord.getStartOfArchive(data); - if (offset == 0) { - return data; - } - return data.getSubsection(offset, data.getSize() - offset); - } - - private void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) { - for (CentralDirectoryVisitor visitor : this.visitors) { - visitor.visitStart(endRecord, centralDirectoryData); - } - } - - private void visitFileHeader(long dataOffset, CentralDirectoryFileHeader fileHeader) { - for (CentralDirectoryVisitor visitor : this.visitors) { - visitor.visitFileHeader(fileHeader, dataOffset); - } - } - - private void visitEnd() { - for (CentralDirectoryVisitor visitor : this.visitors) { - visitor.visitEnd(); - } - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java deleted file mode 100644 index 7e4134fe5649..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/FileHeader.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.jar; - -import java.util.zip.ZipEntry; - -/** - * A file header record that has been loaded from a Jar file. - * - * @author Phillip Webb - * @see JarEntry - * @see CentralDirectoryFileHeader - */ -interface FileHeader { - - /** - * Returns {@code true} if the header has the given name. - * @param name the name to test - * @param suffix an additional suffix (or {@code 0}) - * @return {@code true} if the header has the given name - */ - boolean hasName(CharSequence name, char suffix); - - /** - * Return the offset of the load file header within the archive data. - * @return the local header offset - */ - long getLocalHeaderOffset(); - - /** - * Return the compressed size of the entry. - * @return the compressed size. - */ - long getCompressedSize(); - - /** - * Return the uncompressed size of the entry. - * @return the uncompressed size. - */ - long getSize(); - - /** - * Return the method used to compress the data. - * @return the zip compression method - * @see ZipEntry#STORED - * @see ZipEntry#DEFLATED - */ - int getMethod(); - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java deleted file mode 100644 index 932dea654867..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java +++ /dev/null @@ -1,466 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.jar; - -import java.io.File; -import java.io.IOException; -import java.lang.ref.SoftReference; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLStreamHandler; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.regex.Pattern; - -/** - * {@link URLStreamHandler} for Spring Boot loader {@link JarFile}s. - * - * @author Phillip Webb - * @author Andy Wilkinson - * @since 1.0.0 - * @see JarFile#registerUrlProtocolHandler() - */ -public class Handler extends URLStreamHandler { - - // NOTE: in order to be found as a URL protocol handler, this class must be public, - // must be named Handler and must be in a package ending '.jar' - - private static final String JAR_PROTOCOL = "jar:"; - - private static final String FILE_PROTOCOL = "file:"; - - private static final String TOMCAT_WARFILE_PROTOCOL = "war:file:"; - - private static final String SEPARATOR = "!/"; - - private static final Pattern SEPARATOR_PATTERN = Pattern.compile(SEPARATOR, Pattern.LITERAL); - - private static final String CURRENT_DIR = "/./"; - - private static final Pattern CURRENT_DIR_PATTERN = Pattern.compile(CURRENT_DIR, Pattern.LITERAL); - - private static final String PARENT_DIR = "/../"; - - private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs"; - - private static final String[] FALLBACK_HANDLERS = { "sun.net.www.protocol.jar.Handler" }; - - private static URL jarContextUrl; - - private static SoftReference> rootFileCache; - - static { - rootFileCache = new SoftReference<>(null); - } - - private final JarFile jarFile; - - private URLStreamHandler fallbackHandler; - - public Handler() { - this(null); - } - - public Handler(JarFile jarFile) { - this.jarFile = jarFile; - } - - @Override - protected URLConnection openConnection(URL url) throws IOException { - if (this.jarFile != null && isUrlInJarFile(url, this.jarFile)) { - return JarURLConnection.get(url, this.jarFile); - } - try { - return JarURLConnection.get(url, getRootJarFileFromUrl(url)); - } - catch (Exception ex) { - return openFallbackConnection(url, ex); - } - } - - private boolean isUrlInJarFile(URL url, JarFile jarFile) throws MalformedURLException { - // Try the path first to save building a new url string each time - return url.getPath().startsWith(jarFile.getUrl().getPath()) - && url.toString().startsWith(jarFile.getUrlString()); - } - - private URLConnection openFallbackConnection(URL url, Exception reason) throws IOException { - try { - URLConnection connection = openFallbackTomcatConnection(url); - connection = (connection != null) ? connection : openFallbackContextConnection(url); - return (connection != null) ? connection : openFallbackHandlerConnection(url); - } - catch (Exception ex) { - if (reason instanceof IOException ioException) { - log(false, "Unable to open fallback handler", ex); - throw ioException; - } - log(true, "Unable to open fallback handler", ex); - if (reason instanceof RuntimeException runtimeException) { - throw runtimeException; - } - throw new IllegalStateException(reason); - } - } - - /** - * Attempt to open a Tomcat formatted 'jar:war:file:...' URL. This method allows us to - * use our own nested JAR support to open the content rather than the logic in - * {@code sun.net.www.protocol.jar.URLJarFile} which will extract the nested jar to - * the temp folder to that its content can be accessed. - * @param url the URL to open - * @return a {@link URLConnection} or {@code null} - */ - private URLConnection openFallbackTomcatConnection(URL url) { - String file = url.getFile(); - if (isTomcatWarUrl(file)) { - file = file.substring(TOMCAT_WARFILE_PROTOCOL.length()); - file = file.replaceFirst("\\*/", "!/"); - try { - URLConnection connection = openConnection(new URL("jar:file:" + file)); - connection.getInputStream().close(); - return connection; - } - catch (IOException ex) { - } - } - return null; - } - - private boolean isTomcatWarUrl(String file) { - if (file.startsWith(TOMCAT_WARFILE_PROTOCOL) || !file.contains("*/")) { - try { - URLConnection connection = new URL(file).openConnection(); - if (connection.getClass().getName().startsWith("org.apache.catalina")) { - return true; - } - } - catch (Exception ex) { - } - } - return false; - } - - /** - * Attempt to open a fallback connection by using a context URL captured before the - * jar handler was replaced with our own version. Since this method doesn't use - * reflection it won't trigger "illegal reflective access operation has occurred" - * warnings on Java 13+. - * @param url the URL to open - * @return a {@link URLConnection} or {@code null} - */ - private URLConnection openFallbackContextConnection(URL url) { - try { - if (jarContextUrl != null) { - return new URL(jarContextUrl, url.toExternalForm()).openConnection(); - } - } - catch (Exception ex) { - } - return null; - } - - /** - * Attempt to open a fallback connection by using reflection to access Java's default - * jar {@link URLStreamHandler}. - * @param url the URL to open - * @return the {@link URLConnection} - * @throws Exception if not connection could be opened - */ - private URLConnection openFallbackHandlerConnection(URL url) throws Exception { - URLStreamHandler fallbackHandler = getFallbackHandler(); - return new URL(null, url.toExternalForm(), fallbackHandler).openConnection(); - } - - private URLStreamHandler getFallbackHandler() { - if (this.fallbackHandler != null) { - return this.fallbackHandler; - } - for (String handlerClassName : FALLBACK_HANDLERS) { - try { - Class handlerClass = Class.forName(handlerClassName); - this.fallbackHandler = (URLStreamHandler) handlerClass.getDeclaredConstructor().newInstance(); - return this.fallbackHandler; - } - catch (Exception ex) { - // Ignore - } - } - throw new IllegalStateException("Unable to find fallback handler"); - } - - private void log(boolean warning, String message, Exception cause) { - try { - Level level = warning ? Level.WARNING : Level.FINEST; - Logger.getLogger(getClass().getName()).log(level, message, cause); - } - catch (Exception ex) { - if (warning) { - System.err.println("WARNING: " + message); - } - } - } - - @Override - protected void parseURL(URL context, String spec, int start, int limit) { - if (spec.regionMatches(true, 0, JAR_PROTOCOL, 0, JAR_PROTOCOL.length())) { - setFile(context, getFileFromSpec(spec.substring(start, limit))); - } - else { - setFile(context, getFileFromContext(context, spec.substring(start, limit))); - } - } - - private String getFileFromSpec(String spec) { - int separatorIndex = spec.lastIndexOf("!/"); - if (separatorIndex == -1) { - throw new IllegalArgumentException("No !/ in spec '" + spec + "'"); - } - try { - new URL(spec.substring(0, separatorIndex)); - return spec; - } - catch (MalformedURLException ex) { - throw new IllegalArgumentException("Invalid spec URL '" + spec + "'", ex); - } - } - - private String getFileFromContext(URL context, String spec) { - String file = context.getFile(); - if (spec.startsWith("/")) { - return trimToJarRoot(file) + SEPARATOR + spec.substring(1); - } - if (file.endsWith("/")) { - return file + spec; - } - int lastSlashIndex = file.lastIndexOf('/'); - if (lastSlashIndex == -1) { - throw new IllegalArgumentException("No / found in context URL's file '" + file + "'"); - } - return file.substring(0, lastSlashIndex + 1) + spec; - } - - private String trimToJarRoot(String file) { - int lastSeparatorIndex = file.lastIndexOf(SEPARATOR); - if (lastSeparatorIndex == -1) { - throw new IllegalArgumentException("No !/ found in context URL's file '" + file + "'"); - } - return file.substring(0, lastSeparatorIndex); - } - - private void setFile(URL context, String file) { - String path = normalize(file); - String query = null; - int queryIndex = path.lastIndexOf('?'); - if (queryIndex != -1) { - query = path.substring(queryIndex + 1); - path = path.substring(0, queryIndex); - } - setURL(context, JAR_PROTOCOL, null, -1, null, null, path, query, context.getRef()); - } - - private String normalize(String file) { - if (!file.contains(CURRENT_DIR) && !file.contains(PARENT_DIR)) { - return file; - } - int afterLastSeparatorIndex = file.lastIndexOf(SEPARATOR) + SEPARATOR.length(); - String afterSeparator = file.substring(afterLastSeparatorIndex); - afterSeparator = replaceParentDir(afterSeparator); - afterSeparator = replaceCurrentDir(afterSeparator); - return file.substring(0, afterLastSeparatorIndex) + afterSeparator; - } - - private String replaceParentDir(String file) { - int parentDirIndex; - while ((parentDirIndex = file.indexOf(PARENT_DIR)) >= 0) { - int precedingSlashIndex = file.lastIndexOf('/', parentDirIndex - 1); - if (precedingSlashIndex >= 0) { - file = file.substring(0, precedingSlashIndex) + file.substring(parentDirIndex + 3); - } - else { - file = file.substring(parentDirIndex + 4); - } - } - return file; - } - - private String replaceCurrentDir(String file) { - return CURRENT_DIR_PATTERN.matcher(file).replaceAll("/"); - } - - @Override - protected int hashCode(URL u) { - return hashCode(u.getProtocol(), u.getFile()); - } - - private int hashCode(String protocol, String file) { - int result = (protocol != null) ? protocol.hashCode() : 0; - int separatorIndex = file.indexOf(SEPARATOR); - if (separatorIndex == -1) { - return result + file.hashCode(); - } - String source = file.substring(0, separatorIndex); - String entry = canonicalize(file.substring(separatorIndex + 2)); - try { - result += new URL(source).hashCode(); - } - catch (MalformedURLException ex) { - result += source.hashCode(); - } - result += entry.hashCode(); - return result; - } - - @Override - protected boolean sameFile(URL u1, URL u2) { - if (!u1.getProtocol().equals("jar") || !u2.getProtocol().equals("jar")) { - return false; - } - int separator1 = u1.getFile().indexOf(SEPARATOR); - int separator2 = u2.getFile().indexOf(SEPARATOR); - if (separator1 == -1 || separator2 == -1) { - return super.sameFile(u1, u2); - } - String nested1 = u1.getFile().substring(separator1 + SEPARATOR.length()); - String nested2 = u2.getFile().substring(separator2 + SEPARATOR.length()); - if (!nested1.equals(nested2)) { - String canonical1 = canonicalize(nested1); - String canonical2 = canonicalize(nested2); - if (!canonical1.equals(canonical2)) { - return false; - } - } - String root1 = u1.getFile().substring(0, separator1); - String root2 = u2.getFile().substring(0, separator2); - try { - return super.sameFile(new URL(root1), new URL(root2)); - } - catch (MalformedURLException ex) { - // Continue - } - return super.sameFile(u1, u2); - } - - private String canonicalize(String path) { - return SEPARATOR_PATTERN.matcher(path).replaceAll("/"); - } - - public JarFile getRootJarFileFromUrl(URL url) throws IOException { - String spec = url.getFile(); - int separatorIndex = spec.indexOf(SEPARATOR); - if (separatorIndex == -1) { - throw new MalformedURLException("Jar URL does not contain !/ separator"); - } - String name = spec.substring(0, separatorIndex); - return getRootJarFile(name); - } - - private JarFile getRootJarFile(String name) throws IOException { - try { - if (!name.startsWith(FILE_PROTOCOL)) { - throw new IllegalStateException("Not a file URL"); - } - File file = new File(URI.create(name)); - Map cache = rootFileCache.get(); - JarFile result = (cache != null) ? cache.get(file) : null; - if (result == null) { - result = new JarFile(file); - addToRootFileCache(file, result); - } - return result; - } - catch (Exception ex) { - throw new IOException("Unable to open root Jar file '" + name + "'", ex); - } - } - - /** - * Add the given {@link JarFile} to the root file cache. - * @param sourceFile the source file to add - * @param jarFile the jar file. - */ - static void addToRootFileCache(File sourceFile, JarFile jarFile) { - Map cache = rootFileCache.get(); - if (cache == null) { - cache = new ConcurrentHashMap<>(); - rootFileCache = new SoftReference<>(cache); - } - cache.put(sourceFile, jarFile); - } - - /** - * If possible, capture a URL that is configured with the original jar handler so that - * we can use it as a fallback context later. We can only do this if we know that we - * can reset the handlers after. - */ - static void captureJarContextUrl() { - if (canResetCachedUrlHandlers()) { - String handlers = System.getProperty(PROTOCOL_HANDLER); - try { - System.clearProperty(PROTOCOL_HANDLER); - try { - resetCachedUrlHandlers(); - jarContextUrl = new URL("jar:file:context.jar!/"); - URLConnection connection = jarContextUrl.openConnection(); - if (connection instanceof JarURLConnection) { - jarContextUrl = null; - } - } - catch (Exception ex) { - } - } - finally { - if (handlers == null) { - System.clearProperty(PROTOCOL_HANDLER); - } - else { - System.setProperty(PROTOCOL_HANDLER, handlers); - } - } - resetCachedUrlHandlers(); - } - } - - private static boolean canResetCachedUrlHandlers() { - try { - resetCachedUrlHandlers(); - return true; - } - catch (Error ex) { - return false; - } - } - - private static void resetCachedUrlHandlers() { - URL.setURLStreamHandlerFactory(null); - } - - /** - * Set if a generic static exception can be thrown when a URL cannot be connected. - * This optimization is used during class loading to save creating lots of exceptions - * which are then swallowed. - * @param useFastConnectionExceptions if fast connection exceptions can be used. - */ - public static void setUseFastConnectionExceptions(boolean useFastConnectionExceptions) { - JarURLConnection.setUseFastExceptions(useFastConnectionExceptions); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java deleted file mode 100644 index 8f54dc3070df..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntry.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.jar; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.security.CodeSigner; -import java.security.cert.Certificate; -import java.util.jar.Attributes; -import java.util.jar.Manifest; - -/** - * Extended variant of {@link java.util.jar.JarEntry} returned by {@link JarFile}s. - * - * @author Phillip Webb - * @author Andy Wilkinson - */ -class JarEntry extends java.util.jar.JarEntry implements FileHeader { - - private final int index; - - private final AsciiBytes name; - - private final AsciiBytes headerName; - - private final JarFile jarFile; - - private final long localHeaderOffset; - - private volatile JarEntryCertification certification; - - JarEntry(JarFile jarFile, int index, CentralDirectoryFileHeader header, AsciiBytes nameAlias) { - super((nameAlias != null) ? nameAlias.toString() : header.getName().toString()); - this.index = index; - this.name = (nameAlias != null) ? nameAlias : header.getName(); - this.headerName = header.getName(); - this.jarFile = jarFile; - this.localHeaderOffset = header.getLocalHeaderOffset(); - setCompressedSize(header.getCompressedSize()); - setMethod(header.getMethod()); - setCrc(header.getCrc()); - setComment(header.getComment().toString()); - setSize(header.getSize()); - setTime(header.getTime()); - if (header.hasExtra()) { - setExtra(header.getExtra()); - } - } - - int getIndex() { - return this.index; - } - - AsciiBytes getAsciiBytesName() { - return this.name; - } - - @Override - public boolean hasName(CharSequence name, char suffix) { - return this.headerName.matches(name, suffix); - } - - /** - * Return a {@link URL} for this {@link JarEntry}. - * @return the URL for the entry - * @throws MalformedURLException if the URL is not valid - */ - URL getUrl() throws MalformedURLException { - return new URL(this.jarFile.getUrl(), getName()); - } - - @Override - public Attributes getAttributes() throws IOException { - Manifest manifest = this.jarFile.getManifest(); - return (manifest != null) ? manifest.getAttributes(getName()) : null; - } - - @Override - public Certificate[] getCertificates() { - return getCertification().getCertificates(); - } - - @Override - public CodeSigner[] getCodeSigners() { - return getCertification().getCodeSigners(); - } - - private JarEntryCertification getCertification() { - if (!this.jarFile.isSigned()) { - return JarEntryCertification.NONE; - } - JarEntryCertification certification = this.certification; - if (certification == null) { - certification = this.jarFile.getCertification(this); - this.certification = certification; - } - return certification; - } - - @Override - public long getLocalHeaderOffset() { - return this.localHeaderOffset; - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java deleted file mode 100644 index ffd629e09428..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryCertification.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.jar; - -import java.security.CodeSigner; -import java.security.cert.Certificate; - -/** - * {@link Certificate} and {@link CodeSigner} details for a {@link JarEntry} from a signed - * {@link JarFile}. - * - * @author Phillip Webb - */ -class JarEntryCertification { - - static final JarEntryCertification NONE = new JarEntryCertification(null, null); - - private final Certificate[] certificates; - - private final CodeSigner[] codeSigners; - - JarEntryCertification(Certificate[] certificates, CodeSigner[] codeSigners) { - this.certificates = certificates; - this.codeSigners = codeSigners; - } - - Certificate[] getCertificates() { - return (this.certificates != null) ? this.certificates.clone() : null; - } - - CodeSigner[] getCodeSigners() { - return (this.codeSigners != null) ? this.codeSigners.clone() : null; - } - - static JarEntryCertification from(java.util.jar.JarEntry certifiedEntry) { - Certificate[] certificates = (certifiedEntry != null) ? certifiedEntry.getCertificates() : null; - CodeSigner[] codeSigners = (certifiedEntry != null) ? certifiedEntry.getCodeSigners() : null; - if (certificates == null && codeSigners == null) { - return NONE; - } - return new JarEntryCertification(certificates, codeSigners); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java deleted file mode 100644 index 6e548048dbf0..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFile.java +++ /dev/null @@ -1,475 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.jar; - -import java.io.File; -import java.io.FilePermission; -import java.io.IOException; -import java.io.InputStream; -import java.lang.ref.SoftReference; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLStreamHandler; -import java.net.URLStreamHandlerFactory; -import java.security.Permission; -import java.util.Enumeration; -import java.util.Iterator; -import java.util.Spliterator; -import java.util.Spliterators; -import java.util.function.Supplier; -import java.util.jar.Manifest; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; -import java.util.zip.ZipEntry; - -import org.springframework.boot.loader.data.RandomAccessData; -import org.springframework.boot.loader.data.RandomAccessDataFile; - -/** - * Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but - * offers the following additional functionality. - *
      - *
    • A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based - * on any directory entry.
    • - *
    • A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for - * embedded JAR files (as long as their entry is not compressed).
    • - *
    - * - * @author Phillip Webb - * @author Andy Wilkinson - * @since 1.0.0 - */ -public class JarFile extends AbstractJarFile implements Iterable { - - private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; - - private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs"; - - private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader"; - - private static final AsciiBytes META_INF = new AsciiBytes("META-INF/"); - - private static final AsciiBytes SIGNATURE_FILE_EXTENSION = new AsciiBytes(".SF"); - - private static final String READ_ACTION = "read"; - - private final RandomAccessDataFile rootFile; - - private final String pathFromRoot; - - private final RandomAccessData data; - - private final JarFileType type; - - private URL url; - - private String urlString; - - private final JarFileEntries entries; - - private final Supplier manifestSupplier; - - private SoftReference manifest; - - private boolean signed; - - private String comment; - - private volatile boolean closed; - - private volatile JarFileWrapper wrapper; - - /** - * Create a new {@link JarFile} backed by the specified file. - * @param file the root jar file - * @throws IOException if the file cannot be read - */ - public JarFile(File file) throws IOException { - this(new RandomAccessDataFile(file)); - } - - /** - * Create a new {@link JarFile} backed by the specified file. - * @param file the root jar file - * @throws IOException if the file cannot be read - */ - JarFile(RandomAccessDataFile file) throws IOException { - this(file, "", file, JarFileType.DIRECT); - } - - /** - * Private constructor used to create a new {@link JarFile} either directly or from a - * nested entry. - * @param rootFile the root jar file - * @param pathFromRoot the name of this file - * @param data the underlying data - * @param type the type of the jar file - * @throws IOException if the file cannot be read - */ - private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, RandomAccessData data, JarFileType type) - throws IOException { - this(rootFile, pathFromRoot, data, null, type, null); - } - - private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, RandomAccessData data, JarEntryFilter filter, - JarFileType type, Supplier manifestSupplier) throws IOException { - super(rootFile.getFile()); - super.close(); - this.rootFile = rootFile; - this.pathFromRoot = pathFromRoot; - CentralDirectoryParser parser = new CentralDirectoryParser(); - this.entries = parser.addVisitor(new JarFileEntries(this, filter)); - this.type = type; - parser.addVisitor(centralDirectoryVisitor()); - try { - this.data = parser.parse(data, filter == null); - } - catch (RuntimeException ex) { - try { - this.rootFile.close(); - super.close(); - } - catch (IOException ioex) { - } - throw ex; - } - this.manifestSupplier = (manifestSupplier != null) ? manifestSupplier : () -> { - try (InputStream inputStream = getInputStream(MANIFEST_NAME)) { - if (inputStream == null) { - return null; - } - return new Manifest(inputStream); - } - catch (IOException ex) { - throw new RuntimeException(ex); - } - }; - } - - private CentralDirectoryVisitor centralDirectoryVisitor() { - return new CentralDirectoryVisitor() { - - @Override - public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) { - JarFile.this.comment = endRecord.getComment(); - } - - @Override - public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) { - AsciiBytes name = fileHeader.getName(); - if (name.startsWith(META_INF) && name.endsWith(SIGNATURE_FILE_EXTENSION)) { - JarFile.this.signed = true; - } - } - - @Override - public void visitEnd() { - } - - }; - } - - JarFileWrapper getWrapper() throws IOException { - JarFileWrapper wrapper = this.wrapper; - if (wrapper == null) { - wrapper = new JarFileWrapper(this); - this.wrapper = wrapper; - } - return wrapper; - } - - @Override - Permission getPermission() { - return new FilePermission(this.rootFile.getFile().getPath(), READ_ACTION); - } - - protected final RandomAccessDataFile getRootJarFile() { - return this.rootFile; - } - - RandomAccessData getData() { - return this.data; - } - - @Override - public Manifest getManifest() throws IOException { - Manifest manifest = (this.manifest != null) ? this.manifest.get() : null; - if (manifest == null) { - try { - manifest = this.manifestSupplier.get(); - } - catch (RuntimeException ex) { - throw new IOException(ex); - } - this.manifest = new SoftReference<>(manifest); - } - return manifest; - } - - @Override - public Enumeration entries() { - return new JarEntryEnumeration(this.entries.iterator()); - } - - @Override - public Stream stream() { - Spliterator spliterator = Spliterators.spliterator(iterator(), size(), - Spliterator.ORDERED | Spliterator.DISTINCT | Spliterator.IMMUTABLE | Spliterator.NONNULL); - return StreamSupport.stream(spliterator, false); - } - - /** - * Return an iterator for the contained entries. - * @since 2.3.0 - * @see java.lang.Iterable#iterator() - */ - @Override - @SuppressWarnings({ "unchecked", "rawtypes" }) - public Iterator iterator() { - return (Iterator) this.entries.iterator(this::ensureOpen); - } - - public JarEntry getJarEntry(CharSequence name) { - return this.entries.getEntry(name); - } - - @Override - public JarEntry getJarEntry(String name) { - return (JarEntry) getEntry(name); - } - - public boolean containsEntry(String name) { - return this.entries.containsEntry(name); - } - - @Override - public ZipEntry getEntry(String name) { - ensureOpen(); - return this.entries.getEntry(name); - } - - @Override - InputStream getInputStream() throws IOException { - return this.data.getInputStream(); - } - - @Override - public synchronized InputStream getInputStream(ZipEntry entry) throws IOException { - ensureOpen(); - if (entry instanceof JarEntry jarEntry) { - return this.entries.getInputStream(jarEntry); - } - return getInputStream((entry != null) ? entry.getName() : null); - } - - InputStream getInputStream(String name) throws IOException { - return this.entries.getInputStream(name); - } - - /** - * Return a nested {@link JarFile} loaded from the specified entry. - * @param entry the zip entry - * @return a {@link JarFile} for the entry - * @throws IOException if the nested jar file cannot be read - */ - public synchronized JarFile getNestedJarFile(ZipEntry entry) throws IOException { - return getNestedJarFile((JarEntry) entry); - } - - /** - * Return a nested {@link JarFile} loaded from the specified entry. - * @param entry the zip entry - * @return a {@link JarFile} for the entry - * @throws IOException if the nested jar file cannot be read - */ - public synchronized JarFile getNestedJarFile(JarEntry entry) throws IOException { - try { - return createJarFileFromEntry(entry); - } - catch (Exception ex) { - throw new IOException("Unable to open nested jar file '" + entry.getName() + "'", ex); - } - } - - private JarFile createJarFileFromEntry(JarEntry entry) throws IOException { - if (entry.isDirectory()) { - return createJarFileFromDirectoryEntry(entry); - } - return createJarFileFromFileEntry(entry); - } - - private JarFile createJarFileFromDirectoryEntry(JarEntry entry) throws IOException { - AsciiBytes name = entry.getAsciiBytesName(); - JarEntryFilter filter = (candidate) -> { - if (candidate.startsWith(name) && !candidate.equals(name)) { - return candidate.substring(name.length()); - } - return null; - }; - return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName().substring(0, name.length() - 1), - this.data, filter, JarFileType.NESTED_DIRECTORY, this.manifestSupplier); - } - - private JarFile createJarFileFromFileEntry(JarEntry entry) throws IOException { - if (entry.getMethod() != ZipEntry.STORED) { - throw new IllegalStateException( - "Unable to open nested entry '" + entry.getName() + "'. It has been compressed and nested " - + "jar files must be stored without compression. Please check the " - + "mechanism used to create your executable jar file"); - } - RandomAccessData entryData = this.entries.getEntryData(entry.getName()); - return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName(), entryData, - JarFileType.NESTED_JAR); - } - - @Override - public String getComment() { - ensureOpen(); - return this.comment; - } - - @Override - public int size() { - ensureOpen(); - return this.entries.getSize(); - } - - @Override - public void close() throws IOException { - if (this.closed) { - return; - } - super.close(); - if (this.type == JarFileType.DIRECT) { - this.rootFile.close(); - } - this.closed = true; - } - - private void ensureOpen() { - if (this.closed) { - throw new IllegalStateException("zip file closed"); - } - } - - boolean isClosed() { - return this.closed; - } - - String getUrlString() throws MalformedURLException { - if (this.urlString == null) { - this.urlString = getUrl().toString(); - } - return this.urlString; - } - - @Override - public URL getUrl() throws MalformedURLException { - if (this.url == null) { - String file = this.rootFile.getFile().toURI() + this.pathFromRoot + "!/"; - file = file.replace("file:////", "file://"); // Fix UNC paths - this.url = new URL("jar", "", -1, file, new Handler(this)); - } - return this.url; - } - - @Override - public String toString() { - return getName(); - } - - @Override - public String getName() { - return this.rootFile.getFile() + this.pathFromRoot; - } - - boolean isSigned() { - return this.signed; - } - - JarEntryCertification getCertification(JarEntry entry) { - try { - return this.entries.getCertification(entry); - } - catch (IOException ex) { - throw new IllegalStateException(ex); - } - } - - public void clearCache() { - this.entries.clearCache(); - } - - protected String getPathFromRoot() { - return this.pathFromRoot; - } - - @Override - JarFileType getType() { - return this.type; - } - - /** - * Register a {@literal 'java.protocol.handler.pkgs'} property so that a - * {@link URLStreamHandler} will be located to deal with jar URLs. - */ - public static void registerUrlProtocolHandler() { - Handler.captureJarContextUrl(); - String handlers = System.getProperty(PROTOCOL_HANDLER, ""); - System.setProperty(PROTOCOL_HANDLER, - ((handlers == null || handlers.isEmpty()) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE)); - resetCachedUrlHandlers(); - } - - /** - * Reset any cached handlers just in case a jar protocol has already been used. We - * reset the handler by trying to set a null {@link URLStreamHandlerFactory} which - * should have no effect other than clearing the handlers cache. - */ - private static void resetCachedUrlHandlers() { - try { - URL.setURLStreamHandlerFactory(null); - } - catch (Error ex) { - // Ignore - } - } - - /** - * An {@link Enumeration} on {@linkplain java.util.jar.JarEntry jar entries}. - */ - private static class JarEntryEnumeration implements Enumeration { - - private final Iterator iterator; - - JarEntryEnumeration(Iterator iterator) { - this.iterator = iterator; - } - - @Override - public boolean hasMoreElements() { - return this.iterator.hasNext(); - } - - @Override - public java.util.jar.JarEntry nextElement() { - return this.iterator.next(); - } - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java deleted file mode 100644 index d151c8d80a85..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileEntries.java +++ /dev/null @@ -1,491 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.jar; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.jar.Attributes; -import java.util.jar.Attributes.Name; -import java.util.jar.JarInputStream; -import java.util.jar.Manifest; -import java.util.zip.ZipEntry; - -import org.springframework.boot.loader.data.RandomAccessData; - -/** - * Provides access to entries from a {@link JarFile}. In order to reduce memory - * consumption entry details are stored using arrays. The {@code hashCodes} array stores - * the hash code of the entry name, the {@code centralDirectoryOffsets} provides the - * offset to the central directory record and {@code positions} provides the original - * order position of the entry. The arrays are stored in hashCode order so that a binary - * search can be used to find a name. - *

    - * A typical Spring Boot application will have somewhere in the region of 10,500 entries - * which should consume about 122K. - * - * @author Phillip Webb - * @author Andy Wilkinson - */ -class JarFileEntries implements CentralDirectoryVisitor, Iterable { - - private static final Runnable NO_VALIDATION = () -> { - }; - - private static final String META_INF_PREFIX = "META-INF/"; - - private static final Name MULTI_RELEASE = new Name("Multi-Release"); - - private static final int BASE_VERSION = 8; - - private static final int RUNTIME_VERSION = Runtime.version().feature(); - - private static final long LOCAL_FILE_HEADER_SIZE = 30; - - private static final char SLASH = '/'; - - private static final char NO_SUFFIX = 0; - - protected static final int ENTRY_CACHE_SIZE = 25; - - private final JarFile jarFile; - - private final JarEntryFilter filter; - - private RandomAccessData centralDirectoryData; - - private int size; - - private int[] hashCodes; - - private Offsets centralDirectoryOffsets; - - private int[] positions; - - private Boolean multiReleaseJar; - - private JarEntryCertification[] certifications; - - private final Map entriesCache = Collections - .synchronizedMap(new LinkedHashMap<>(16, 0.75f, true) { - - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() >= ENTRY_CACHE_SIZE; - } - - }); - - JarFileEntries(JarFile jarFile, JarEntryFilter filter) { - this.jarFile = jarFile; - this.filter = filter; - } - - @Override - public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) { - int maxSize = endRecord.getNumberOfRecords(); - this.centralDirectoryData = centralDirectoryData; - this.hashCodes = new int[maxSize]; - this.centralDirectoryOffsets = Offsets.from(endRecord); - this.positions = new int[maxSize]; - } - - @Override - public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) { - AsciiBytes name = applyFilter(fileHeader.getName()); - if (name != null) { - add(name, dataOffset); - } - } - - private void add(AsciiBytes name, long dataOffset) { - this.hashCodes[this.size] = name.hashCode(); - this.centralDirectoryOffsets.set(this.size, dataOffset); - this.positions[this.size] = this.size; - this.size++; - } - - @Override - public void visitEnd() { - sort(0, this.size - 1); - int[] positions = this.positions; - this.positions = new int[positions.length]; - for (int i = 0; i < this.size; i++) { - this.positions[positions[i]] = i; - } - } - - int getSize() { - return this.size; - } - - private void sort(int left, int right) { - // Quick sort algorithm, uses hashCodes as the source but sorts all arrays - if (left < right) { - int pivot = this.hashCodes[left + (right - left) / 2]; - int i = left; - int j = right; - while (i <= j) { - while (this.hashCodes[i] < pivot) { - i++; - } - while (this.hashCodes[j] > pivot) { - j--; - } - if (i <= j) { - swap(i, j); - i++; - j--; - } - } - if (left < j) { - sort(left, j); - } - if (right > i) { - sort(i, right); - } - } - } - - private void swap(int i, int j) { - swap(this.hashCodes, i, j); - this.centralDirectoryOffsets.swap(i, j); - swap(this.positions, i, j); - } - - @Override - public Iterator iterator() { - return new EntryIterator(NO_VALIDATION); - } - - Iterator iterator(Runnable validator) { - return new EntryIterator(validator); - } - - boolean containsEntry(CharSequence name) { - return getEntry(name, FileHeader.class, true) != null; - } - - JarEntry getEntry(CharSequence name) { - return getEntry(name, JarEntry.class, true); - } - - InputStream getInputStream(String name) throws IOException { - FileHeader entry = getEntry(name, FileHeader.class, false); - return getInputStream(entry); - } - - InputStream getInputStream(FileHeader entry) throws IOException { - if (entry == null) { - return null; - } - InputStream inputStream = getEntryData(entry).getInputStream(); - if (entry.getMethod() == ZipEntry.DEFLATED) { - inputStream = new ZipInflaterInputStream(inputStream, (int) entry.getSize()); - } - return inputStream; - } - - RandomAccessData getEntryData(String name) throws IOException { - FileHeader entry = getEntry(name, FileHeader.class, false); - if (entry == null) { - return null; - } - return getEntryData(entry); - } - - private RandomAccessData getEntryData(FileHeader entry) throws IOException { - // aspectjrt-1.7.4.jar has a different ext bytes length in the - // local directory to the central directory. We need to re-read - // here to skip them - RandomAccessData data = this.jarFile.getData(); - byte[] localHeader = data.read(entry.getLocalHeaderOffset(), LOCAL_FILE_HEADER_SIZE); - long nameLength = Bytes.littleEndianValue(localHeader, 26, 2); - long extraLength = Bytes.littleEndianValue(localHeader, 28, 2); - return data.getSubsection(entry.getLocalHeaderOffset() + LOCAL_FILE_HEADER_SIZE + nameLength + extraLength, - entry.getCompressedSize()); - } - - private T getEntry(CharSequence name, Class type, boolean cacheEntry) { - T entry = doGetEntry(name, type, cacheEntry, null); - if (!isMetaInfEntry(name) && isMultiReleaseJar()) { - int version = RUNTIME_VERSION; - AsciiBytes nameAlias = (entry instanceof JarEntry jarEntry) ? jarEntry.getAsciiBytesName() - : new AsciiBytes(name.toString()); - while (version > BASE_VERSION) { - T versionedEntry = doGetEntry("META-INF/versions/" + version + "/" + name, type, cacheEntry, nameAlias); - if (versionedEntry != null) { - return versionedEntry; - } - version--; - } - } - return entry; - } - - private boolean isMetaInfEntry(CharSequence name) { - return name.toString().startsWith(META_INF_PREFIX); - } - - private boolean isMultiReleaseJar() { - Boolean multiRelease = this.multiReleaseJar; - if (multiRelease != null) { - return multiRelease; - } - try { - Manifest manifest = this.jarFile.getManifest(); - if (manifest == null) { - multiRelease = false; - } - else { - Attributes attributes = manifest.getMainAttributes(); - multiRelease = attributes.containsKey(MULTI_RELEASE); - } - } - catch (IOException ex) { - multiRelease = false; - } - this.multiReleaseJar = multiRelease; - return multiRelease; - } - - private T doGetEntry(CharSequence name, Class type, boolean cacheEntry, - AsciiBytes nameAlias) { - int hashCode = AsciiBytes.hashCode(name); - T entry = getEntry(hashCode, name, NO_SUFFIX, type, cacheEntry, nameAlias); - if (entry == null) { - hashCode = AsciiBytes.hashCode(hashCode, SLASH); - entry = getEntry(hashCode, name, SLASH, type, cacheEntry, nameAlias); - } - return entry; - } - - private T getEntry(int hashCode, CharSequence name, char suffix, Class type, - boolean cacheEntry, AsciiBytes nameAlias) { - int index = getFirstIndex(hashCode); - while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) { - T entry = getEntry(index, type, cacheEntry, nameAlias); - if (entry.hasName(name, suffix)) { - return entry; - } - index++; - } - return null; - } - - @SuppressWarnings("unchecked") - private T getEntry(int index, Class type, boolean cacheEntry, AsciiBytes nameAlias) { - try { - long offset = this.centralDirectoryOffsets.get(index); - FileHeader cached = this.entriesCache.get(index); - FileHeader entry = (cached != null) ? cached - : CentralDirectoryFileHeader.fromRandomAccessData(this.centralDirectoryData, offset, this.filter); - if (CentralDirectoryFileHeader.class.equals(entry.getClass()) && type.equals(JarEntry.class)) { - entry = new JarEntry(this.jarFile, index, (CentralDirectoryFileHeader) entry, nameAlias); - } - if (cacheEntry && cached != entry) { - this.entriesCache.put(index, entry); - } - return (T) entry; - } - catch (IOException ex) { - throw new IllegalStateException(ex); - } - } - - private int getFirstIndex(int hashCode) { - int index = Arrays.binarySearch(this.hashCodes, 0, this.size, hashCode); - if (index < 0) { - return -1; - } - while (index > 0 && this.hashCodes[index - 1] == hashCode) { - index--; - } - return index; - } - - void clearCache() { - this.entriesCache.clear(); - } - - private AsciiBytes applyFilter(AsciiBytes name) { - return (this.filter != null) ? this.filter.apply(name) : name; - } - - JarEntryCertification getCertification(JarEntry entry) throws IOException { - JarEntryCertification[] certifications = this.certifications; - if (certifications == null) { - certifications = new JarEntryCertification[this.size]; - // We fall back to use JarInputStream to obtain the certs. This isn't that - // fast, but hopefully doesn't happen too often. - try (JarInputStream certifiedJarStream = new JarInputStream(this.jarFile.getData().getInputStream())) { - java.util.jar.JarEntry certifiedEntry; - while ((certifiedEntry = certifiedJarStream.getNextJarEntry()) != null) { - // Entry must be closed to trigger a read and set entry certificates - certifiedJarStream.closeEntry(); - int index = getEntryIndex(certifiedEntry.getName()); - if (index != -1) { - certifications[index] = JarEntryCertification.from(certifiedEntry); - } - } - } - this.certifications = certifications; - } - JarEntryCertification certification = certifications[entry.getIndex()]; - return (certification != null) ? certification : JarEntryCertification.NONE; - } - - private int getEntryIndex(CharSequence name) { - int hashCode = AsciiBytes.hashCode(name); - int index = getFirstIndex(hashCode); - while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) { - FileHeader candidate = getEntry(index, FileHeader.class, false, null); - if (candidate.hasName(name, NO_SUFFIX)) { - return index; - } - index++; - } - return -1; - } - - private static void swap(int[] array, int i, int j) { - int temp = array[i]; - array[i] = array[j]; - array[j] = temp; - } - - private static void swap(long[] array, int i, int j) { - long temp = array[i]; - array[i] = array[j]; - array[j] = temp; - } - - /** - * Iterator for contained entries. - */ - private final class EntryIterator implements Iterator { - - private final Runnable validator; - - private int index = 0; - - private EntryIterator(Runnable validator) { - this.validator = validator; - validator.run(); - } - - @Override - public boolean hasNext() { - this.validator.run(); - return this.index < JarFileEntries.this.size; - } - - @Override - public JarEntry next() { - this.validator.run(); - if (!hasNext()) { - throw new NoSuchElementException(); - } - int entryIndex = JarFileEntries.this.positions[this.index]; - this.index++; - return getEntry(entryIndex, JarEntry.class, false, null); - } - - } - - /** - * Interface to manage offsets to central directory records. Regular zip files are - * backed by an {@code int[]} based implementation, Zip64 files are backed by a - * {@code long[]} and will consume more memory. - */ - private interface Offsets { - - void set(int index, long value); - - long get(int index); - - void swap(int i, int j); - - static Offsets from(CentralDirectoryEndRecord endRecord) { - int size = endRecord.getNumberOfRecords(); - return endRecord.isZip64() ? new Zip64Offsets(size) : new ZipOffsets(size); - } - - } - - /** - * {@link Offsets} implementation for regular zip files. - */ - private static final class ZipOffsets implements Offsets { - - private final int[] offsets; - - private ZipOffsets(int size) { - this.offsets = new int[size]; - } - - @Override - public void swap(int i, int j) { - JarFileEntries.swap(this.offsets, i, j); - } - - @Override - public void set(int index, long value) { - this.offsets[index] = (int) value; - } - - @Override - public long get(int index) { - return this.offsets[index]; - } - - } - - /** - * {@link Offsets} implementation for zip64 files. - */ - private static final class Zip64Offsets implements Offsets { - - private final long[] offsets; - - private Zip64Offsets(int size) { - this.offsets = new long[size]; - } - - @Override - public void swap(int i, int j) { - JarFileEntries.swap(this.offsets, i, j); - } - - @Override - public void set(int index, long value) { - this.offsets[index] = value; - } - - @Override - public long get(int index) { - return this.offsets[index]; - } - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java deleted file mode 100644 index b65358947ad1..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarFileWrapper.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.jar; - -import java.io.IOException; -import java.io.InputStream; -import java.net.MalformedURLException; -import java.net.URL; -import java.security.Permission; -import java.util.Enumeration; -import java.util.jar.JarEntry; -import java.util.jar.Manifest; -import java.util.stream.Stream; -import java.util.zip.ZipEntry; - -/** - * A wrapper used to create a copy of a {@link JarFile} so that it can be safely closed - * without closing the original. - * - * @author Phillip Webb - */ -class JarFileWrapper extends AbstractJarFile { - - private final JarFile parent; - - JarFileWrapper(JarFile parent) throws IOException { - super(parent.getRootJarFile().getFile()); - this.parent = parent; - super.close(); - } - - @Override - URL getUrl() throws MalformedURLException { - return this.parent.getUrl(); - } - - @Override - JarFileType getType() { - return this.parent.getType(); - } - - @Override - Permission getPermission() { - return this.parent.getPermission(); - } - - @Override - public Manifest getManifest() throws IOException { - return this.parent.getManifest(); - } - - @Override - public Enumeration entries() { - return this.parent.entries(); - } - - @Override - public Stream stream() { - return this.parent.stream(); - } - - @Override - public JarEntry getJarEntry(String name) { - return this.parent.getJarEntry(name); - } - - @Override - public ZipEntry getEntry(String name) { - return this.parent.getEntry(name); - } - - @Override - InputStream getInputStream() throws IOException { - return this.parent.getInputStream(); - } - - @Override - public synchronized InputStream getInputStream(ZipEntry ze) throws IOException { - return this.parent.getInputStream(ze); - } - - @Override - public String getComment() { - return this.parent.getComment(); - } - - @Override - public int size() { - return this.parent.size(); - } - - @Override - public String toString() { - return this.parent.toString(); - } - - @Override - public String getName() { - return this.parent.getName(); - } - - static JarFile unwrap(java.util.jar.JarFile jarFile) { - if (jarFile instanceof JarFile file) { - return file; - } - if (jarFile instanceof JarFileWrapper wrapper) { - return unwrap(wrapper.parent); - } - throw new IllegalStateException("Not a JarFile or Wrapper"); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java deleted file mode 100644 index 859ae88ab000..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarURLConnection.java +++ /dev/null @@ -1,393 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.jar; - -import java.io.ByteArrayOutputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLEncoder; -import java.net.URLStreamHandler; -import java.security.Permission; - -/** - * {@link java.net.JarURLConnection} used to support {@link JarFile#getUrl()}. - * - * @author Phillip Webb - * @author Andy Wilkinson - * @author Rostyslav Dudka - */ -final class JarURLConnection extends java.net.JarURLConnection { - - private static final ThreadLocal useFastExceptions = new ThreadLocal<>(); - - private static final FileNotFoundException FILE_NOT_FOUND_EXCEPTION = new FileNotFoundException( - "Jar file or entry not found"); - - private static final IllegalStateException NOT_FOUND_CONNECTION_EXCEPTION = new IllegalStateException( - FILE_NOT_FOUND_EXCEPTION); - - private static final String SEPARATOR = "!/"; - - private static final URL EMPTY_JAR_URL; - - static { - try { - EMPTY_JAR_URL = new URL("jar:", null, 0, "file:!/", new URLStreamHandler() { - @Override - protected URLConnection openConnection(URL u) throws IOException { - // Stub URLStreamHandler to prevent the wrong JAR Handler from being - // Instantiated and cached. - return null; - } - }); - } - catch (MalformedURLException ex) { - throw new IllegalStateException(ex); - } - } - - private static final JarEntryName EMPTY_JAR_ENTRY_NAME = new JarEntryName(new StringSequence("")); - - private static final JarURLConnection NOT_FOUND_CONNECTION = JarURLConnection.notFound(); - - private final AbstractJarFile jarFile; - - private Permission permission; - - private URL jarFileUrl; - - private final JarEntryName jarEntryName; - - private java.util.jar.JarEntry jarEntry; - - private JarURLConnection(URL url, AbstractJarFile jarFile, JarEntryName jarEntryName) throws IOException { - // What we pass to super is ultimately ignored - super(EMPTY_JAR_URL); - this.url = url; - this.jarFile = jarFile; - this.jarEntryName = jarEntryName; - } - - @Override - public void connect() throws IOException { - if (this.jarFile == null) { - throw FILE_NOT_FOUND_EXCEPTION; - } - if (!this.jarEntryName.isEmpty() && this.jarEntry == null) { - this.jarEntry = this.jarFile.getJarEntry(getEntryName()); - if (this.jarEntry == null) { - throwFileNotFound(this.jarEntryName, this.jarFile); - } - } - this.connected = true; - } - - @Override - public java.util.jar.JarFile getJarFile() throws IOException { - connect(); - return this.jarFile; - } - - @Override - public URL getJarFileURL() { - if (this.jarFile == null) { - throw NOT_FOUND_CONNECTION_EXCEPTION; - } - if (this.jarFileUrl == null) { - this.jarFileUrl = buildJarFileUrl(); - } - return this.jarFileUrl; - } - - private URL buildJarFileUrl() { - try { - String spec = this.jarFile.getUrl().getFile(); - if (spec.endsWith(SEPARATOR)) { - spec = spec.substring(0, spec.length() - SEPARATOR.length()); - } - if (!spec.contains(SEPARATOR)) { - return new URL(spec); - } - return new URL("jar:" + spec); - } - catch (MalformedURLException ex) { - throw new IllegalStateException(ex); - } - } - - @Override - public java.util.jar.JarEntry getJarEntry() throws IOException { - if (this.jarEntryName == null || this.jarEntryName.isEmpty()) { - return null; - } - connect(); - return this.jarEntry; - } - - @Override - public String getEntryName() { - if (this.jarFile == null) { - throw NOT_FOUND_CONNECTION_EXCEPTION; - } - return this.jarEntryName.toString(); - } - - @Override - public InputStream getInputStream() throws IOException { - if (this.jarFile == null) { - throw FILE_NOT_FOUND_EXCEPTION; - } - if (this.jarEntryName.isEmpty() && this.jarFile.getType() == JarFile.JarFileType.DIRECT) { - throw new IOException("no entry name specified"); - } - connect(); - InputStream inputStream = (this.jarEntryName.isEmpty() ? this.jarFile.getInputStream() - : this.jarFile.getInputStream(this.jarEntry)); - if (inputStream == null) { - throwFileNotFound(this.jarEntryName, this.jarFile); - } - return inputStream; - } - - private void throwFileNotFound(Object entry, AbstractJarFile jarFile) throws FileNotFoundException { - if (Boolean.TRUE.equals(useFastExceptions.get())) { - throw FILE_NOT_FOUND_EXCEPTION; - } - throw new FileNotFoundException("JAR entry " + entry + " not found in " + jarFile.getName()); - } - - @Override - public int getContentLength() { - long length = getContentLengthLong(); - if (length > Integer.MAX_VALUE) { - return -1; - } - return (int) length; - } - - @Override - public long getContentLengthLong() { - if (this.jarFile == null) { - return -1; - } - try { - if (this.jarEntryName.isEmpty()) { - return this.jarFile.size(); - } - java.util.jar.JarEntry entry = getJarEntry(); - return (entry != null) ? (int) entry.getSize() : -1; - } - catch (IOException ex) { - return -1; - } - } - - @Override - public Object getContent() throws IOException { - connect(); - return this.jarEntryName.isEmpty() ? this.jarFile : super.getContent(); - } - - @Override - public String getContentType() { - return (this.jarEntryName != null) ? this.jarEntryName.getContentType() : null; - } - - @Override - public Permission getPermission() throws IOException { - if (this.jarFile == null) { - throw FILE_NOT_FOUND_EXCEPTION; - } - if (this.permission == null) { - this.permission = this.jarFile.getPermission(); - } - return this.permission; - } - - @Override - public long getLastModified() { - if (this.jarFile == null || this.jarEntryName.isEmpty()) { - return 0; - } - try { - java.util.jar.JarEntry entry = getJarEntry(); - return (entry != null) ? entry.getTime() : 0; - } - catch (IOException ex) { - return 0; - } - } - - static void setUseFastExceptions(boolean useFastExceptions) { - JarURLConnection.useFastExceptions.set(useFastExceptions); - } - - static JarURLConnection get(URL url, JarFile jarFile) throws IOException { - StringSequence spec = new StringSequence(url.getFile()); - int index = indexOfRootSpec(spec, jarFile.getPathFromRoot()); - if (index == -1) { - return (Boolean.TRUE.equals(useFastExceptions.get()) ? NOT_FOUND_CONNECTION - : new JarURLConnection(url, null, EMPTY_JAR_ENTRY_NAME)); - } - int separator; - while ((separator = spec.indexOf(SEPARATOR, index)) > 0) { - JarEntryName entryName = JarEntryName.get(spec.subSequence(index, separator)); - JarEntry jarEntry = jarFile.getJarEntry(entryName.toCharSequence()); - if (jarEntry == null) { - return JarURLConnection.notFound(jarFile, entryName); - } - jarFile = jarFile.getNestedJarFile(jarEntry); - index = separator + SEPARATOR.length(); - } - JarEntryName jarEntryName = JarEntryName.get(spec, index); - if (Boolean.TRUE.equals(useFastExceptions.get()) && !jarEntryName.isEmpty() - && !jarFile.containsEntry(jarEntryName.toString())) { - return NOT_FOUND_CONNECTION; - } - return new JarURLConnection(url, jarFile.getWrapper(), jarEntryName); - } - - private static int indexOfRootSpec(StringSequence file, String pathFromRoot) { - int separatorIndex = file.indexOf(SEPARATOR); - if (separatorIndex < 0 || !file.startsWith(pathFromRoot, separatorIndex)) { - return -1; - } - return separatorIndex + SEPARATOR.length() + pathFromRoot.length(); - } - - private static JarURLConnection notFound() { - try { - return notFound(null, null); - } - catch (IOException ex) { - throw new IllegalStateException(ex); - } - } - - private static JarURLConnection notFound(JarFile jarFile, JarEntryName jarEntryName) throws IOException { - if (Boolean.TRUE.equals(useFastExceptions.get())) { - return NOT_FOUND_CONNECTION; - } - return new JarURLConnection(null, jarFile, jarEntryName); - } - - /** - * A JarEntryName parsed from a URL String. - */ - static class JarEntryName { - - private final StringSequence name; - - private String contentType; - - JarEntryName(StringSequence spec) { - this.name = decode(spec); - } - - private StringSequence decode(StringSequence source) { - if (source.isEmpty() || (source.indexOf('%') < 0)) { - return source; - } - ByteArrayOutputStream bos = new ByteArrayOutputStream(source.length()); - write(source.toString(), bos); - // AsciiBytes is what is used to store the JarEntries so make it symmetric - return new StringSequence(AsciiBytes.toString(bos.toByteArray())); - } - - private void write(String source, ByteArrayOutputStream outputStream) { - int length = source.length(); - for (int i = 0; i < length; i++) { - int c = source.charAt(i); - if (c > 127) { - try { - String encoded = URLEncoder.encode(String.valueOf((char) c), "UTF-8"); - write(encoded, outputStream); - } - catch (UnsupportedEncodingException ex) { - throw new IllegalStateException(ex); - } - } - else { - if (c == '%') { - if ((i + 2) >= length) { - throw new IllegalArgumentException( - "Invalid encoded sequence \"" + source.substring(i) + "\""); - } - c = decodeEscapeSequence(source, i); - i += 2; - } - outputStream.write(c); - } - } - } - - private char decodeEscapeSequence(String source, int i) { - int hi = Character.digit(source.charAt(i + 1), 16); - int lo = Character.digit(source.charAt(i + 2), 16); - if (hi == -1 || lo == -1) { - throw new IllegalArgumentException("Invalid encoded sequence \"" + source.substring(i) + "\""); - } - return ((char) ((hi << 4) + lo)); - } - - CharSequence toCharSequence() { - return this.name; - } - - @Override - public String toString() { - return this.name.toString(); - } - - boolean isEmpty() { - return this.name.isEmpty(); - } - - String getContentType() { - if (this.contentType == null) { - this.contentType = deduceContentType(); - } - return this.contentType; - } - - private String deduceContentType() { - // Guess the content type, don't bother with streams as mark is not supported - String type = isEmpty() ? "x-java/jar" : null; - type = (type != null) ? type : guessContentTypeFromName(toString()); - type = (type != null) ? type : "content/unknown"; - return type; - } - - static JarEntryName get(StringSequence spec) { - return get(spec, 0); - } - - static JarEntryName get(StringSequence spec, int beginIndex) { - if (spec.length() <= beginIndex) { - return EMPTY_JAR_ENTRY_NAME; - } - return new JarEntryName(spec.subSequence(beginIndex)); - } - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ManifestInfo.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ManifestInfo.java new file mode 100644 index 000000000000..1a6b592f3287 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ManifestInfo.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.util.jar.Attributes; +import java.util.jar.Attributes.Name; +import java.util.jar.Manifest; + +import org.springframework.boot.loader.zip.ZipContent; + +/** + * Info obtained from a {@link ZipContent} instance relating to the {@link Manifest}. + * + * @author Phillip Webb + */ +class ManifestInfo { + + private static final Name MULTI_RELEASE = new Name("Multi-Release"); + + static final ManifestInfo NONE = new ManifestInfo(null, false); + + private final Manifest manifest; + + private volatile Boolean multiRelease; + + /** + * Create a new {@link ManifestInfo} instance. + * @param manifest the jar manifest + */ + ManifestInfo(Manifest manifest) { + this(manifest, null); + } + + private ManifestInfo(Manifest manifest, Boolean multiRelease) { + this.manifest = manifest; + this.multiRelease = multiRelease; + } + + /** + * Return the manifest, if any. + * @return the manifest or {@code null} + */ + Manifest getManifest() { + return this.manifest; + } + + /** + * Return if this is a multi-release jar. + * @return if the jar is multi-release + */ + boolean isMultiRelease() { + if (this.manifest == null) { + return false; + } + Boolean multiRelease = this.multiRelease; + if (multiRelease != null) { + return multiRelease; + } + Attributes attributes = this.manifest.getMainAttributes(); + multiRelease = attributes.containsKey(MULTI_RELEASE); + this.multiRelease = multiRelease; + return multiRelease; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/MetaInfVersionsInfo.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/MetaInfVersionsInfo.java new file mode 100644 index 000000000000..caf76a2b96f6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/MetaInfVersionsInfo.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.util.Collections; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.IntFunction; + +import org.springframework.boot.loader.zip.ZipContent; + +/** + * Info obtained from a {@link ZipContent} instance relating to the directories listed + * under {@code META-INF/versions/}. + * + * @author Phillip Webb + */ +final class MetaInfVersionsInfo { + + static final MetaInfVersionsInfo NONE = new MetaInfVersionsInfo(Collections.emptySet()); + + private static final String META_INF_VERSIONS = NestedJarFile.META_INF_VERSIONS; + + private final int[] versions; + + private final String[] directories; + + private MetaInfVersionsInfo(Set versions) { + this.versions = versions.stream().mapToInt(Integer::intValue).toArray(); + this.directories = versions.stream().map((version) -> META_INF_VERSIONS + version + "/").toArray(String[]::new); + } + + /** + * Return the versions listed under {@code META-INF/versions/} in ascending order. + * @return the versions + */ + int[] versions() { + return this.versions; + } + + /** + * Return the version directories in the same order as {@link #versions()}. + * @return the version directories + */ + String[] directories() { + return this.directories; + } + + /** + * Get {@link MetaInfVersionsInfo} for the given {@link ZipContent}. + * @param zipContent the zip content + * @return the {@link MetaInfVersionsInfo}. + */ + static MetaInfVersionsInfo get(ZipContent zipContent) { + return get(zipContent.size(), zipContent::getEntry); + } + + /** + * Get {@link MetaInfVersionsInfo} for the given details. + * @param size the number of entries + * @param entries a function to get an entry from an index + * @return the {@link MetaInfVersionsInfo}. + */ + static MetaInfVersionsInfo get(int size, IntFunction entries) { + Set versions = new TreeSet<>(); + for (int i = 0; i < size; i++) { + ZipContent.Entry contentEntry = entries.apply(i); + if (contentEntry.hasNameStartingWith(META_INF_VERSIONS) && !contentEntry.isDirectory()) { + String name = contentEntry.getName(); + int slash = name.indexOf('/', META_INF_VERSIONS.length()); + String version = name.substring(META_INF_VERSIONS.length(), slash); + try { + int versionNumber = Integer.parseInt(version); + if (versionNumber >= NestedJarFile.BASE_VERSION) { + versions.add(versionNumber); + } + } + catch (NumberFormatException ex) { + // Ignore + } + } + } + return (!versions.isEmpty()) ? new MetaInfVersionsInfo(versions) : NONE; + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java new file mode 100644 index 000000000000..b3de537026a0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java @@ -0,0 +1,801 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.lang.ref.Cleaner.Cleanable; +import java.nio.ByteBuffer; +import java.nio.file.attribute.FileTime; +import java.security.CodeSigner; +import java.security.cert.Certificate; +import java.time.LocalDateTime; +import java.util.Enumeration; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Spliterator; +import java.util.Spliterators.AbstractSpliterator; +import java.util.function.Consumer; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import java.util.zip.Inflater; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; + +import org.springframework.boot.loader.log.DebugLogger; +import org.springframework.boot.loader.ref.Cleaner; +import org.springframework.boot.loader.zip.CloseableDataBlock; +import org.springframework.boot.loader.zip.ZipContent; +import org.springframework.boot.loader.zip.ZipContent.Entry; + +/** + * Extended variant of {@link JarFile} that behaves in the same way but can open nested + * jars. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @since 3.2.0 + */ +public class NestedJarFile extends JarFile { + + private static final int DECIMAL = 10; + + private static final String META_INF = "META-INF/"; + + static final String META_INF_VERSIONS = META_INF + "versions/"; + + static final int BASE_VERSION = baseVersion().feature(); + + private static final DebugLogger debug = DebugLogger.get(NestedJarFile.class); + + private final Cleaner cleaner; + + private final NestedJarFileResources resources; + + private final Cleanable cleanup; + + private final String name; + + private final int version; + + private volatile NestedJarEntry lastEntry; + + private volatile boolean closed; + + private volatile ManifestInfo manifestInfo; + + private volatile MetaInfVersionsInfo metaInfVersionsInfo; + + /** + * Creates a new {@link NestedJarFile} instance to read from the specific + * {@code File}. + * @param file the jar file to be opened for reading + * @throws IOException on I/O error + */ + NestedJarFile(File file) throws IOException { + this(file, null, null, false, Cleaner.instance); + } + + /** + * Creates a new {@link NestedJarFile} instance to read from the specific + * {@code File}. + * @param file the jar file to be opened for reading + * @param nestedEntryName the nested entry name to open or {@code null} + * @throws IOException on I/O error + */ + public NestedJarFile(File file, String nestedEntryName) throws IOException { + this(file, nestedEntryName, null, true, Cleaner.instance); + } + + /** + * Creates a new {@link NestedJarFile} instance to read from the specific + * {@code File}. + * @param file the jar file to be opened for reading + * @param nestedEntryName the nested entry name to open or {@code null} + * @param version the release version to use when opening a multi-release jar + * @throws IOException on I/O error + */ + public NestedJarFile(File file, String nestedEntryName, Runtime.Version version) throws IOException { + this(file, nestedEntryName, version, true, Cleaner.instance); + } + + /** + * Creates a new {@link NestedJarFile} instance to read from the specific + * {@code File}. + * @param file the jar file to be opened for reading + * @param nestedEntryName the nested entry name to open or {@code null} + * @param version the release version to use when opening a multi-release jar + * @param onlyNestedJars if only nested jars should be opened + * @param cleaner the cleaner used to release resources + * @throws IOException on I/O error + */ + NestedJarFile(File file, String nestedEntryName, Runtime.Version version, boolean onlyNestedJars, Cleaner cleaner) + throws IOException { + super(file); + if (onlyNestedJars && (nestedEntryName == null || nestedEntryName.isEmpty())) { + throw new IllegalArgumentException("nestedEntryName must not be empty"); + } + debug.log("Created nested jar file (%s, %s, %s)", file, nestedEntryName, version); + this.cleaner = cleaner; + this.resources = new NestedJarFileResources(file, nestedEntryName); + this.cleanup = cleaner.register(this, this.resources); + this.name = file.getPath() + ((nestedEntryName != null) ? "!/" + nestedEntryName : ""); + this.version = (version != null) ? version.feature() : baseVersion().feature(); + } + + @Override + public Manifest getManifest() throws IOException { + try { + return this.resources.zipContent().getInfo(ManifestInfo.class, this::getManifestInfo).getManifest(); + } + catch (UncheckedIOException ex) { + throw ex.getCause(); + } + } + + @Override + public Enumeration entries() { + synchronized (this) { + ensureOpen(); + return new JarEntriesEnumeration(this.resources.zipContent()); + } + } + + @Override + public Stream stream() { + synchronized (this) { + ensureOpen(); + return streamContentEntries().map(NestedJarEntry::new); + } + } + + @Override + public Stream versionedStream() { + synchronized (this) { + ensureOpen(); + return streamContentEntries().map(this::getBaseName) + .filter(Objects::nonNull) + .distinct() + .map(this::getJarEntry) + .filter(Objects::nonNull); + } + } + + private Stream streamContentEntries() { + ZipContentEntriesSpliterator spliterator = new ZipContentEntriesSpliterator(this.resources.zipContent()); + return StreamSupport.stream(spliterator, false); + } + + private String getBaseName(ZipContent.Entry contentEntry) { + String name = contentEntry.getName(); + if (!name.startsWith(META_INF_VERSIONS)) { + return name; + } + int versionNumberStartIndex = META_INF_VERSIONS.length(); + int versionNumberEndIndex = (versionNumberStartIndex != -1) ? name.indexOf('/', versionNumberStartIndex) : -1; + if (versionNumberEndIndex == -1 || versionNumberEndIndex == (name.length() - 1)) { + return null; + } + try { + int versionNumber = Integer.parseInt(name, versionNumberStartIndex, versionNumberEndIndex, DECIMAL); + if (versionNumber > this.version) { + return null; + } + } + catch (NumberFormatException ex) { + return null; + } + return name.substring(versionNumberEndIndex + 1); + } + + @Override + public JarEntry getJarEntry(String name) { + return getNestedJarEntry(name); + } + + @Override + public JarEntry getEntry(String name) { + return getNestedJarEntry(name); + } + + /** + * Return if an entry with the given name exists. + * @param name the name to check + * @return if the entry exists + */ + public boolean hasEntry(String name) { + NestedJarEntry lastEntry = this.lastEntry; + if (lastEntry != null && name.equals(lastEntry.getName())) { + return true; + } + ZipContent.Entry entry = getVersionedContentEntry(name); + if (entry != null) { + return false; + } + synchronized (this) { + ensureOpen(); + return this.resources.zipContent().hasEntry(null, name); + } + } + + private NestedJarEntry getNestedJarEntry(String name) { + Objects.requireNonNull(name, "name"); + NestedJarEntry lastEntry = this.lastEntry; + if (lastEntry != null && name.equals(lastEntry.getName())) { + return lastEntry; + } + ZipContent.Entry entry = getVersionedContentEntry(name); + entry = (entry != null) ? entry : getContentEntry(null, name); + if (entry == null) { + return null; + } + NestedJarEntry nestedJarEntry = new NestedJarEntry(entry, name); + this.lastEntry = nestedJarEntry; + return nestedJarEntry; + } + + private ZipContent.Entry getVersionedContentEntry(String name) { + // NOTE: we can't call isMultiRelease() directly because it's a final method and + // it inspects the container jar. We use ManifestInfo instead. + if (BASE_VERSION >= this.version || name.startsWith(META_INF) || !getManifestInfo().isMultiRelease()) { + return null; + } + MetaInfVersionsInfo metaInfVersionsInfo = getMetaInfVersionsInfo(); + int[] versions = metaInfVersionsInfo.versions(); + String[] directories = metaInfVersionsInfo.directories(); + for (int i = versions.length - 1; i >= 0; i--) { + if (versions[i] <= this.version) { + ZipContent.Entry entry = getContentEntry(directories[i], name); + if (entry != null) { + return entry; + } + } + } + return null; + } + + private ZipContent.Entry getContentEntry(String namePrefix, String name) { + synchronized (this) { + ensureOpen(); + return this.resources.zipContent().getEntry(namePrefix, name); + } + } + + private ManifestInfo getManifestInfo() { + ManifestInfo manifestInfo = this.manifestInfo; + if (manifestInfo != null) { + return manifestInfo; + } + synchronized (this) { + ensureOpen(); + manifestInfo = this.resources.zipContent().getInfo(ManifestInfo.class, this::getManifestInfo); + } + this.manifestInfo = manifestInfo; + return manifestInfo; + } + + private ManifestInfo getManifestInfo(ZipContent zipContent) { + ZipContent.Entry contentEntry = zipContent.getEntry(MANIFEST_NAME); + if (contentEntry == null) { + return ManifestInfo.NONE; + } + try { + try (InputStream inputStream = getInputStream(contentEntry)) { + Manifest manifest = new Manifest(inputStream); + return new ManifestInfo(manifest); + } + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private MetaInfVersionsInfo getMetaInfVersionsInfo() { + MetaInfVersionsInfo metaInfVersionsInfo = this.metaInfVersionsInfo; + if (metaInfVersionsInfo != null) { + return metaInfVersionsInfo; + } + synchronized (this) { + ensureOpen(); + metaInfVersionsInfo = this.resources.zipContent() + .getInfo(MetaInfVersionsInfo.class, MetaInfVersionsInfo::get); + } + this.metaInfVersionsInfo = metaInfVersionsInfo; + return metaInfVersionsInfo; + } + + @Override + public InputStream getInputStream(ZipEntry entry) throws IOException { + Objects.requireNonNull(entry, "entry"); + if (entry instanceof NestedJarEntry nestedJarEntry && nestedJarEntry.isOwnedBy(this)) { + return getInputStream(nestedJarEntry.contentEntry()); + } + return getInputStream(getNestedJarEntry(entry.getName()).contentEntry()); + } + + private InputStream getInputStream(ZipContent.Entry contentEntry) throws IOException { + int compression = contentEntry.getCompressionMethod(); + if (compression != ZipEntry.STORED && compression != ZipEntry.DEFLATED) { + throw new ZipException("invalid compression method"); + } + synchronized (this) { + ensureOpen(); + InputStream inputStream = new JarEntryInputStream(contentEntry); + try { + if (compression == ZipEntry.DEFLATED) { + inputStream = new JarEntryInflaterInputStream((JarEntryInputStream) inputStream, this.resources); + } + this.resources.addInputStream(inputStream); + return inputStream; + } + catch (RuntimeException ex) { + inputStream.close(); + throw ex; + } + } + } + + @Override + public String getComment() { + synchronized (this) { + ensureOpen(); + return this.resources.zipContent().getComment(); + } + } + + @Override + public int size() { + synchronized (this) { + ensureOpen(); + return this.resources.zipContent().size(); + } + } + + @Override + public void close() throws IOException { + if (this.closed) { + return; + } + this.closed = true; + synchronized (this) { + try { + this.cleanup.clean(); + } + catch (UncheckedIOException ex) { + throw ex.getCause(); + } + } + } + + @Override + public String getName() { + return this.name; + } + + private void ensureOpen() { + if (this.closed) { + throw new IllegalStateException("Zip file closed"); + } + if (this.resources.zipContent() == null) { + throw new IllegalStateException("The object is not initialized."); + } + } + + /** + * Clear any internal caches. + */ + public void clearCache() { + synchronized (this) { + this.lastEntry = null; + } + } + + /** + * An individual entry from a {@link NestedJarFile}. + */ + private class NestedJarEntry extends java.util.jar.JarEntry { + + private static final IllegalStateException CANNOT_BE_MODIFIED_EXCEPTION = new IllegalStateException( + "Neste jar entries cannot be modified"); + + private final ZipContent.Entry contentEntry; + + private final String name; + + private volatile boolean populated; + + NestedJarEntry(Entry contentEntry) { + this(contentEntry, contentEntry.getName()); + } + + NestedJarEntry(ZipContent.Entry contentEntry, String name) { + super(contentEntry.getName()); + this.contentEntry = contentEntry; + this.name = name; + } + + @Override + public long getTime() { + populate(); + return super.getTime(); + } + + @Override + public LocalDateTime getTimeLocal() { + populate(); + return super.getTimeLocal(); + } + + @Override + public void setTime(long time) { + throw CANNOT_BE_MODIFIED_EXCEPTION; + } + + @Override + public void setTimeLocal(LocalDateTime time) { + throw CANNOT_BE_MODIFIED_EXCEPTION; + } + + @Override + public FileTime getLastModifiedTime() { + populate(); + return super.getLastModifiedTime(); + } + + @Override + public ZipEntry setLastModifiedTime(FileTime time) { + throw CANNOT_BE_MODIFIED_EXCEPTION; + } + + @Override + public FileTime getLastAccessTime() { + populate(); + return super.getLastAccessTime(); + } + + @Override + public ZipEntry setLastAccessTime(FileTime time) { + throw CANNOT_BE_MODIFIED_EXCEPTION; + } + + @Override + public FileTime getCreationTime() { + populate(); + return super.getCreationTime(); + } + + @Override + public ZipEntry setCreationTime(FileTime time) { + throw CANNOT_BE_MODIFIED_EXCEPTION; + } + + @Override + public long getSize() { + return this.contentEntry.getUncompressedSize() & 0xFFFFFFFFL; + } + + @Override + public void setSize(long size) { + throw CANNOT_BE_MODIFIED_EXCEPTION; + } + + @Override + public long getCompressedSize() { + populate(); + return super.getCompressedSize(); + } + + @Override + public void setCompressedSize(long csize) { + throw CANNOT_BE_MODIFIED_EXCEPTION; + } + + @Override + public long getCrc() { + populate(); + return super.getCrc(); + } + + @Override + public void setCrc(long crc) { + throw CANNOT_BE_MODIFIED_EXCEPTION; + } + + @Override + public int getMethod() { + populate(); + return super.getMethod(); + } + + @Override + public void setMethod(int method) { + throw CANNOT_BE_MODIFIED_EXCEPTION; + } + + @Override + public byte[] getExtra() { + populate(); + return super.getExtra(); + } + + @Override + public void setExtra(byte[] extra) { + throw CANNOT_BE_MODIFIED_EXCEPTION; + } + + @Override + public String getComment() { + populate(); + return super.getComment(); + } + + @Override + public void setComment(String comment) { + throw CANNOT_BE_MODIFIED_EXCEPTION; + } + + boolean isOwnedBy(NestedJarFile nestedJarFile) { + return NestedJarFile.this == nestedJarFile; + } + + @Override + public String getRealName() { + return super.getName(); + } + + @Override + public String getName() { + return this.name; + } + + @Override + public Attributes getAttributes() throws IOException { + Manifest manifest = getManifest(); + return (manifest != null) ? manifest.getAttributes(getName()) : null; + } + + @Override + public Certificate[] getCertificates() { + return getSecurityInfo().getCertificates(contentEntry()); + } + + @Override + public CodeSigner[] getCodeSigners() { + return getSecurityInfo().getCodeSigners(contentEntry()); + } + + private SecurityInfo getSecurityInfo() { + return NestedJarFile.this.resources.zipContent().getInfo(SecurityInfo.class, SecurityInfo::get); + } + + ZipContent.Entry contentEntry() { + return this.contentEntry; + } + + private void populate() { + boolean populated = this.populated; + if (!populated) { + ZipEntry entry = this.contentEntry.as(ZipEntry::new); + super.setMethod(entry.getMethod()); + super.setTime(entry.getTime()); + super.setCrc(entry.getCrc()); + super.setCompressedSize(entry.getCompressedSize()); + super.setSize(entry.getSize()); + super.setExtra(entry.getExtra()); + super.setComment(entry.getComment()); + this.populated = true; + } + } + + } + + /** + * {@link Enumeration} of {@link NestedJarEntry} instances. + */ + private class JarEntriesEnumeration implements Enumeration { + + private final ZipContent zipContent; + + private int cursor; + + JarEntriesEnumeration(ZipContent zipContent) { + this.zipContent = zipContent; + } + + @Override + public boolean hasMoreElements() { + return this.cursor < this.zipContent.size(); + } + + @Override + public NestedJarEntry nextElement() { + if (!hasMoreElements()) { + throw new NoSuchElementException(); + } + synchronized (NestedJarFile.this) { + ensureOpen(); + return new NestedJarEntry(this.zipContent.getEntry(this.cursor++)); + } + } + + } + + /** + * {@link Spliterator} for {@link ZipContent.Entry} instances. + */ + private class ZipContentEntriesSpliterator extends AbstractSpliterator { + + private static final int ADDITIONAL_CHARACTERISTICS = Spliterator.ORDERED | Spliterator.DISTINCT + | Spliterator.IMMUTABLE | Spliterator.NONNULL; + + private final ZipContent zipContent; + + private int cursor; + + ZipContentEntriesSpliterator(ZipContent zipContent) { + super(zipContent.size(), ADDITIONAL_CHARACTERISTICS); + this.zipContent = zipContent; + } + + @Override + public boolean tryAdvance(Consumer action) { + if (this.cursor < this.zipContent.size()) { + synchronized (NestedJarFile.this) { + ensureOpen(); + action.accept(this.zipContent.getEntry(this.cursor++)); + } + return true; + } + return false; + } + + } + + /** + * {@link InputStream} to read jar entry content. + */ + private class JarEntryInputStream extends InputStream { + + private final int uncompressedSize; + + private final CloseableDataBlock content; + + private long pos; + + private long remaining; + + private volatile boolean closed; + + JarEntryInputStream(ZipContent.Entry entry) throws IOException { + this.uncompressedSize = entry.getUncompressedSize(); + this.content = entry.openContent(); + } + + @Override + public int read() throws IOException { + byte[] b = new byte[1]; + return (read(b, 0, 1) == 1) ? b[0] & 0xFF : -1; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int result; + synchronized (NestedJarFile.this) { + ensureOpen(); + ByteBuffer dst = ByteBuffer.wrap(b, off, len); + int count = this.content.read(dst, this.pos); + if (count > 0) { + this.pos += count; + this.remaining -= count; + } + result = count; + } + if (this.remaining == 0) { + close(); + } + return result; + } + + @Override + public long skip(long n) throws IOException { + long result; + synchronized (NestedJarFile.this) { + result = (n > 0) ? maxForwardSkip(n) : maxBackwardSkip(n); + this.pos += result; + this.remaining -= result; + } + if (this.remaining == 0) { + close(); + } + return result; + } + + private long maxForwardSkip(long n) { + boolean willCauseOverflow = (this.pos + n) < 0; + return (willCauseOverflow || n > this.remaining) ? this.remaining : n; + } + + private long maxBackwardSkip(long n) { + return Math.max(-this.pos, n); + } + + @Override + public int available() { + return (this.remaining < Integer.MAX_VALUE) ? (int) this.remaining : Integer.MAX_VALUE; + } + + private void ensureOpen() throws ZipException { + if (NestedJarFile.this.closed || this.closed) { + throw new ZipException("ZipFile closed"); + } + } + + @Override + public void close() throws IOException { + if (this.closed) { + return; + } + this.closed = true; + this.content.close(); + NestedJarFile.this.resources.removeInputStream(this); + } + + int getUncompressedSize() { + return this.uncompressedSize; + } + + } + + /** + * {@link ZipInflaterInputStream} to read and inflate jar entry content. + */ + private class JarEntryInflaterInputStream extends ZipInflaterInputStream { + + private final Cleanable cleanup; + + private volatile boolean closed; + + JarEntryInflaterInputStream(JarEntryInputStream inputStream, NestedJarFileResources resources) { + this(inputStream, resources, resources.getOrCreateInflater()); + } + + private JarEntryInflaterInputStream(JarEntryInputStream inputStream, NestedJarFileResources resources, + Inflater inflater) { + super(inputStream, inflater, inputStream.getUncompressedSize()); + this.cleanup = NestedJarFile.this.cleaner.register(this, resources.createInflatorCleanupAction(inflater)); + } + + @Override + public void close() throws IOException { + if (this.closed) { + return; + } + this.closed = true; + super.close(); + NestedJarFile.this.resources.removeInputStream(this); + this.cleanup.clean(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFileResources.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFileResources.java new file mode 100644 index 000000000000..4f57e03497f8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFileResources.java @@ -0,0 +1,206 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.Deque; +import java.util.List; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.zip.Inflater; + +import org.springframework.boot.loader.ref.Cleaner; +import org.springframework.boot.loader.zip.ZipContent; + +/** + * Resources created managed and cleaned by a {@link NestedJarFile} instance and suitable + * for registration with a {@link Cleaner}. + * + * @author Phillip Webb + */ +class NestedJarFileResources implements Runnable { + + private static final int INFLATER_CACHE_LIMIT = 20; + + private ZipContent zipContent; + + private final Set inputStreams = Collections.newSetFromMap(new WeakHashMap<>()); + + private Deque inflaterCache = new ArrayDeque<>(); + + /** + * Create a new {@link NestedJarFileResources} instance. + * @param file the source zip file + * @param nestedEntryName the nested entry or {@code null} + * @throws IOException on I/O error + */ + NestedJarFileResources(File file, String nestedEntryName) throws IOException { + this.zipContent = ZipContent.open(file.toPath(), nestedEntryName); + } + + /** + * Return the underling {@link ZipContent}. + * @return the zip content + */ + ZipContent zipContent() { + return this.zipContent; + } + + /** + * Add a managed input stream resource. + * @param inputStream the input stream + */ + void addInputStream(InputStream inputStream) { + synchronized (this.inputStreams) { + this.inputStreams.add(inputStream); + } + } + + /** + * Remove a managed input stream resource. + * @param inputStream the input stream + */ + void removeInputStream(InputStream inputStream) { + synchronized (this.inputStreams) { + this.inputStreams.remove(inputStream); + } + } + + /** + * Create a {@link Runnable} action to cleanup the given inflater. + * @param inflater the inflater to cleanup + * @return the cleanup action + */ + Runnable createInflatorCleanupAction(Inflater inflater) { + return () -> endOrCacheInflater(inflater); + } + + /** + * Get previously used {@link Inflater} from the cache, or create a new one. + * @return a usable {@link Inflater} + */ + Inflater getOrCreateInflater() { + Deque inflaterCache = this.inflaterCache; + if (inflaterCache != null) { + synchronized (inflaterCache) { + Inflater inflater = this.inflaterCache.poll(); + if (inflater != null) { + return inflater; + } + } + } + return new Inflater(true); + } + + /** + * Either release the given {@link Inflater} by calling {@link Inflater#end()} or add + * it to the cache for later reuse. + * @param inflater the inflater to end or cache + */ + private void endOrCacheInflater(Inflater inflater) { + Deque inflaterCache = this.inflaterCache; + if (inflaterCache != null) { + synchronized (inflaterCache) { + if (this.inflaterCache == inflaterCache && inflaterCache.size() < INFLATER_CACHE_LIMIT) { + inflater.reset(); + this.inflaterCache.add(inflater); + return; + } + } + } + inflater.end(); + } + + /** + * Called by the {@link Cleaner} to free resources. + * @see java.lang.Runnable#run() + */ + @Override + public void run() { + releaseAll(); + } + + private void releaseAll() { + IOException exceptionChain = null; + exceptionChain = releaseInflators(exceptionChain); + exceptionChain = releaseInputStreams(exceptionChain); + exceptionChain = releaseZipContent(exceptionChain); + if (exceptionChain != null) { + throw new UncheckedIOException(exceptionChain); + } + } + + private IOException releaseInflators(IOException exceptionChain) { + Deque inflaterCache = this.inflaterCache; + if (inflaterCache != null) { + try { + synchronized (inflaterCache) { + inflaterCache.forEach(Inflater::end); + } + } + finally { + this.inflaterCache = null; + } + } + return exceptionChain; + } + + private IOException releaseInputStreams(IOException exceptionChain) { + synchronized (this.inputStreams) { + for (InputStream inputStream : List.copyOf(this.inputStreams)) { + try { + inputStream.close(); + } + catch (IOException ex) { + exceptionChain = addToExceptionChain(exceptionChain, ex); + } + } + this.inputStreams.clear(); + } + return exceptionChain; + } + + private IOException releaseZipContent(IOException exceptionChain) { + ZipContent zipContent = this.zipContent; + if (zipContent != null) { + try { + zipContent.close(); + } + catch (IOException ex) { + exceptionChain = addToExceptionChain(exceptionChain, ex); + } + finally { + this.zipContent = null; + } + } + return exceptionChain; + } + + private IOException addToExceptionChain(IOException exceptionChain, IOException ex) { + if (exceptionChain != null) { + exceptionChain.addSuppressed(ex); + return exceptionChain; + } + return ex; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/SecurityInfo.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/SecurityInfo.java new file mode 100644 index 000000000000..3b20bebdbe4d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/SecurityInfo.java @@ -0,0 +1,110 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.security.CodeSigner; +import java.security.cert.Certificate; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; + +import org.springframework.boot.loader.zip.ZipContent; + +/** + * Security information ({@link Certificate} and {@link CodeSigner} details) for entries + * in the jar. + * + * @author Phillip Webb + */ +final class SecurityInfo { + + static final SecurityInfo NONE = new SecurityInfo(null, null); + + private final Certificate[][] certificateLookups; + + private final CodeSigner[][] codeSignerLookups; + + private SecurityInfo(Certificate[][] entryCertificates, CodeSigner[][] entryCodeSigners) { + this.certificateLookups = entryCertificates; + this.codeSignerLookups = entryCodeSigners; + } + + Certificate[] getCertificates(ZipContent.Entry contentEntry) { + return (this.certificateLookups != null) ? clone(this.certificateLookups[contentEntry.getLookupIndex()]) : null; + } + + CodeSigner[] getCodeSigners(ZipContent.Entry contentEntry) { + return (this.codeSignerLookups != null) ? clone(this.codeSignerLookups[contentEntry.getLookupIndex()]) : null; + } + + private T[] clone(T[] array) { + return (array != null) ? array.clone() : null; + } + + /** + * Get the {@link SecurityInfo} for the given {@link ZipContent}. + * @param content the zip content + * @return the security info + */ + static SecurityInfo get(ZipContent content) { + if (!content.hasJarSignatureFile()) { + return NONE; + } + try { + return load(content); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + /** + * Load security info from the jar file. We need to use {@link JarInputStream} to + * obtain the security info since we don't have an actual real file to read. This + * isn't that fast, but hopefully doesn't happen too often and the result is cached. + * @param content the zip content + * @return the security info + * @throws IOException on I/O error + */ + private static SecurityInfo load(ZipContent content) throws IOException { + int size = content.size(); + boolean hasSecurityInfo = false; + Certificate[][] entryCertificates = new Certificate[size][]; + CodeSigner[][] entryCodeSigners = new CodeSigner[size][]; + try (JarInputStream in = new JarInputStream(content.openRawZipData().asInputStream())) { + JarEntry jarEntry = in.getNextJarEntry(); + while (jarEntry != null) { + in.closeEntry(); // Close to trigger a read and set certs/signers + Certificate[] certificates = jarEntry.getCertificates(); + CodeSigner[] codeSigners = jarEntry.getCodeSigners(); + if (certificates != null || codeSigners != null) { + ZipContent.Entry contentEntry = content.getEntry(jarEntry.getName()); + if (contentEntry != null) { + hasSecurityInfo = true; + entryCertificates[contentEntry.getLookupIndex()] = certificates; + entryCodeSigners[contentEntry.getLookupIndex()] = codeSigners; + } + } + jarEntry = in.getNextJarEntry(); + } + return (!hasSecurityInfo) ? NONE : new SecurityInfo(entryCertificates, entryCodeSigners); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java deleted file mode 100644 index 12850a4ebe3e..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/StringSequence.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.jar; - -import java.util.Objects; - -/** - * A {@link CharSequence} backed by a single shared {@link String}. Unlike a regular - * {@link String}, {@link #subSequence(int, int)} operations will not copy the underlying - * character array. - * - * @author Phillip Webb - */ -final class StringSequence implements CharSequence { - - private final String source; - - private final int start; - - private final int end; - - private int hash; - - StringSequence(String source) { - this(source, 0, (source != null) ? source.length() : -1); - } - - StringSequence(String source, int start, int end) { - Objects.requireNonNull(source, "Source must not be null"); - if (start < 0) { - throw new StringIndexOutOfBoundsException(start); - } - if (end > source.length()) { - throw new StringIndexOutOfBoundsException(end); - } - this.source = source; - this.start = start; - this.end = end; - } - - StringSequence subSequence(int start) { - return subSequence(start, length()); - } - - @Override - public StringSequence subSequence(int start, int end) { - int subSequenceStart = this.start + start; - int subSequenceEnd = this.start + end; - if (subSequenceStart > this.end) { - throw new StringIndexOutOfBoundsException(start); - } - if (subSequenceEnd > this.end) { - throw new StringIndexOutOfBoundsException(end); - } - if (start == 0 && subSequenceEnd == this.end) { - return this; - } - return new StringSequence(this.source, subSequenceStart, subSequenceEnd); - } - - /** - * Returns {@code true} if the sequence is empty. Public to be compatible with JDK 15. - * @return {@code true} if {@link #length()} is {@code 0}, otherwise {@code false} - */ - public boolean isEmpty() { - return length() == 0; - } - - @Override - public int length() { - return this.end - this.start; - } - - @Override - public char charAt(int index) { - return this.source.charAt(this.start + index); - } - - int indexOf(char ch) { - return this.source.indexOf(ch, this.start) - this.start; - } - - int indexOf(String str) { - return this.source.indexOf(str, this.start) - this.start; - } - - int indexOf(String str, int fromIndex) { - return this.source.indexOf(str, this.start + fromIndex) - this.start; - } - - boolean startsWith(String prefix) { - return startsWith(prefix, 0); - } - - boolean startsWith(String prefix, int offset) { - int prefixLength = prefix.length(); - int length = length(); - if (length - prefixLength - offset < 0) { - return false; - } - return this.source.startsWith(prefix, this.start + offset); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!(obj instanceof CharSequence other)) { - return false; - } - int n = length(); - if (n != other.length()) { - return false; - } - int i = 0; - while (n-- != 0) { - if (charAt(i) != other.charAt(i)) { - return false; - } - i++; - } - return true; - } - - @Override - public int hashCode() { - int hash = this.hash; - if (hash == 0 && length() > 0) { - for (int i = this.start; i < this.end; i++) { - hash = 31 * hash + this.source.charAt(i); - } - this.hash = hash; - } - return hash; - } - - @Override - public String toString() { - return this.source.substring(this.start, this.end); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java index 67624460ccd7..1528f0b9c507 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/ZipInflaterInputStream.java @@ -24,27 +24,32 @@ /** * {@link InflaterInputStream} that supports the writing of an extra "dummy" byte (which - * is required with JDK 6) and returns accurate available() results. + * is required when using an {@link Inflater} with {@code nowrap}) and returns accurate + * available() results. * * @author Phillip Webb */ -class ZipInflaterInputStream extends InflaterInputStream { +abstract class ZipInflaterInputStream extends InflaterInputStream { private int available; private boolean extraBytesWritten; - ZipInflaterInputStream(InputStream inputStream, int size) { - super(inputStream, new Inflater(true), getInflaterBufferSize(size)); + ZipInflaterInputStream(InputStream inputStream, Inflater inflater, int size) { + super(inputStream, inflater, getInflaterBufferSize(size)); this.available = size; } + private static int getInflaterBufferSize(long size) { + size += 2; // inflater likes some space + size = (size > 65536) ? 8192 : size; + size = (size <= 0) ? 4096 : size; + return (int) size; + } + @Override public int available() throws IOException { - if (this.available < 0) { - return super.available(); - } - return this.available; + return (this.available >= 0) ? this.available : super.available(); } @Override @@ -56,12 +61,6 @@ public int read(byte[] b, int off, int len) throws IOException { return result; } - @Override - public void close() throws IOException { - super.close(); - this.inf.end(); - } - @Override protected void fill() throws IOException { try { @@ -78,11 +77,4 @@ protected void fill() throws IOException { } } - private static int getInflaterBufferSize(long size) { - size += 2; // inflater likes some space - size = (size > 65536) ? 8192 : size; - size = (size <= 0) ? 4096 : size; - return (int) size; - } - } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/package-info.java index 638afe45f497..ae1ba30639e2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/package-info.java @@ -15,6 +15,7 @@ */ /** - * Support for loading and manipulating JAR/WAR files. + * Alternative {@link java.util.jar.JarFile} implementation with support for nested jars. + * @see org.springframework.boot.loader.jar.NestedJarFile */ package org.springframework.boot.loader.jar; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/package-info.java index 2f3b5a74e8fd..d68ef83474eb 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/package-info.java @@ -16,7 +16,5 @@ /** * Support for launching the JAR using jarmode. - * - * @see org.springframework.boot.loader.jarmode.JarModeLauncher */ package org.springframework.boot.loader.jarmode; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/Archive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/Archive.java new file mode 100644 index 000000000000..933a630ffb30 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/Archive.java @@ -0,0 +1,150 @@ +/* + * Copyright 2012-2023 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.loader.launch; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.security.CodeSource; +import java.security.ProtectionDomain; +import java.util.Set; +import java.util.function.Predicate; +import java.util.jar.Manifest; + +/** + * An archive that can be launched by the {@link Launcher}. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public interface Archive extends AutoCloseable { + + /** + * Predicate that accepts all entries. + */ + Predicate ALL_ENTRIES = (entry) -> true; + + /** + * Returns the manifest of the archive. + * @return the manifest or {@code null} + * @throws IOException if the manifest cannot be read + */ + Manifest getManifest() throws IOException; + + /** + * Returns classpath URLs for the archive that match the specified filter. + * @param includeFilter filter used to determine which entries should be included. + * @return the classpath URLs + * @throws IOException on IO error + */ + default Set getClassPathUrls(Predicate includeFilter) throws IOException { + return getClassPathUrls(includeFilter, ALL_ENTRIES); + + } + + /** + * Returns classpath URLs for the archive that match the specified filters. + * @param includeFilter filter used to determine which entries should be included + * @param directorySearchFilter filter used to optimize tree walking for exploded + * archives by determining if a directory needs to be searched or not + * @return the classpath URLs + * @throws IOException on IO error + */ + Set getClassPathUrls(Predicate includeFilter, Predicate directorySearchFilter) + throws IOException; + + /** + * Returns if this archive is backed by an exploded archive directory. + * @return if the archive is exploded + */ + default boolean isExploded() { + return getRootDirectory() != null; + } + + /** + * Returns the root directory of this archive or {@code null} if the archive is not + * backed by a directory. + * @return the root directory + */ + default File getRootDirectory() { + return null; + } + + /** + * Closes the {@code Archive}, releasing any open resources. + * @throws Exception if an error occurs during close processing + */ + @Override + default void close() throws Exception { + } + + /** + * Factory method to create an appropriate {@link Archive} from the given + * {@link Class} target. + * @param target a target class that will be used to find the archive code source + * @return an new {@link Archive} instance + * @throws Exception if the archive cannot be created + */ + static Archive create(Class target) throws Exception { + return create(target.getProtectionDomain()); + } + + static Archive create(ProtectionDomain protectionDomain) throws Exception { + CodeSource codeSource = protectionDomain.getCodeSource(); + URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null; + String path = (location != null) ? location.getSchemeSpecificPart() : null; + if (path == null) { + throw new IllegalStateException("Unable to determine code source archive"); + } + return create(new File(path)); + } + + /** + * Factory method to create an {@link Archive} from the given {@link File} target. + * @param target a target {@link File} used to create the archive. May be a directory + * or a jar file. + * @return a new {@link Archive} instance. + * @throws Exception if the archive cannot be created + */ + static Archive create(File target) throws Exception { + if (!target.exists()) { + throw new IllegalStateException("Unable to determine code source archive from " + target); + } + return (target.isDirectory() ? new ExplodedArchive(target) : new JarFileArchive(target)); + } + + /** + * Represents a single entry in the archive. + */ + interface Entry { + + /** + * Returns the name of the entry. + * @return the name of the entry + */ + String name(); + + /** + * Returns {@code true} if the entry represents a directory. + * @return if the entry is a directory + */ + boolean isDirectory(); + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/ClassPathIndexFile.java similarity index 54% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/ClassPathIndexFile.java index 5ad01e507127..dcc4384099a7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ClassPathIndexFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/ClassPathIndexFile.java @@ -14,24 +14,20 @@ * limitations under the License. */ -package org.springframework.boot.loader; +package org.springframework.boot.loader.launch; -import java.io.BufferedReader; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; import java.net.MalformedURLException; -import java.net.URISyntaxException; import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collections; +import java.nio.file.Files; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; /** - * A class path index file that provides ordering information for JARs. + * A class path index file that provides an ordered classpath for exploded JARs. * * @author Madhura Bhave * @author Phillip Webb @@ -40,11 +36,11 @@ final class ClassPathIndexFile { private final File root; - private final List lines; + private final Set lines; private ClassPathIndexFile(File root, List lines) { this.root = root; - this.lines = lines.stream().map(this::extractName).toList(); + this.lines = lines.stream().map(this::extractName).collect(Collectors.toCollection(LinkedHashSet::new)); } private String extractName(String line) { @@ -78,46 +74,23 @@ private URL asUrl(String line) { } } - static ClassPathIndexFile loadIfPossible(URL root, String location) throws IOException { - return loadIfPossible(asFile(root), location); - } - - private static ClassPathIndexFile loadIfPossible(File root, String location) throws IOException { + static ClassPathIndexFile loadIfPossible(File root, String location) throws IOException { return loadIfPossible(root, new File(root, location)); } private static ClassPathIndexFile loadIfPossible(File root, File indexFile) throws IOException { if (indexFile.exists() && indexFile.isFile()) { - try (InputStream inputStream = new FileInputStream(indexFile)) { - return new ClassPathIndexFile(root, loadLines(inputStream)); - } + List lines = Files.readAllLines(indexFile.toPath()) + .stream() + .filter(ClassPathIndexFile::lineHasText) + .toList(); + return new ClassPathIndexFile(root, lines); } return null; } - private static List loadLines(InputStream inputStream) throws IOException { - List lines = new ArrayList<>(); - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); - String line = reader.readLine(); - while (line != null) { - if (!line.trim().isEmpty()) { - lines.add(line); - } - line = reader.readLine(); - } - return Collections.unmodifiableList(lines); - } - - private static File asFile(URL url) { - if (!"file".equals(url.getProtocol())) { - throw new IllegalArgumentException("URL does not reference a file"); - } - try { - return new File(url.toURI()); - } - catch (URISyntaxException ex) { - return new File(url.getPath()); - } + private static boolean lineHasText(String line) { + return !line.trim().isEmpty(); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/ExecutableArchiveLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/ExecutableArchiveLauncher.java new file mode 100644 index 000000000000..fd6bd8cf527c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/ExecutableArchiveLauncher.java @@ -0,0 +1,136 @@ +/* + * Copyright 2012-2023 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.loader.launch; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +import org.springframework.boot.loader.launch.Archive.Entry; + +/** + * Base class for a {@link Launcher} backed by an executable archive. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Madhura Bhave + * @author Scott Frederick + * @since 3.2.0 + * @see JarLauncher + * @see WarLauncher + */ +public abstract class ExecutableArchiveLauncher extends Launcher { + + private static final String START_CLASS_ATTRIBUTE = "Start-Class"; + + protected static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index"; + + protected static final String DEFAULT_CLASSPATH_INDEX_FILE_NAME = "classpath.idx"; + + private final Archive archive; + + private final ClassPathIndexFile classPathIndex; + + public ExecutableArchiveLauncher() throws Exception { + this(Archive.create(Launcher.class)); + } + + protected ExecutableArchiveLauncher(Archive archive) throws Exception { + this.archive = archive; + this.classPathIndex = getClassPathIndex(this.archive); + } + + ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException { + if (!archive.isExploded()) { + return null; // Regular archives already have a defined order + } + String location = getClassPathIndexFileLocation(archive); + return ClassPathIndexFile.loadIfPossible(archive.getRootDirectory(), location); + } + + private String getClassPathIndexFileLocation(Archive archive) throws IOException { + Manifest manifest = archive.getManifest(); + Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null; + String location = (attributes != null) ? attributes.getValue(BOOT_CLASSPATH_INDEX_ATTRIBUTE) : null; + return (location != null) ? location : getEntryPathPrefix() + DEFAULT_CLASSPATH_INDEX_FILE_NAME; + } + + @Override + protected ClassLoader createClassLoader(Collection urls) throws Exception { + if (this.classPathIndex != null) { + urls = new ArrayList<>(urls); + urls.addAll(this.classPathIndex.getUrls()); + } + return super.createClassLoader(urls); + } + + @Override + protected final Archive getArchive() { + return this.archive; + } + + @Override + protected String getMainClass() throws Exception { + Manifest manifest = this.archive.getManifest(); + String mainClass = (manifest != null) ? manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE) : null; + if (mainClass == null) { + throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this); + } + return mainClass; + } + + @Override + protected Set getClassPathUrls() throws Exception { + return this.archive.getClassPathUrls(this::isIncludedOnClassPathAndNotIndexed, this::isSearchedDirectory); + } + + private boolean isIncludedOnClassPathAndNotIndexed(Entry entry) { + if (!isIncludedOnClassPath(entry)) { + return false; + } + return (this.classPathIndex == null) || !this.classPathIndex.containsEntry(entry.name()); + } + + /** + * Determine if the specified directory entry is a candidate for further searching. + * @param entry the entry to check + * @return {@code true} if the entry is a candidate for further searching + */ + protected boolean isSearchedDirectory(Archive.Entry entry) { + return ((getEntryPathPrefix() == null) || entry.name().startsWith(getEntryPathPrefix())) + && !isIncludedOnClassPath(entry); + } + + /** + * Determine if the specified entry is a nested item that should be added to the + * classpath. + * @param entry the entry to check + * @return {@code true} if the entry is a nested item (jar or directory) + */ + protected abstract boolean isIncludedOnClassPath(Archive.Entry entry); + + /** + * Return the path prefix for relevant entries in the archive. + * @return the entry path prefix + */ + protected abstract String getEntryPathPrefix(); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/ExplodedArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/ExplodedArchive.java new file mode 100644 index 000000000000..79cb729f60f7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/ExplodedArchive.java @@ -0,0 +1,139 @@ +/* + * Copyright 2012-2023 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.loader.launch; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; +import java.util.jar.Manifest; + +/** + * {@link Archive} implementation backed by an exploded archive directory. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Madhura Bhave + */ +class ExplodedArchive implements Archive { + + private static final Object NO_MANIFEST = new Object(); + + private static final Set SKIPPED_NAMES = Set.of(".", ".."); + + private static final Comparator entryComparator = Comparator.comparing(File::getAbsolutePath); + + private final File rootDirectory; + + private final String rootUriPath; + + private volatile Object manifest; + + /** + * Create a new {@link ExplodedArchive} instance. + * @param rootDirectory the root directory + */ + ExplodedArchive(File rootDirectory) { + if (!rootDirectory.exists() || !rootDirectory.isDirectory()) { + throw new IllegalArgumentException("Invalid source directory " + rootDirectory); + } + this.rootDirectory = rootDirectory; + this.rootUriPath = ExplodedArchive.this.rootDirectory.toURI().getPath(); + } + + @Override + public Manifest getManifest() throws IOException { + Object manifest = this.manifest; + if (manifest == null) { + manifest = loadManifest(); + this.manifest = manifest; + } + return (manifest != NO_MANIFEST) ? (Manifest) manifest : null; + } + + private Object loadManifest() throws IOException { + File file = new File(this.rootDirectory, "META-INF/MANIFEST.MF"); + if (!file.exists()) { + return NO_MANIFEST; + } + try (FileInputStream inputStream = new FileInputStream(file)) { + return new Manifest(inputStream); + } + } + + @Override + public Set getClassPathUrls(Predicate includeFilter, Predicate directorySearchFilter) + throws IOException { + Set urls = new LinkedHashSet<>(); + LinkedList files = new LinkedList<>(listFiles(this.rootDirectory)); + while (!files.isEmpty()) { + File file = files.poll(); + if (SKIPPED_NAMES.contains(file.getName())) { + continue; + } + String entryName = file.toURI().getPath().substring(this.rootUriPath.length()); + Entry entry = new FileArchiveEntry(entryName, file); + if (entry.isDirectory() && directorySearchFilter.test(entry)) { + files.addAll(0, listFiles(file)); + } + if (includeFilter.test(entry)) { + urls.add(file.toURI().toURL()); + } + } + return urls; + } + + private List listFiles(File file) { + File[] files = file.listFiles(); + if (files == null) { + return Collections.emptyList(); + } + Arrays.sort(files, entryComparator); + return Arrays.asList(files); + } + + @Override + public File getRootDirectory() { + return this.rootDirectory; + } + + @Override + public String toString() { + return this.rootDirectory.toString(); + } + + /** + * {@link Entry} backed by a File. + */ + private record FileArchiveEntry(String name, File file) implements Entry { + + @Override + public boolean isDirectory() { + return this.file.isDirectory(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarFileArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarFileArchive.java new file mode 100755 index 000000000000..3ccb32009fb3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarFileArchive.java @@ -0,0 +1,205 @@ +/* + * Copyright 2012-2023 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.loader.launch; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.UUID; +import java.util.function.Predicate; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.stream.Collectors; + +import org.springframework.boot.loader.net.protocol.jar.JarUrl; + +/** + * {@link Archive} implementation backed by a {@link JarFile}. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +class JarFileArchive implements Archive { + + private static final String UNPACK_MARKER = "UNPACK:"; + + private static final FileAttribute[] NO_FILE_ATTRIBUTES = {}; + + private static final FileAttribute[] DIRECTORY_PERMISSION_ATTRIBUTES = asFileAttributes( + PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE); + + private static final FileAttribute[] FILE_PERMISSION_ATTRIBUTES = asFileAttributes( + PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE); + + private static final Path TEMP = Paths.get(System.getProperty("java.io.tmpdir")); + + private final File file; + + private final JarFile jarFile; + + private volatile Path tempUnpackDirectory; + + JarFileArchive(File file) throws IOException { + this(file, new JarFile(file)); + } + + private JarFileArchive(File file, JarFile jarFile) { + this.file = file; + this.jarFile = jarFile; + } + + @Override + public Manifest getManifest() throws IOException { + return this.jarFile.getManifest(); + } + + @Override + public Set getClassPathUrls(Predicate includeFilter, Predicate directorySearchFilter) + throws IOException { + return this.jarFile.stream() + .map(JarArchiveEntry::new) + .filter(includeFilter) + .map(this::getNestedJarUrl) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + private URL getNestedJarUrl(JarArchiveEntry archiveEntry) { + try { + JarEntry jarEntry = archiveEntry.jarEntry(); + String comment = jarEntry.getComment(); + if (comment != null && comment.startsWith(UNPACK_MARKER)) { + return getUnpackedNestedJarUrl(jarEntry); + } + return JarUrl.create(this.file, jarEntry); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private URL getUnpackedNestedJarUrl(JarEntry jarEntry) throws IOException { + String name = jarEntry.getName(); + if (name.lastIndexOf('/') != -1) { + name = name.substring(name.lastIndexOf('/') + 1); + } + Path path = getTempUnpackDirectory().resolve(name); + if (!Files.exists(path) || Files.size(path) != jarEntry.getSize()) { + unpack(jarEntry, path); + } + return JarUrl.create(path.toFile()); + } + + private Path getTempUnpackDirectory() { + Path tempUnpackDirectory = this.tempUnpackDirectory; + if (tempUnpackDirectory != null) { + return tempUnpackDirectory; + } + synchronized (TEMP) { + tempUnpackDirectory = this.tempUnpackDirectory; + if (tempUnpackDirectory == null) { + tempUnpackDirectory = createUnpackDirectory(TEMP); + this.tempUnpackDirectory = tempUnpackDirectory; + } + } + return tempUnpackDirectory; + } + + private Path createUnpackDirectory(Path parent) { + int attempts = 0; + String fileName = Paths.get(this.jarFile.getName()).getFileName().toString(); + while (attempts++ < 100) { + Path unpackDirectory = parent.resolve(fileName + "-spring-boot-libs-" + UUID.randomUUID()); + try { + createDirectory(unpackDirectory); + return unpackDirectory; + } + catch (IOException ex) { + // Ignore + } + } + throw new IllegalStateException("Failed to create unpack directory in directory '" + parent + "'"); + } + + private void createDirectory(Path path) throws IOException { + Files.createDirectory(path, getFileAttributes(path, DIRECTORY_PERMISSION_ATTRIBUTES)); + } + + private void unpack(JarEntry entry, Path path) throws IOException { + createFile(path); + path.toFile().deleteOnExit(); + try (InputStream in = this.jarFile.getInputStream(entry)) { + Files.copy(in, path, StandardCopyOption.REPLACE_EXISTING); + } + } + + private void createFile(Path path) throws IOException { + Files.createFile(path, getFileAttributes(path, FILE_PERMISSION_ATTRIBUTES)); + } + + private FileAttribute[] getFileAttributes(Path path, FileAttribute[] permissionAttributes) { + return (!supportsPosix(path.getFileSystem())) ? NO_FILE_ATTRIBUTES : permissionAttributes; + } + + private boolean supportsPosix(FileSystem fileSystem) { + return fileSystem.supportedFileAttributeViews().contains("posix"); + } + + @Override + public void close() throws IOException { + this.jarFile.close(); + } + + @Override + public String toString() { + return this.file.toString(); + } + + private static FileAttribute[] asFileAttributes(PosixFilePermission... permissions) { + return new FileAttribute[] { PosixFilePermissions.asFileAttribute(Set.of(permissions)) }; + } + + /** + * {@link Entry} implementation backed by a {@link JarEntry}. + */ + private record JarArchiveEntry(JarEntry jarEntry) implements Entry { + + @Override + public String name() { + return this.jarEntry.getName(); + } + + @Override + public boolean isDirectory() { + return this.jarEntry.isDirectory(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java index 5beb8d109640..ecabbc1fdf1d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java @@ -17,18 +17,41 @@ package org.springframework.boot.loader.launch; /** - * Repackaged {@link org.springframework.boot.loader.JarLauncher}. + * {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are + * included inside a {@code /BOOT-INF/lib} directory and that application classes are + * included inside a {@code /BOOT-INF/classes} directory. * * @author Phillip Webb + * @author Andy Wilkinson + * @author Madhura Bhave + * @author Scott Frederick * @since 3.2.0 */ -public final class JarLauncher { +public class JarLauncher extends ExecutableArchiveLauncher { - private JarLauncher() { + public JarLauncher() throws Exception { + } + + protected JarLauncher(Archive archive) throws Exception { + super(archive); + } + + @Override + protected boolean isIncludedOnClassPath(Archive.Entry entry) { + String name = entry.name(); + if (entry.isDirectory()) { + return name.equals("BOOT-INF/classes/"); + } + return name.startsWith("BOOT-INF/lib/"); + } + + @Override + protected String getEntryPathPrefix() { + return "BOOT-INF/"; } public static void main(String[] args) throws Exception { - org.springframework.boot.loader.JarLauncher.main(args); + new JarLauncher().launch(args); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarModeRunner.java similarity index 77% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarModeRunner.java index 44fcb7902ee7..4805a633d48c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarModeRunner.java @@ -14,27 +14,27 @@ * limitations under the License. */ -package org.springframework.boot.loader.jarmode; +package org.springframework.boot.loader.launch; import java.util.List; +import org.springframework.boot.loader.jarmode.JarMode; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.util.ClassUtils; /** - * Delegate class used to launch the fat jar in a specific mode. + * Delegate class used to run the nested jar in a specific mode. * * @author Phillip Webb - * @since 2.3.0 */ -public final class JarModeLauncher { +final class JarModeRunner { - static final String DISABLE_SYSTEM_EXIT = JarModeLauncher.class.getName() + ".DISABLE_SYSTEM_EXIT"; + static final String DISABLE_SYSTEM_EXIT = JarModeRunner.class.getName() + ".DISABLE_SYSTEM_EXIT"; - private JarModeLauncher() { + private JarModeRunner() { } - public static void main(String[] args) { + static void main(String[] args) { String mode = System.getProperty("jarmode"); List candidates = SpringFactoriesLoader.loadFactories(JarMode.class, ClassUtils.getDefaultClassLoader()); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/LaunchedClassLoader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/LaunchedClassLoader.java new file mode 100644 index 000000000000..c604df0487a4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/LaunchedClassLoader.java @@ -0,0 +1,189 @@ +/* + * Copyright 2012-2023 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.loader.launch; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.function.Supplier; +import java.util.jar.Manifest; + +import org.springframework.boot.loader.net.protocol.jar.JarUrlClassLoader; + +/** + * {@link ClassLoader} used by the {@link Launcher}. + * + * @author Phillip Webb + * @author Dave Syer + * @author Andy Wilkinson + * @since 3.2.0 + */ +public class LaunchedClassLoader extends JarUrlClassLoader { + + private static final String JAR_MODE_PACKAGE_PREFIX = "org.springframework.boot.loader.jarmode."; + + private static final String JAR_MODE_RUNNER_CLASS_NAME = JarModeRunner.class.getName(); + + static { + ClassLoader.registerAsParallelCapable(); + } + + private final boolean exploded; + + private final Archive rootArchive; + + private final Object definePackageLock = new Object(); + + private volatile DefinePackageCallType definePackageCallType; + + /** + * Create a new {@link LaunchedClassLoader} instance. + * @param exploded if the underlying archive is exploded + * @param urls the URLs from which to load classes and resources + * @param parent the parent class loader for delegation + */ + public LaunchedClassLoader(boolean exploded, URL[] urls, ClassLoader parent) { + this(exploded, null, urls, parent); + } + + /** + * Create a new {@link LaunchedClassLoader} instance. + * @param exploded if the underlying archive is exploded + * @param rootArchive the root archive or {@code null} + * @param urls the URLs from which to load classes and resources + * @param parent the parent class loader for delegation + */ + public LaunchedClassLoader(boolean exploded, Archive rootArchive, URL[] urls, ClassLoader parent) { + super(urls, parent); + this.exploded = exploded; + this.rootArchive = rootArchive; + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.startsWith(JAR_MODE_PACKAGE_PREFIX) || name.equals(JAR_MODE_RUNNER_CLASS_NAME)) { + try { + Class result = loadClassInLaunchedClassLoader(name); + if (resolve) { + resolveClass(result); + } + return result; + } + catch (ClassNotFoundException ex) { + // Ignore + } + } + return super.loadClass(name, resolve); + } + + private Class loadClassInLaunchedClassLoader(String name) throws ClassNotFoundException { + try { + String internalName = name.replace('.', '/') + ".class"; + try (InputStream inputStream = getParent().getResourceAsStream(internalName); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + if (inputStream == null) { + throw new ClassNotFoundException(name); + } + inputStream.transferTo(outputStream); + byte[] bytes = outputStream.toByteArray(); + Class definedClass = defineClass(name, bytes, 0, bytes.length); + definePackageIfNecessary(name); + return definedClass; + } + } + catch (IOException ex) { + throw new ClassNotFoundException("Cannot load resource for class [" + name + "]", ex); + } + } + + @Override + protected Package definePackage(String name, Manifest man, URL url) throws IllegalArgumentException { + return (!this.exploded) ? super.definePackage(name, man, url) : definePackageForExploded(name, man, url); + } + + private Package definePackageForExploded(String name, Manifest man, URL url) { + synchronized (this.definePackageLock) { + return definePackage(DefinePackageCallType.MANIFEST, () -> super.definePackage(name, man, url)); + } + } + + @Override + protected Package definePackage(String name, String specTitle, String specVersion, String specVendor, + String implTitle, String implVersion, String implVendor, URL sealBase) throws IllegalArgumentException { + if (!this.exploded) { + return super.definePackage(name, specTitle, specVersion, specVendor, implTitle, implVersion, implVendor, + sealBase); + } + return definePackageForExploded(name, sealBase, () -> super.definePackage(name, specTitle, specVersion, + specVendor, implTitle, implVersion, implVendor, sealBase)); + } + + private Package definePackageForExploded(String name, URL sealBase, Supplier call) { + synchronized (this.definePackageLock) { + if (this.definePackageCallType == null) { + // We're not part of a call chain which means that the URLClassLoader + // is trying to define a package for our exploded JAR. We use the + // manifest version to ensure package attributes are set + Manifest manifest = getManifest(this.rootArchive); + if (manifest != null) { + return definePackage(name, manifest, sealBase); + } + } + return definePackage(DefinePackageCallType.ATTRIBUTES, call); + } + } + + private T definePackage(DefinePackageCallType type, Supplier call) { + DefinePackageCallType existingType = this.definePackageCallType; + try { + this.definePackageCallType = type; + return call.get(); + } + finally { + this.definePackageCallType = existingType; + } + } + + private Manifest getManifest(Archive archive) { + try { + return (archive != null) ? archive.getManifest() : null; + } + catch (IOException ex) { + return null; + } + } + + /** + * The different types of call made to define a package. We track these for exploded + * jars so that we can detect packages that should have manifest attributes applied. + */ + private enum DefinePackageCallType { + + /** + * A define package call from a resource that has a manifest. + */ + MANIFEST, + + /** + * A define package call with a direct set of attributes. + */ + ATTRIBUTES + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/Launcher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/Launcher.java new file mode 100644 index 000000000000..2cae9b06b916 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/Launcher.java @@ -0,0 +1,125 @@ +/* + * Copyright 2012-2023 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.loader.launch; + +import java.io.UncheckedIOException; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.Collection; +import java.util.Set; + +import org.springframework.boot.loader.net.protocol.Handlers; + +/** + * Base class for launchers that can start an application with a fully configured + * classpath. + * + * @author Phillip Webb + * @author Dave Syer + * @since 3.2.0 + */ +public abstract class Launcher { + + private static final String JAR_MODE_RUNNER_CLASS_NAME = JarModeRunner.class.getName(); + + /** + * Launch the application. This method is the initial entry point that should be + * called by a subclass {@code public static void main(String[] args)} method. + * @param args the incoming arguments + * @throws Exception if the application fails to launch + */ + protected void launch(String[] args) throws Exception { + if (!isExploded()) { + Handlers.register(); + } + try { + ClassLoader classLoader = createClassLoader(getClassPathUrls()); + String jarMode = System.getProperty("jarmode"); + String mainClassName = hasLength(jarMode) ? JAR_MODE_RUNNER_CLASS_NAME : getMainClass(); + launch(classLoader, mainClassName, args); + } + catch (UncheckedIOException ex) { + throw ex.getCause(); + } + } + + private boolean hasLength(String jarMode) { + return (jarMode != null) && !jarMode.isEmpty(); + } + + /** + * Create a classloader for the specified archives. + * @param urls the classpath URLs + * @return the classloader + * @throws Exception if the classloader cannot be created + */ + protected ClassLoader createClassLoader(Collection urls) throws Exception { + return createClassLoader(urls.toArray(new URL[0])); + } + + private ClassLoader createClassLoader(URL[] urls) { + ClassLoader parent = getClass().getClassLoader(); + return new LaunchedClassLoader(isExploded(), getArchive(), urls, parent); + } + + /** + * Launch the application given the archive file and a fully configured classloader. + * @param classLoader the classloader + * @param mainClassName the main class to run + * @param args the incoming arguments + * @throws Exception if the launch fails + */ + protected void launch(ClassLoader classLoader, String mainClassName, String[] args) throws Exception { + Thread.currentThread().setContextClassLoader(classLoader); + Class mainClass = Class.forName(mainClassName, false, classLoader); + Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); + mainMethod.setAccessible(true); + mainMethod.invoke(null, new Object[] { args }); + } + + /** + * Returns if the launcher is running in an exploded mode. If this method returns + * {@code true} then only regular JARs are supported and the additional URL and + * ClassLoader support infrastructure can be optimized. + * @return if the jar is exploded. + */ + protected boolean isExploded() { + Archive archive = getArchive(); + return (archive != null) && archive.isExploded(); + } + + /** + * Return the archive being launched or {@code null} if there is no archive. + * @return the launched archive + */ + protected abstract Archive getArchive(); + + /** + * Returns the main class that should be launched. + * @return the name of the main class + * @throws Exception if the main class cannot be obtained + */ + protected abstract String getMainClass() throws Exception; + + /** + * Returns the archives that will be used to construct the class path. + * @return the class path archives + * @throws Exception if the class path archives cannot be obtained + */ + protected abstract Set getClassPathUrls() throws Exception; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java index d80fb0bb7105..8b88484df48f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java @@ -16,19 +16,585 @@ package org.springframework.boot.loader.launch; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.Set; +import java.util.function.Predicate; +import java.util.jar.Manifest; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.boot.loader.launch.Archive.Entry; +import org.springframework.boot.loader.log.DebugLogger; +import org.springframework.boot.loader.net.protocol.jar.JarUrl; + /** - * Repackaged {@link org.springframework.boot.loader.PropertiesLauncher}. + * {@link Launcher} for archives with user-configured classpath and main class through a + * properties file. + *

    + * Looks in various places for a properties file to extract loader settings, defaulting to + * {@code loader.properties} either on the current classpath or in the current working + * directory. The name of the properties file can be changed by setting a System property + * {@code loader.config.name} (e.g. {@code -Dloader.config.name=my} will look for + * {@code my.properties}. If that file doesn't exist then tries + * {@code loader.config.location} (with allowed prefixes {@code classpath:} and + * {@code file:} or any valid URL). Once that file is located turns it into Properties and + * extracts optional values (which can also be provided overridden as System properties in + * case the file doesn't exist): + *

      + *
    • {@code loader.path}: a comma-separated list of directories (containing file + * resources and/or nested archives in *.jar or *.zip or archives) or archives to append + * to the classpath. {@code BOOT-INF/classes,BOOT-INF/lib} in the application archive are + * always used
    • + *
    • {@code loader.main}: the main method to delegate execution to once the class loader + * is set up. No default, but will fall back to looking for a {@code Start-Class} in a + * {@code MANIFEST.MF}, if there is one in ${loader.home}/META-INF.
    • + *
    * + * @author Dave Syer + * @author Janne Valkealahti + * @author Andy Wilkinson * @author Phillip Webb * @since 3.2.0 */ -public final class PropertiesLauncher { +public class PropertiesLauncher extends Launcher { + + /** + * Properties key for main class. As a manifest entry can also be specified as + * {@code Start-Class}. + */ + public static final String MAIN = "loader.main"; + + /** + * Properties key for classpath entries (directories possibly containing jars or + * jars). Multiple entries can be specified using a comma-separated list. {@code + * BOOT-INF/classes,BOOT-INF/lib} in the application archive are always used. + */ + public static final String PATH = "loader.path"; + + /** + * Properties key for home directory. This is the location of external configuration + * if not on classpath, and also the base path for any relative paths in the + * {@link #PATH loader path}. Defaults to current working directory ( + * ${user.dir}). + */ + public static final String HOME = "loader.home"; + + /** + * Properties key for default command line arguments. These arguments (if present) are + * prepended to the main method arguments before launching. + */ + public static final String ARGS = "loader.args"; + + /** + * Properties key for name of external configuration file (excluding suffix). Defaults + * to "application". Ignored if {@link #CONFIG_LOCATION loader config location} is + * provided instead. + */ + public static final String CONFIG_NAME = "loader.config.name"; + + /** + * Properties key for config file location (including optional classpath:, file: or + * URL prefix). + */ + public static final String CONFIG_LOCATION = "loader.config.location"; + + /** + * Properties key for boolean flag (default false) which, if set, will cause the + * external configuration properties to be copied to System properties (assuming that + * is allowed by Java security). + */ + public static final String SET_SYSTEM_PROPERTIES = "loader.system"; + + private static final URL[] NO_URLS = new URL[0]; + + private static final Pattern WORD_SEPARATOR = Pattern.compile("\\W+"); + + private static final String NESTED_ARCHIVE_SEPARATOR = "!" + File.separator; + + private static final String JAR_FILE_PREFIX = "jar:file:"; + + private static final DebugLogger debug = DebugLogger.get(PropertiesLauncher.class); - private PropertiesLauncher() { + private final Archive archive; + + private final File homeDirectory; + + private final List paths; + + private final Properties properties = new Properties(); + + public PropertiesLauncher() throws Exception { + this.archive = Archive.create(Launcher.class); + this.homeDirectory = getHomeDirectory(); + initializeProperties(); + this.paths = getPaths(); + } + + protected File getHomeDirectory() throws Exception { + return new File(getPropertyWithDefault(HOME, "${user.dir}")); + } + + private void initializeProperties() throws Exception { + List configs = new ArrayList<>(); + if (getProperty(CONFIG_LOCATION) != null) { + configs.add(getProperty(CONFIG_LOCATION)); + } + else { + String[] names = getPropertyWithDefault(CONFIG_NAME, "loader").split(","); + for (String name : names) { + String propertiesFile = name + ".properties"; + configs.add("file:" + this.homeDirectory + "/" + propertiesFile); + configs.add("classpath:" + propertiesFile); + configs.add("classpath:BOOT-INF/classes/" + propertiesFile); + } + } + for (String config : configs) { + try (InputStream resource = getResource(config)) { + if (resource == null) { + debug.log("Not found: %s", config); + continue; + } + debug.log("Found: %s", config); + loadResource(resource); + return; // Load the first one we find + } + } + } + + private InputStream getResource(String config) throws Exception { + if (config.startsWith("classpath:")) { + return getClasspathResource(config.substring("classpath:".length())); + } + config = handleUrl(config); + if (isUrl(config)) { + return getURLResource(config); + } + return getFileResource(config); + } + + private InputStream getClasspathResource(String config) { + config = stripLeadingSlashes(config); + config = "/" + config; + debug.log("Trying classpath: %s", config); + return getClass().getResourceAsStream(config); + } + + private String handleUrl(String path) { + if (path.startsWith("jar:file:") || path.startsWith("file:")) { + path = URLDecoder.decode(path, StandardCharsets.UTF_8); + if (path.startsWith("file:")) { + path = path.substring("file:".length()); + if (path.startsWith("//")) { + path = path.substring(2); + } + } + } + return path; + } + + private boolean isUrl(String config) { + return config.contains("://"); + } + + private InputStream getURLResource(String config) throws Exception { + URL url = new URL(config); + if (exists(url)) { + URLConnection connection = url.openConnection(); + try { + return connection.getInputStream(); + } + catch (IOException ex) { + disconnect(connection); + throw ex; + } + } + return null; + } + + private boolean exists(URL url) throws IOException { + URLConnection connection = url.openConnection(); + try { + connection.setUseCaches(connection.getClass().getSimpleName().startsWith("JNLP")); + if (connection instanceof HttpURLConnection httpConnection) { + httpConnection.setRequestMethod("HEAD"); + int responseCode = httpConnection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { + return true; + } + if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) { + return false; + } + } + return (connection.getContentLength() >= 0); + } + finally { + disconnect(connection); + } + } + + private void disconnect(URLConnection connection) { + if (connection instanceof HttpURLConnection httpConnection) { + httpConnection.disconnect(); + } + } + + private InputStream getFileResource(String config) throws Exception { + File file = new File(config); + debug.log("Trying file: %s", config); + return (!file.canRead()) ? null : new FileInputStream(file); + } + + private void loadResource(InputStream resource) throws Exception { + this.properties.load(resource); + resolvePropertyPlaceholders(); + if ("true".equalsIgnoreCase(getProperty(SET_SYSTEM_PROPERTIES))) { + addToSystemProperties(); + } + } + + private void resolvePropertyPlaceholders() { + for (String name : this.properties.stringPropertyNames()) { + String value = this.properties.getProperty(name); + String resolved = SystemPropertyUtils.resolvePlaceholders(this.properties, value); + if (resolved != null) { + this.properties.put(name, resolved); + } + } + } + + private void addToSystemProperties() { + debug.log("Adding resolved properties to System properties"); + for (String name : this.properties.stringPropertyNames()) { + String value = this.properties.getProperty(name); + System.setProperty(name, value); + } + } + + private List getPaths() throws Exception { + String path = getProperty(PATH); + List paths = (path != null) ? parsePathsProperty(path) : Collections.emptyList(); + debug.log("Nested archive paths: %s", this.paths); + return paths; + } + + private List parsePathsProperty(String commaSeparatedPaths) { + List paths = new ArrayList<>(); + for (String path : commaSeparatedPaths.split(",")) { + path = cleanupPath(path); + // "" means the user wants root of archive but not current directory + path = (path.isEmpty()) ? "/" : path; + paths.add(path); + } + if (paths.isEmpty()) { + paths.add("lib"); + } + return paths; + } + + private String cleanupPath(String path) { + path = path.trim(); + // No need for current dir path + if (path.startsWith("./")) { + path = path.substring(2); + } + if (isArchive(path)) { + return path; + } + if (path.endsWith("/*")) { + return path.substring(0, path.length() - 1); + } + // It's a directory + return (!path.endsWith("/") && !path.equals(".")) ? path + "/" : path; + } + + @Override + protected ClassLoader createClassLoader(Collection urls) throws Exception { + String loaderClassName = getProperty("loader.classLoader"); + if (loaderClassName == null) { + return super.createClassLoader(urls); + } + ClassLoader parent = getClass().getClassLoader(); + ClassLoader classLoader = new LaunchedClassLoader(false, urls.toArray(new URL[0]), parent); + debug.log("Classpath for custom loader: %s", urls); + classLoader = wrapWithCustomClassLoader(classLoader, loaderClassName); + debug.log("Using custom class loader: %s", loaderClassName); + return classLoader; + } + + private ClassLoader wrapWithCustomClassLoader(ClassLoader parent, String loaderClassName) throws Exception { + Instantiator instantiator = new Instantiator<>(parent, loaderClassName); + ClassLoader loader = instantiator.declaredConstructor(ClassLoader.class).newInstance(parent); + loader = (loader != null) ? loader + : instantiator.declaredConstructor(URL[].class, ClassLoader.class).newInstance(NO_URLS, parent); + loader = (loader != null) ? loader : instantiator.constructWithoutParameters(); + if (loader != null) { + return loader; + } + throw new IllegalStateException("Unable to create class loader for " + loaderClassName); + } + + @Override + protected Archive getArchive() { + return null; // We don't have a single archive and are not exploded. + } + + @Override + protected String getMainClass() throws Exception { + String mainClass = getProperty(MAIN, "Start-Class"); + if (mainClass == null) { + throw new IllegalStateException("No '%s' or 'Start-Class' specified".formatted(MAIN)); + } + return mainClass; + } + + protected String[] getArgs(String... args) throws Exception { + String loaderArgs = getProperty(ARGS); + return (loaderArgs != null) ? merge(loaderArgs.split("\\s+"), args) : args; + } + + private String[] merge(String[] a1, String[] a2) { + String[] result = new String[a1.length + a2.length]; + System.arraycopy(a1, 0, result, 0, a1.length); + System.arraycopy(a2, 0, result, a1.length, a2.length); + return result; + } + + private String getProperty(String name) throws Exception { + return getProperty(name, null, null); + } + + private String getProperty(String name, String manifestKey) throws Exception { + return getProperty(name, manifestKey, null); + } + + private String getPropertyWithDefault(String name, String defaultValue) throws Exception { + return getProperty(name, null, defaultValue); + } + + private String getProperty(String name, String manifestKey, String defaultValue) throws Exception { + manifestKey = (manifestKey != null) ? manifestKey : toCamelCase(name.replace('.', '-')); + String value = SystemPropertyUtils.getProperty(name); + if (value != null) { + return getResolvedProperty(name, manifestKey, value, "environment"); + } + if (this.properties.containsKey(name)) { + value = this.properties.getProperty(name); + return getResolvedProperty(name, manifestKey, value, "properties"); + } + // Prefer home dir for MANIFEST if there is one + if (this.homeDirectory != null) { + try { + try (ExplodedArchive explodedArchive = new ExplodedArchive(this.homeDirectory)) { + value = getManifestValue(explodedArchive, manifestKey); + if (value != null) { + return getResolvedProperty(name, manifestKey, value, "home directory manifest"); + } + } + } + catch (IllegalStateException ex) { + // Ignore + } + } + // Otherwise try the root archive + value = getManifestValue(this.archive, manifestKey); + if (value != null) { + return getResolvedProperty(name, manifestKey, value, "manifest"); + } + return SystemPropertyUtils.resolvePlaceholders(this.properties, defaultValue); + } + + String getManifestValue(Archive archive, String manifestKey) throws Exception { + Manifest manifest = archive.getManifest(); + return (manifest != null) ? manifest.getMainAttributes().getValue(manifestKey) : null; + } + + private String getResolvedProperty(String name, String manifestKey, String value, String from) { + value = SystemPropertyUtils.resolvePlaceholders(this.properties, value); + String altName = (manifestKey != null && !manifestKey.equals(name)) ? "[%s] ".formatted(manifestKey) : ""; + debug.log("Property '%s'%s from %s: %s", name, altName, from, value); + return value; + + } + + void close() throws Exception { + if (this.archive != null) { + this.archive.close(); + } + } + + public static String toCamelCase(CharSequence string) { + if (string == null) { + return null; + } + StringBuilder result = new StringBuilder(); + Matcher matcher = WORD_SEPARATOR.matcher(string); + int pos = 0; + while (matcher.find()) { + result.append(capitalize(string.subSequence(pos, matcher.end()).toString())); + pos = matcher.end(); + } + result.append(capitalize(string.subSequence(pos, string.length()).toString())); + return result.toString(); + } + + private static String capitalize(String str) { + return Character.toUpperCase(str.charAt(0)) + str.substring(1); + } + + @Override + protected Set getClassPathUrls() throws Exception { + Set urls = new LinkedHashSet<>(); + for (String path : getPaths()) { + path = cleanupPath(handleUrl(path)); + urls.addAll(getClassPathUrlsForPath(path)); + } + return urls; + } + + private Set getClassPathUrlsForPath(String path) throws Exception { + File file = (!isAbsolutePath(path)) ? new File(this.homeDirectory, path) : new File(path); + Set urls = new LinkedHashSet<>(); + if (!"/".equals(path)) { + if (file.isDirectory()) { + try (ExplodedArchive explodedArchive = new ExplodedArchive(file)) { + debug.log("Adding classpath entries from directory %s", file); + urls.add(file.toURI().toURL()); + urls.addAll(explodedArchive.getClassPathUrls(this::isArchive)); + } + } + } + if (!file.getPath().contains(NESTED_ARCHIVE_SEPARATOR) && isArchive(file.getName())) { + debug.log("Adding classpath entries from jar/zip archive %s", path); + urls.add(file.toURI().toURL()); + } + Set nested = getClassPathUrlsForNested(path); + if (!nested.isEmpty()) { + debug.log("Adding classpath entries from nested %s", path); + urls.addAll(nested); + } + return urls; + } + + private Set getClassPathUrlsForNested(String path) throws Exception { + boolean isJustArchive = isArchive(path); + if (!path.equals("/") && path.startsWith("/") + || (this.archive.isExploded() && this.archive.getRootDirectory().equals(this.homeDirectory))) { + return Collections.emptySet(); + } + File file = null; + if (isJustArchive) { + File candidate = new File(this.homeDirectory, path); + if (candidate.exists()) { + file = candidate; + path = ""; + } + } + int separatorIndex = path.indexOf('!'); + if (separatorIndex != -1) { + file = (!path.startsWith(JAR_FILE_PREFIX)) ? new File(this.homeDirectory, path.substring(0, separatorIndex)) + : new File(path.substring(JAR_FILE_PREFIX.length(), separatorIndex)); + path = path.substring(separatorIndex + 1); + path = stripLeadingSlashes(path); + } + if (path.equals("/") || path.equals("./") || path.equals(".")) { + // The prefix for nested jars is actually empty if it's at the root + path = ""; + } + Archive archive = (file != null) ? new JarFileArchive(file) : this.archive; + try { + Set urls = new LinkedHashSet<>(archive.getClassPathUrls(includeByPrefix(path))); + if (!isJustArchive && file != null && path.isEmpty()) { + urls.add(JarUrl.create(file)); + } + return urls; + } + finally { + if (archive != this.archive) { + archive.close(); + } + } + } + + private Predicate includeByPrefix(String prefix) { + return (entry) -> (entry.isDirectory() && entry.name().equals(prefix)) + || (isArchive(entry) && entry.name().startsWith(prefix)); + } + + private boolean isArchive(Entry entry) { + return isArchive(entry.name()); + } + + private boolean isArchive(String name) { + name = name.toLowerCase(Locale.ENGLISH); + return name.endsWith(".jar") || name.endsWith(".zip"); + } + + private boolean isAbsolutePath(String root) { + // Windows contains ":" others start with "/" + return root.contains(":") || root.startsWith("/"); + } + + private String stripLeadingSlashes(String string) { + while (string.startsWith("/")) { + string = string.substring(1); + } + return string; } public static void main(String[] args) throws Exception { - org.springframework.boot.loader.PropertiesLauncher.main(args); + PropertiesLauncher launcher = new PropertiesLauncher(); + args = launcher.getArgs(args); + launcher.launch(args); + } + + /** + * Utility to help instantiate objects. + */ + private record Instantiator(ClassLoader parent, Class type) { + + Instantiator(ClassLoader parent, String className) throws ClassNotFoundException { + this(parent, Class.forName(className, true, parent)); + } + + T constructWithoutParameters() throws Exception { + return declaredConstructor().newInstance(); + } + + Using declaredConstructor(Class... parameterTypes) { + return new Using<>(this, parameterTypes); + } + + private record Using(Instantiator instantiator, Class... parameterTypes) { + + @SuppressWarnings("unchecked") + T newInstance(Object... initargs) throws Exception { + try { + Constructor constructor = this.instantiator.type().getDeclaredConstructor(this.parameterTypes); + constructor.setAccessible(true); + return (T) constructor.newInstance(initargs); + } + catch (NoSuchMethodException ex) { + return null; + } + } + + } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/SystemPropertyUtils.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/SystemPropertyUtils.java new file mode 100644 index 000000000000..5efb96f3540c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/SystemPropertyUtils.java @@ -0,0 +1,151 @@ +/* + * Copyright 2012-2023 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.loader.launch; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Properties; +import java.util.Set; + +/** + * Internal helper class adapted from Spring Framework for resolving placeholders in + * texts. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @author Dave Syer + * @author Phillip Webb + */ +final class SystemPropertyUtils { + + private static final String PLACEHOLDER_PREFIX = "${"; + + private static final String PLACEHOLDER_SUFFIX = "}"; + + private static final String VALUE_SEPARATOR = ":"; + + private static final String SIMPLE_PREFIX = PLACEHOLDER_PREFIX.substring(1); + + private SystemPropertyUtils() { + } + + static String resolvePlaceholders(Properties properties, String text) { + return (text != null) ? parseStringValue(properties, text, text, new HashSet<>()) : null; + } + + private static String parseStringValue(Properties properties, String value, String current, + Set visitedPlaceholders) { + StringBuilder result = new StringBuilder(current); + int startIndex = current.indexOf(PLACEHOLDER_PREFIX); + while (startIndex != -1) { + int endIndex = findPlaceholderEndIndex(result, startIndex); + if (endIndex == -1) { + startIndex = -1; + continue; + } + String placeholder = result.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex); + String originalPlaceholder = placeholder; + if (!visitedPlaceholders.add(originalPlaceholder)) { + throw new IllegalArgumentException( + "Circular placeholder reference '" + originalPlaceholder + "' in property definitions"); + } + placeholder = parseStringValue(properties, value, placeholder, visitedPlaceholders); + String propertyValue = resolvePlaceholder(properties, value, placeholder); + if (propertyValue == null) { + int separatorIndex = placeholder.indexOf(VALUE_SEPARATOR); + if (separatorIndex != -1) { + String actualPlaceholder = placeholder.substring(0, separatorIndex); + String defaultValue = placeholder.substring(separatorIndex + VALUE_SEPARATOR.length()); + propertyValue = resolvePlaceholder(properties, value, actualPlaceholder); + propertyValue = (propertyValue != null) ? propertyValue : defaultValue; + } + } + if (propertyValue != null) { + propertyValue = parseStringValue(properties, value, propertyValue, visitedPlaceholders); + result.replace(startIndex, endIndex + PLACEHOLDER_SUFFIX.length(), propertyValue); + startIndex = result.indexOf(PLACEHOLDER_PREFIX, startIndex + propertyValue.length()); + } + else { + startIndex = result.indexOf(PLACEHOLDER_PREFIX, endIndex + PLACEHOLDER_SUFFIX.length()); + } + visitedPlaceholders.remove(originalPlaceholder); + } + return result.toString(); + } + + private static String resolvePlaceholder(Properties properties, String text, String placeholderName) { + String propertyValue = getProperty(placeholderName, null, text); + if (propertyValue != null) { + return propertyValue; + } + return (properties != null) ? properties.getProperty(placeholderName) : null; + } + + static String getProperty(String key) { + return getProperty(key, null, ""); + } + + private static String getProperty(String key, String defaultValue, String text) { + try { + String value = System.getProperty(key); + value = (value != null) ? value : System.getenv(key); + value = (value != null) ? value : System.getenv(key.replace('.', '_')); + value = (value != null) ? value : System.getenv(key.toUpperCase(Locale.ENGLISH).replace('.', '_')); + return (value != null) ? value : defaultValue; + } + catch (Throwable ex) { + System.err.println("Could not resolve key '" + key + "' in '" + text + + "' as system property or in environment: " + ex); + return defaultValue; + } + } + + private static int findPlaceholderEndIndex(CharSequence buf, int startIndex) { + int index = startIndex + PLACEHOLDER_PREFIX.length(); + int withinNestedPlaceholder = 0; + while (index < buf.length()) { + if (substringMatch(buf, index, PLACEHOLDER_SUFFIX)) { + if (withinNestedPlaceholder > 0) { + withinNestedPlaceholder--; + index = index + PLACEHOLDER_SUFFIX.length(); + } + else { + return index; + } + } + else if (substringMatch(buf, index, SIMPLE_PREFIX)) { + withinNestedPlaceholder++; + index = index + SIMPLE_PREFIX.length(); + } + else { + index++; + } + } + return -1; + } + + private static boolean substringMatch(CharSequence str, int index, CharSequence substring) { + for (int j = 0; j < substring.length(); j++) { + int i = index + j; + if (i >= str.length() || str.charAt(i) != substring.charAt(j)) { + return false; + } + } + return true; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java index 9392d3bf2b45..a74e63c4abcd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java @@ -17,18 +17,40 @@ package org.springframework.boot.loader.launch; /** - * Repackaged {@link org.springframework.boot.loader.WarLauncher}. + * {@link Launcher} for WAR based archives. This launcher for standard WAR archives. + * Supports dependencies in {@code WEB-INF/lib} as well as {@code WEB-INF/lib-provided}, + * classes are loaded from {@code WEB-INF/classes}. * * @author Phillip Webb + * @author Andy Wilkinson + * @author Scott Frederick * @since 3.2.0 */ -public final class WarLauncher { +public class WarLauncher extends ExecutableArchiveLauncher { - private WarLauncher() { + public WarLauncher() throws Exception { + } + + protected WarLauncher(Archive archive) throws Exception { + super(archive); + } + + @Override + public boolean isIncludedOnClassPath(Archive.Entry entry) { + String name = entry.name(); + if (entry.isDirectory()) { + return name.equals("WEB-INF/classes/"); + } + return name.startsWith("WEB-INF/lib/") || name.startsWith("WEB-INF/lib-provided/"); + } + + @Override + protected String getEntryPathPrefix() { + return "WEB-INF/"; } public static void main(String[] args) throws Exception { - org.springframework.boot.loader.WarLauncher.main(args); + new WarLauncher().launch(args); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/package-info.java index 7968d509a2bb..5c5115bf0e34 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/package-info.java @@ -15,7 +15,10 @@ */ /** - * Repackaged launcher classes. + * System that allows self-contained JAR/WAR archives to be launched using + * {@code java -jar}. Archives can include nested packaged dependency JARs (there is no + * need to create shade style jars) and are executed without unpacking. The only + * constraint is that nested JARs must be stored in the archive uncompressed. * * @see org.springframework.boot.loader.launch.JarLauncher * @see org.springframework.boot.loader.launch.WarLauncher diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/log/DebugLogger.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/log/DebugLogger.java new file mode 100644 index 000000000000..417a9c5a4b7d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/log/DebugLogger.java @@ -0,0 +1,152 @@ +/* + * Copyright 2012-2023 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.loader.log; + +/** + * Simple logger class used for {@link System#err} debugging. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public abstract sealed class DebugLogger { + + private static final String ENABLED_PROPERTY = "loader.debug"; + + private static final DebugLogger disabled; + static { + disabled = Boolean.getBoolean(ENABLED_PROPERTY) ? null : new DisabledDebugLogger(); + } + + /** + * Log a message. + * @param message the message to log + */ + public abstract void log(String message); + + /** + * Log a formatted message. + * @param message the message to log + * @param arg1 the first format argument + */ + public abstract void log(String message, Object arg1); + + /** + * Log a formatted message. + * @param message the message to log + * @param arg1 the first format argument + * @param arg2 the second format argument + */ + public abstract void log(String message, Object arg1, Object arg2); + + /** + * Log a formatted message. + * @param message the message to log + * @param arg1 the first format argument + * @param arg2 the second format argument + * @param arg3 the third format argument + */ + public abstract void log(String message, Object arg1, Object arg2, Object arg3); + + /** + * Log a formatted message. + * @param message the message to log + * @param arg1 the first format argument + * @param arg2 the second format argument + * @param arg3 the third format argument + * @param arg4 the fourth format argument + */ + public abstract void log(String message, Object arg1, Object arg2, Object arg3, Object arg4); + + /** + * Get a {@link DebugLogger} to log messages for the given source class. + * @param sourceClass the source class + * @return a {@link DebugLogger} instance + */ + public static DebugLogger get(Class sourceClass) { + return (disabled != null) ? disabled : new SystemErrDebugLogger(sourceClass); + } + + /** + * {@link DebugLogger} used for disabled logging that does nothing. + */ + private static final class DisabledDebugLogger extends DebugLogger { + + @Override + public void log(String message) { + } + + @Override + public void log(String message, Object arg1) { + } + + @Override + public void log(String message, Object arg1, Object arg2) { + } + + @Override + public void log(String message, Object arg1, Object arg2, Object arg3) { + } + + @Override + public void log(String message, Object arg1, Object arg2, Object arg3, Object arg4) { + } + + } + + /** + * {@link DebugLogger} that prints messages to {@link System#err}. + */ + private static final class SystemErrDebugLogger extends DebugLogger { + + private final String prefix; + + SystemErrDebugLogger(Class sourceClass) { + this.prefix = "LOADER: " + sourceClass + " : "; + } + + @Override + public void log(String message) { + print(message); + } + + @Override + public void log(String message, Object arg1) { + print(message.formatted(arg1)); + } + + @Override + public void log(String message, Object arg1, Object arg2) { + print(message.formatted(arg1, arg2)); + } + + @Override + public void log(String message, Object arg1, Object arg2, Object arg3) { + print(message.formatted(arg1, arg2, arg3)); + } + + @Override + public void log(String message, Object arg1, Object arg2, Object arg3, Object arg4) { + print(message.formatted(arg1, arg2, arg3, arg4)); + } + + private void print(String message) { + System.err.println(this.prefix + message); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/log/package-info.java similarity index 77% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/package-info.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/log/package-info.java index 34bf2ead4378..c94baf14b30a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/data/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/log/package-info.java @@ -15,8 +15,6 @@ */ /** - * Classes and interfaces to allow random access to a block of data. - * - * @see org.springframework.boot.loader.data.RandomAccessData + * Debug {@link java.lang.System#err} logging support. */ -package org.springframework.boot.loader.data; +package org.springframework.boot.loader.log; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/Handlers.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/Handlers.java new file mode 100644 index 000000000000..781daeaf0f0a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/Handlers.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol; + +import java.net.URL; +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; + +/** + * Utility used to register loader {@link URLStreamHandler URL handlers}. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public final class Handlers { + + private static final String PROTOCOL_HANDLER_PACKAGES = "java.protocol.handler.pkgs"; + + private static final String PACKAGE = Handlers.class.getPackageName(); + + private Handlers() { + } + + /** + * Register a {@literal 'java.protocol.handler.pkgs'} property so that a + * {@link URLStreamHandler} will be located to deal with jar URLs. + */ + public static void register() { + String packages = System.getProperty(PROTOCOL_HANDLER_PACKAGES, ""); + packages = (!packages.isEmpty() && !packages.contains(PACKAGE)) ? packages + "|" + PACKAGE : PACKAGE; + System.setProperty(PROTOCOL_HANDLER_PACKAGES, packages); + resetCachedUrlHandlers(); + } + + /** + * Reset any cached handlers just in case a jar protocol has already been used. We + * reset the handler by trying to set a null {@link URLStreamHandlerFactory} which + * should have no effect other than clearing the handlers cache. + */ + private static void resetCachedUrlHandlers() { + try { + URL.setURLStreamHandlerFactory(null); + } + catch (Error ex) { + // Ignore + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/Canonicalizer.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/Canonicalizer.java new file mode 100644 index 000000000000..f06227498fa0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/Canonicalizer.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +/** + * Internal utility used by the {@link Handler} to canonicalize paths. This implementation + * should behave the same as the canonicalization functions in + * {@code sun.net.www.protocol.jar.Handler}. + * + * @author Phillip Webb + */ +final class Canonicalizer { + + private Canonicalizer() { + } + + static String canonicalizeAfter(String path, int pos) { + int pathLength = path.length(); + boolean noDotSlash = path.indexOf("./", pos) == -1; + if (pos >= pathLength || (noDotSlash && path.charAt(pathLength - 1) != '.')) { + return path; + } + String before = path.substring(0, pos); + String after = path.substring(pos); + return before + canonicalize(after); + } + + static String canonicalize(String path) { + path = removeEmbeddedSlashDotDotSlash(path); + path = removedEmbdeddedSlashDotSlash(path); + path = removeTrailingSlashDotDot(path); + path = removeTrailingSlashDot(path); + return path; + } + + private static String removeEmbeddedSlashDotDotSlash(String path) { + int index; + while ((index = path.indexOf("/../")) >= 0) { + int priorSlash = path.lastIndexOf('/', index - 1); + String after = path.substring(index + 3); + path = (priorSlash >= 0) ? path.substring(0, priorSlash) + after : after; + } + return path; + } + + private static String removedEmbdeddedSlashDotSlash(String path) { + int index; + while ((index = path.indexOf("/./")) >= 0) { + String before = path.substring(0, index); + String after = path.substring(index + 2); + path = before + after; + } + return path; + } + + private static String removeTrailingSlashDot(String path) { + return (!path.endsWith("/.")) ? path : path.substring(0, path.length() - 1); + } + + private static String removeTrailingSlashDotDot(String path) { + int index; + while (path.endsWith("/..")) { + index = path.indexOf("/.."); + int priorSlash = path.lastIndexOf('/', index - 1); + path = (priorSlash >= 0) ? path.substring(0, priorSlash + 1) : path.substring(0, index); + } + return path; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/Handler.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/Handler.java new file mode 100644 index 000000000000..2778beeccaf3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/Handler.java @@ -0,0 +1,190 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +/** + * {@link URLStreamHandler} alternative to {@code sun.net.www.protocol.jar.Handler} with + * optimized support for nested jars. + * + * @author Phillip Webb + * @since 3.2.0 + * @see org.springframework.boot.loader.net.protocol.Handlers + */ +public class Handler extends URLStreamHandler { + + // NOTE: in order to be found as a URL protocol handler, this class must be public, + // must be named Handler and must be in a package ending '.jar' + + private static final String PROTOCOL = "jar"; + + private static final String SEPARATOR = "!/"; + + static final Handler INSTANCE = new Handler(); + + @Override + protected URLConnection openConnection(URL url) throws IOException { + return JarUrlConnection.open(url); + } + + @Override + protected void parseURL(URL url, String spec, int start, int limit) { + if (spec.regionMatches(true, start, "jar:", 0, 4)) { + throw new IllegalStateException("Nested JAR URLs are not supported"); + } + int anchorIndex = spec.indexOf('#', limit); + String path = extractPath(url, spec, start, limit, anchorIndex); + String ref = (anchorIndex != -1) ? spec.substring(anchorIndex + 1) : null; + setURL(url, PROTOCOL, "", -1, null, null, path, null, ref); + } + + private String extractPath(URL url, String spec, int start, int limit, int anchorIndex) { + if (anchorIndex == start) { + return extractAnchorOnlyPath(url); + } + if (spec.length() >= 4 && spec.regionMatches(true, 0, "jar:", 0, 4)) { + return extractAbsolutePath(spec, start, limit); + } + return extractRelativePath(url, spec, start, limit); + } + + private String extractAnchorOnlyPath(URL url) { + return url.getPath(); + } + + private String extractAbsolutePath(String spec, int start, int limit) { + int indexOfSeparator = indexOfSeparator(spec, start, limit); + if (indexOfSeparator == -1) { + throw new IllegalStateException("no !/ in spec"); + } + String innerUrl = spec.substring(start, indexOfSeparator); + assertInnerUrlIsNotMalformed(spec, innerUrl); + return spec.substring(start, limit); + } + + private String extractRelativePath(URL url, String spec, int start, int limit) { + String contextPath = extractContextPath(url, spec, start); + String path = contextPath + spec.substring(start, limit); + return Canonicalizer.canonicalizeAfter(path, indexOfSeparator(path) + 1); + } + + private String extractContextPath(URL url, String spec, int start) { + String contextPath = url.getPath(); + if (spec.charAt(start) == '/') { + int indexOfContextPathSeparator = indexOfSeparator(contextPath); + if (indexOfContextPathSeparator == -1) { + throw new IllegalStateException("malformed context url:%s: no !/".formatted(url)); + } + return contextPath.substring(0, indexOfContextPathSeparator + 1); + } + int lastSlash = contextPath.lastIndexOf('/'); + if (lastSlash == -1) { + throw new IllegalStateException("malformed context url:%s".formatted(url)); + } + return contextPath.substring(0, lastSlash + 1); + } + + private void assertInnerUrlIsNotMalformed(String spec, String innerUrl) { + if (innerUrl.startsWith("nested:")) { + org.springframework.boot.loader.net.protocol.nested.Handler.assertUrlIsNotMalformed(innerUrl); + return; + } + try { + new URL(innerUrl); + } + catch (MalformedURLException ex) { + throw new IllegalStateException("invalid url: %s (%s)".formatted(spec, ex)); + } + } + + @Override + protected int hashCode(URL url) { + String protocol = url.getProtocol(); + int hash = (protocol != null) ? protocol.hashCode() : 0; + String file = url.getFile(); + int indexOfSeparator = file.indexOf(SEPARATOR); + if (indexOfSeparator == -1) { + return hash + file.hashCode(); + } + String fileWithoutEntry = file.substring(0, indexOfSeparator); + try { + hash += new URL(fileWithoutEntry).hashCode(); + } + catch (MalformedURLException ex) { + hash += fileWithoutEntry.hashCode(); + } + String entry = file.substring(indexOfSeparator + 2); + return hash + entry.hashCode(); + } + + @Override + protected boolean sameFile(URL url1, URL url2) { + if (!url1.getProtocol().equals(PROTOCOL) || !url2.getProtocol().equals(PROTOCOL)) { + return false; + } + String file1 = url1.getFile(); + String file2 = url2.getFile(); + int indexOfSeparator1 = file1.indexOf(SEPARATOR); + int indexOfSeparator2 = file2.indexOf(SEPARATOR); + if (indexOfSeparator1 == -1 || indexOfSeparator2 == -1) { + return super.sameFile(url1, url2); + } + String entry1 = file1.substring(indexOfSeparator1 + 2); + String entry2 = file2.substring(indexOfSeparator2 + 2); + if (!entry1.equals(entry2)) { + return false; + } + try { + URL innerUrl1 = new URL(file1.substring(0, indexOfSeparator1)); + URL innerUrl2 = new URL(file2.substring(0, indexOfSeparator2)); + if (!super.sameFile(innerUrl1, innerUrl2)) { + return false; + } + } + catch (MalformedURLException unused) { + return super.sameFile(url1, url2); + } + return true; + } + + static int indexOfSeparator(String spec) { + return indexOfSeparator(spec, 0, spec.length()); + } + + static int indexOfSeparator(String spec, int start, int limit) { + for (int i = limit - 1; i >= start; i--) { + if (spec.charAt(i) == '!' && (i + 1) < limit && spec.charAt(i + 1) == '/') { + return i; + } + } + return -1; + } + + /** + * Clear any internal caches. + */ + public static void clearCache() { + JarFileUrlKey.clearCache(); + JarUrlConnection.clearCache(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarFileUrlKey.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarFileUrlKey.java new file mode 100644 index 000000000000..e8ce0f503db1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarFileUrlKey.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.lang.ref.SoftReference; +import java.net.URL; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Utility to generate a string key from a jar file {@link URL} that can be used as a + * cache key. + * + * @author Phillip Webb + */ +final class JarFileUrlKey { + + private static volatile SoftReference> cache; + + private JarFileUrlKey() { + } + + /** + * Get the {@link JarFileUrlKey} for the given URL. + * @param url the source URL + * @return a {@link JarFileUrlKey} instance + */ + static String get(URL url) { + Map cache = (JarFileUrlKey.cache != null) ? JarFileUrlKey.cache.get() : null; + if (cache == null) { + cache = new ConcurrentHashMap<>(); + JarFileUrlKey.cache = new SoftReference<>(cache); + } + return cache.computeIfAbsent(url, JarFileUrlKey::create); + } + + private static String create(URL url) { + StringBuilder value = new StringBuilder(); + String protocol = url.getProtocol(); + String host = url.getHost(); + int port = (url.getPort() != -1) ? url.getPort() : url.getDefaultPort(); + String file = url.getFile(); + value.append(protocol.toLowerCase()); + value.append(":"); + if (host != null && !host.isEmpty()) { + value.append(host.toLowerCase()); + value.append((port != -1) ? ":" + port : ""); + } + value.append((file != null) ? file : ""); + if ("runtime".equals(url.getRef())) { + value.append("#runtime"); + } + return value.toString(); + } + + static void clearCache() { + cache = null; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrl.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrl.java new file mode 100644 index 000000000000..1e40ced32f1f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrl.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.jar.JarEntry; + +/** + * Utility class with factory methods that can be used to create JAR URLs. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public final class JarUrl { + + private JarUrl() { + } + + /** + * Create a new jar URL. + * @param file the jar file + * @return a jar file URL + */ + public static URL create(File file) { + return create(file, (String) null); + } + + /** + * Create a new jar URL. + * @param file the jar file + * @param nestedEntry the nested entry or {@code null} + * @return a jar file URL + */ + public static URL create(File file, JarEntry nestedEntry) { + return create(file, (nestedEntry != null) ? nestedEntry.getName() : null); + } + + /** + * Create a new jar URL. + * @param file the jar file + * @param nestedEntryName the nested entry name or {@code null} + * @return a jar file URL + */ + public static URL create(File file, String nestedEntryName) { + return create(file, nestedEntryName, null); + } + + /** + * Create a new jar URL. + * @param file the jar file + * @param nestedEntryName the nested entry name or {@code null} + * @param path the path within the jar or nested jar + * @return a jar file URL + */ + public static URL create(File file, String nestedEntryName, String path) { + try { + path = (path != null) ? path : ""; + return new URL(null, "jar:" + getJarReference(file, nestedEntryName) + "!/" + path, Handler.INSTANCE); + } + catch (MalformedURLException ex) { + throw new IllegalStateException("Unable to create JarFileArchive URL", ex); + } + } + + private static String getJarReference(File file, String nestedEntryName) { + String jarFilePath = file.toURI().getPath(); + return (nestedEntryName != null) ? "nested:" + jarFilePath + "/!" + nestedEntryName : "file:" + jarFilePath; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlClassLoader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlClassLoader.java new file mode 100644 index 000000000000..bf2aadc218e9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlClassLoader.java @@ -0,0 +1,290 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.io.IOException; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLClassLoader; +import java.net.URLConnection; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.jar.JarFile; + +import org.springframework.boot.loader.jar.NestedJarFile; +import org.springframework.boot.loader.launch.LaunchedClassLoader; + +/** + * {@link URLClassLoader} with optimized support for Jar URLs. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @since 3.2.0 + */ +public abstract class JarUrlClassLoader extends URLClassLoader { + + private final URL[] urls; + + private final boolean hasJarUrls; + + private final Map jarFiles = new ConcurrentHashMap<>(); + + private final Set undefinablePackages = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + /** + * Create a new {@link LaunchedClassLoader} instance. + * @param urls the URLs from which to load classes and resources + * @param parent the parent class loader for delegation + */ + public JarUrlClassLoader(URL[] urls, ClassLoader parent) { + super(urls, parent); + this.urls = urls; + this.hasJarUrls = Arrays.stream(urls).anyMatch(this::isJarUrl); + } + + @Override + public URL findResource(String name) { + if (!this.hasJarUrls) { + return super.findResource(name); + } + Optimizations.enable(false); + try { + return super.findResource(name); + } + finally { + Optimizations.disable(); + } + } + + @Override + public Enumeration findResources(String name) throws IOException { + if (!this.hasJarUrls) { + return super.findResources(name); + } + Optimizations.enable(false); + try { + return new OptimizedEnumeration(super.findResources(name)); + } + finally { + Optimizations.disable(); + } + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (!this.hasJarUrls) { + return super.loadClass(name, resolve); + } + Optimizations.enable(true); + try { + try { + definePackageIfNecessary(name); + } + catch (IllegalArgumentException ex) { + tolerateRaceConditionDueToBeingParallelCapable(ex, name); + } + return super.loadClass(name, resolve); + } + finally { + Optimizations.disable(); + } + } + + /** + * Define a package before a {@code findClass} call is made. This is necessary to + * ensure that the appropriate manifest for nested JARs is associated with the + * package. + * @param className the class name being found + */ + protected final void definePackageIfNecessary(String className) { + if (className.startsWith("java.")) { + return; + } + int lastDot = className.lastIndexOf('.'); + if (lastDot >= 0) { + String packageName = className.substring(0, lastDot); + if (getDefinedPackage(packageName) == null) { + try { + definePackage(className, packageName); + } + catch (IllegalArgumentException ex) { + tolerateRaceConditionDueToBeingParallelCapable(ex, packageName); + } + } + } + } + + private void definePackage(String className, String packageName) { + if (this.undefinablePackages.contains(packageName)) { + return; + } + String packageEntryName = packageName.replace('.', '/') + "/"; + String classEntryName = className.replace('.', '/') + ".class"; + for (URL url : this.urls) { + try { + JarFile jarFile = getJarFile(url); + if (jarFile != null) { + if (hasEntry(jarFile, classEntryName) && hasEntry(jarFile, packageEntryName) + && jarFile.getManifest() != null) { + definePackage(packageName, jarFile.getManifest(), url); + return; + } + } + } + catch (IOException ex) { + // Ignore + } + } + this.undefinablePackages.add(packageName); + } + + private void tolerateRaceConditionDueToBeingParallelCapable(IllegalArgumentException ex, String packageName) + throws AssertionError { + if (getDefinedPackage(packageName) == null) { + // This should never happen as the IllegalArgumentException indicates that the + // package has already been defined and, therefore, getDefinedPackage(name) + // should not have returned null. + throw new AssertionError( + "Package %s has already been defined but it could not be found".formatted(packageName), ex); + } + } + + private boolean hasEntry(JarFile jarFile, String name) { + return (jarFile instanceof NestedJarFile nestedJarFile) ? nestedJarFile.hasEntry(name) + : jarFile.getEntry(name) != null; + } + + private JarFile getJarFile(URL url) throws IOException { + JarFile jarFile = this.jarFiles.get(url); + if (jarFile != null) { + return jarFile; + } + URLConnection connection = url.openConnection(); + if (!(connection instanceof JarURLConnection)) { + return null; + } + connection.setUseCaches(false); + jarFile = ((JarURLConnection) connection).getJarFile(); + synchronized (this.jarFiles) { + JarFile previous = this.jarFiles.putIfAbsent(url, jarFile); + if (previous != null) { + jarFile.close(); + jarFile = previous; + } + } + return jarFile; + } + + /** + * Clear any caches. This method is called reflectively by + * {@code ClearCachesApplicationListener}. + */ + public void clearCache() { + Handler.clearCache(); + org.springframework.boot.loader.net.protocol.nested.Handler.clearCache(); + try { + clearJarFiles(); + } + catch (IOException ex) { + // Ignore + } + for (URL url : this.urls) { + if (isJarUrl(url)) { + clearCache(url); + } + } + } + + private void clearCache(URL url) { + try { + URLConnection connection = url.openConnection(); + if (connection instanceof JarURLConnection jarUrlConnection) { + clearCache(jarUrlConnection); + } + } + catch (IOException ex) { + // Ignore + } + } + + private void clearCache(JarURLConnection connection) throws IOException { + JarFile jarFile = connection.getJarFile(); + if (jarFile instanceof NestedJarFile nestedJarFile) { + nestedJarFile.clearCache(); + } + } + + private boolean isJarUrl(URL url) { + return "jar".equals(url.getProtocol()); + } + + @Override + public void close() throws IOException { + super.close(); + clearJarFiles(); + } + + private void clearJarFiles() throws IOException { + synchronized (this.jarFiles) { + for (JarFile jarFile : this.jarFiles.values()) { + jarFile.close(); + } + this.jarFiles.clear(); + } + } + + /** + * {@link Enumeration} that uses fast connections. + */ + private static class OptimizedEnumeration implements Enumeration { + + private final Enumeration delegate; + + OptimizedEnumeration(Enumeration delegate) { + this.delegate = delegate; + } + + @Override + public boolean hasMoreElements() { + Optimizations.enable(false); + try { + return this.delegate.hasMoreElements(); + } + finally { + Optimizations.disable(); + } + + } + + @Override + public URL nextElement() { + Optimizations.enable(false); + try { + return this.delegate.nextElement(); + } + finally { + Optimizations.disable(); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java new file mode 100644 index 000000000000..dc51bfe4ebc5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java @@ -0,0 +1,399 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.security.Permission; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import org.springframework.boot.loader.jar.NestedJarFile; +import org.springframework.boot.loader.net.util.UrlDecoder; + +/** + * {@link java.net.JarURLConnection} alternative to + * {@code sun.net.www.protocol.jar.JarURLConnection} with optimized support for nested + * jars. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Rostyslav Dudka + */ +final class JarUrlConnection extends java.net.JarURLConnection { + + static final UrlJarFiles jarFiles = new UrlJarFiles(); + + static final InputStream emptyInputStream = new ByteArrayInputStream(new byte[0]); + + static final FileNotFoundException FILE_NOT_FOUND_EXCEPTION = new FileNotFoundException( + "Jar file or entry not found"); + + private static final URL NOT_FOUND_URL; + + static final JarUrlConnection NOT_FOUND_CONNECTION; + static { + try { + NOT_FOUND_URL = new URL("jar:", null, 0, "nested:!/", new EmptyUrlStreamHandler()); + NOT_FOUND_CONNECTION = new JarUrlConnection(() -> FILE_NOT_FOUND_EXCEPTION); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + private final String entryName; + + private final Supplier notFound; + + private JarFile jarFile; + + private URLConnection jarFileConnection; + + private JarEntry jarEntry; + + private String contentType; + + private JarUrlConnection(URL url) throws IOException { + super(url); + this.entryName = getEntryName(); + this.notFound = null; + this.jarFileConnection = getJarFileURL().openConnection(); + this.jarFileConnection.setUseCaches(this.useCaches); + } + + private JarUrlConnection(Supplier notFound) throws IOException { + super(NOT_FOUND_URL); + this.entryName = null; + this.notFound = notFound; + } + + @Override + public JarFile getJarFile() throws IOException { + connect(); + return this.jarFile; + } + + @Override + public JarEntry getJarEntry() throws IOException { + connect(); + return this.jarEntry; + } + + @Override + public int getContentLength() { + long contentLength = getContentLengthLong(); + return (contentLength <= Integer.MAX_VALUE) ? (int) contentLength : -1; + } + + @Override + public long getContentLengthLong() { + try { + connect(); + return (this.jarEntry != null) ? this.jarEntry.getSize() : this.jarFileConnection.getContentLengthLong(); + } + catch (IOException ex) { + return -1; + } + } + + @Override + public String getContentType() { + if (this.contentType == null) { + this.contentType = deduceContentType(); + } + return this.contentType; + } + + private String deduceContentType() { + String type = (this.entryName != null) ? null : "x-java/jar"; + type = (type != null) ? type : deduceContentTypeFromStream(); + type = (type != null) ? type : deduceContentTypeFromEntryName(); + return (type != null) ? type : "content/unknown"; + } + + private String deduceContentTypeFromStream() { + try { + connect(); + try (InputStream in = this.jarFile.getInputStream(this.jarEntry)) { + return guessContentTypeFromStream(new BufferedInputStream(in)); + } + } + catch (IOException ex) { + return null; + } + } + + private String deduceContentTypeFromEntryName() { + return guessContentTypeFromName(this.entryName); + } + + @Override + public String getHeaderField(String name) { + return (this.jarFileConnection != null) ? this.jarFileConnection.getHeaderField(name) : null; + } + + @Override + public Object getContent() throws IOException { + connect(); + return (this.entryName != null) ? super.getContent() : this.jarFile; + } + + @Override + public Permission getPermission() throws IOException { + return this.jarFileConnection.getPermission(); + } + + @Override + public InputStream getInputStream() throws IOException { + if (this.notFound != null) { + throwFileNotFound(); + } + if (this.entryName == null) { + throw new IOException("no entry name specified"); + } + if (!getUseCaches() && Optimizations.isEnabled(false)) { + JarFile cached = jarFiles.getCached(getJarFileURL()); + if (cached != null) { + if (cached.getEntry(this.entryName) != null) { + return emptyInputStream; + } + } + } + connect(); + if (this.jarEntry == null) { + throwFileNotFound(); + } + return new ConnectionInputStream(); + } + + @Override + public boolean getAllowUserInteraction() { + return (this.jarFileConnection != null) ? this.jarFileConnection.getAllowUserInteraction() : false; + } + + @Override + public void setAllowUserInteraction(boolean allowuserinteraction) { + if (this.jarFileConnection != null) { + this.jarFileConnection.setAllowUserInteraction(allowuserinteraction); + } + } + + @Override + public boolean getUseCaches() { + return (this.jarFileConnection != null) ? this.jarFileConnection.getUseCaches() : true; + } + + @Override + public void setUseCaches(boolean usecaches) { + if (this.jarFileConnection != null) { + this.jarFileConnection.setUseCaches(usecaches); + } + } + + @Override + public boolean getDefaultUseCaches() { + return (this.jarFileConnection != null) ? this.jarFileConnection.getDefaultUseCaches() : true; + } + + @Override + public void setDefaultUseCaches(boolean defaultusecaches) { + if (this.jarFileConnection != null) { + this.jarFileConnection.setDefaultUseCaches(defaultusecaches); + } + } + + @Override + public void setIfModifiedSince(long ifModifiedSince) { + if (this.jarFileConnection != null) { + this.jarFileConnection.setIfModifiedSince(ifModifiedSince); + } + } + + @Override + public String getRequestProperty(String key) { + return (this.jarFileConnection != null) ? this.jarFileConnection.getRequestProperty(key) : null; + } + + @Override + public void setRequestProperty(String key, String value) { + if (this.jarFileConnection != null) { + this.jarFileConnection.setRequestProperty(key, value); + } + } + + @Override + public void addRequestProperty(String key, String value) { + if (this.jarFileConnection != null) { + this.jarFileConnection.addRequestProperty(key, value); + } + } + + @Override + public Map> getRequestProperties() { + return (this.jarFileConnection != null) ? this.jarFileConnection.getRequestProperties() + : Collections.emptyMap(); + } + + @Override + public void connect() throws IOException { + if (this.connected) { + return; + } + if (this.notFound != null) { + throwFileNotFound(); + } + boolean useCaches = getUseCaches(); + URL jarFileURL = getJarFileURL(); + if (this.entryName != null && Optimizations.isEnabled()) { + assertCachedJarFileHasEntry(jarFileURL, this.entryName); + } + this.jarFile = jarFiles.getOrCreate(useCaches, jarFileURL); + this.jarEntry = getJarEntry(jarFileURL); + boolean addedToCache = jarFiles.cacheIfAbsent(useCaches, jarFileURL, this.jarFile); + if (addedToCache) { + this.jarFileConnection = jarFiles.reconnect(this.jarFile, this.jarFileConnection); + } + this.connected = true; + } + + /** + * The {@link URLClassLoader} connects often to check if a resource exists, we can + * save some object allocations by using the cached copy if we have one. + * @param jarFileURL the jar file to check + * @param entryName the entry name to check + * @throws FileNotFoundException on a missing entry + */ + private void assertCachedJarFileHasEntry(URL jarFileURL, String entryName) throws FileNotFoundException { + JarFile cachedJarFile = jarFiles.getCached(jarFileURL); + if (cachedJarFile != null && cachedJarFile.getJarEntry(entryName) == null) { + throw FILE_NOT_FOUND_EXCEPTION; + } + } + + private JarEntry getJarEntry(URL jarFileUrl) throws IOException { + if (this.entryName == null) { + return null; + } + JarEntry jarEntry = this.jarFile.getJarEntry(this.entryName); + if (jarEntry == null) { + jarFiles.closeIfNotCached(jarFileUrl, this.jarFile); + throwFileNotFound(); + } + return jarEntry; + } + + private void throwFileNotFound() throws FileNotFoundException { + if (Optimizations.isEnabled()) { + throw FILE_NOT_FOUND_EXCEPTION; + } + if (this.notFound != null) { + throw this.notFound.get(); + } + throw new FileNotFoundException("JAR entry " + this.entryName + " not found in " + this.jarFile.getName()); + } + + static JarUrlConnection open(URL url) throws IOException { + String spec = url.getFile(); + if (spec.startsWith("nested:")) { + int separator = spec.indexOf("!/"); + boolean specHasEntry = (separator != -1) && (separator + 2 != spec.length()); + if (specHasEntry) { + URL jarFileUrl = new URL(spec.substring(0, separator)); + if ("runtime".equals(url.getRef())) { + jarFileUrl = new URL(jarFileUrl, "#runtime"); + } + String entryName = UrlDecoder.decode(spec.substring(separator + 2)); + JarFile jarFile = jarFiles.getOrCreate(true, jarFileUrl); + jarFiles.cacheIfAbsent(true, jarFileUrl, jarFile); + if (!hasEntry(jarFile, entryName)) { + return notFoundConnection(jarFile.getName(), entryName); + } + } + } + return new JarUrlConnection(url); + } + + private static boolean hasEntry(JarFile jarFile, String name) { + return (jarFile instanceof NestedJarFile nestedJarFile) ? nestedJarFile.hasEntry(name) + : jarFile.getEntry(name) != null; + } + + private static JarUrlConnection notFoundConnection(String jarFileName, String entryName) throws IOException { + if (Optimizations.isEnabled()) { + return NOT_FOUND_CONNECTION; + } + return new JarUrlConnection( + () -> new FileNotFoundException("JAR entry " + entryName + " not found in " + jarFileName)); + } + + static void clearCache() { + jarFiles.clearCache(); + } + + /** + * Connection {@link InputStream}. This is not a {@link FilterInputStream} since + * {@link URLClassLoader} often creates streams that it doesn't call and we want to be + * lazy about getting the underlying {@link InputStream}. + */ + class ConnectionInputStream extends LazyDelegatingInputStream { + + @Override + public void close() throws IOException { + try { + super.close(); + } + finally { + if (!getUseCaches()) { + JarUrlConnection.this.jarFile.close(); + } + } + } + + @Override + protected InputStream getDelegateInputStream() throws IOException { + return JarUrlConnection.this.jarFile.getInputStream(JarUrlConnection.this.jarEntry); + } + + } + + /** + * Empty {@link URLStreamHandler} used to prevent the wrong JAR Handler from being + * Instantiated and cached. + */ + private static class EmptyUrlStreamHandler extends URLStreamHandler { + + @Override + protected URLConnection openConnection(URL url) { + return null; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/LazyDelegatingInputStream.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/LazyDelegatingInputStream.java new file mode 100644 index 000000000000..95e5cc3c14a7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/LazyDelegatingInputStream.java @@ -0,0 +1,110 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.io.IOException; +import java.io.InputStream; + +/** + * {@link InputStream} that delegates lazily to another {@link InputStream}. + * + * @author Phillip Webb + */ +abstract class LazyDelegatingInputStream extends InputStream { + + private volatile InputStream in; + + @Override + public int read() throws IOException { + return in().read(); + } + + @Override + public int read(byte[] b) throws IOException { + return in().read(b); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return in().read(b, off, len); + } + + @Override + public long skip(long n) throws IOException { + return in().skip(n); + } + + @Override + public int available() throws IOException { + return in().available(); + } + + @Override + public boolean markSupported() { + try { + return in().markSupported(); + } + catch (IOException ex) { + return false; + } + } + + @Override + public synchronized void mark(int readlimit) { + try { + in().mark(readlimit); + } + catch (IOException ex) { + // Ignore + } + } + + @Override + public synchronized void reset() throws IOException { + in().reset(); + } + + private InputStream in() throws IOException { + InputStream in = this.in; + if (in == null) { + synchronized (this) { + in = this.in; + if (in == null) { + in = getDelegateInputStream(); + this.in = in; + } + } + } + return in; + } + + @Override + public void close() throws IOException { + InputStream in = this.in; + if (in != null) { + synchronized (this) { + in = this.in; + if (in != null) { + in.close(); + } + } + } + } + + protected abstract InputStream getDelegateInputStream() throws IOException; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/Optimizations.java similarity index 54% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/Optimizations.java index 6804f0ba37f9..138e8e45e086 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/JarEntryFilter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/Optimizations.java @@ -14,22 +14,34 @@ * limitations under the License. */ -package org.springframework.boot.loader.jar; +package org.springframework.boot.loader.net.protocol.jar; /** - * Interface that can be used to filter and optionally rename jar entries. + * {@link ThreadLocal} state for {@link Handler} optimizations. * * @author Phillip Webb */ -interface JarEntryFilter { - - /** - * Apply the jar entry filter. - * @param name the current entry name. This may be different that the original entry - * name if a previous filter has been applied - * @return the new name of the entry or {@code null} if the entry should not be - * included. - */ - AsciiBytes apply(AsciiBytes name); +final class Optimizations { + + private static final ThreadLocal status = new ThreadLocal<>(); + + private Optimizations() { + } + + static void enable(boolean readContents) { + status.set(readContents); + } + + static void disable() { + status.remove(); + } + + static boolean isEnabled() { + return status.get() != null; + } + + static boolean isEnabled(boolean readContents) { + return Boolean.valueOf(readContents).equals(status.get()); + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarEntry.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarEntry.java new file mode 100644 index 000000000000..5c2b100cf201 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarEntry.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.io.IOException; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.zip.ZipEntry; + +/** + * A {@link JarEntry} returned from a {@link UrlJarFile} or {@link UrlNestedJarFile}. + * + * @author Phillip Webb + */ +final class UrlJarEntry extends JarEntry { + + private final UrlJarManifest manifest; + + private UrlJarEntry(JarEntry entry, UrlJarManifest manifest) { + super(entry); + this.manifest = manifest; + } + + @Override + public Attributes getAttributes() throws IOException { + return this.manifest.getEntryAttributes(this); + } + + static UrlJarEntry of(ZipEntry entry, UrlJarManifest manifest) { + return (entry != null) ? new UrlJarEntry((JarEntry) entry, manifest) : null; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFile.java new file mode 100644 index 000000000000..513d79799c1f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFile.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.io.File; +import java.io.IOException; +import java.util.function.Consumer; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * A {@link JarFile} subclass returned from a {@link JarUrlConnection}. + * + * @author Phillip Webb + */ +class UrlJarFile extends JarFile { + + private final UrlJarManifest manifest; + + private final Consumer closeAction; + + UrlJarFile(File file, Runtime.Version version, Consumer closeAction) throws IOException { + super(file, true, ZipFile.OPEN_READ, version); + this.manifest = new UrlJarManifest(super::getManifest); + this.closeAction = closeAction; + } + + @Override + public ZipEntry getEntry(String name) { + return UrlJarEntry.of(super.getEntry(name), this.manifest); + } + + @Override + public Manifest getManifest() throws IOException { + return this.manifest.get(); + } + + @Override + public void close() throws IOException { + this.closeAction.accept(this); + super.close(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java new file mode 100644 index 000000000000..1fb8173b87a6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java @@ -0,0 +1,118 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.Runtime.Version; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.function.Consumer; +import java.util.jar.JarFile; + +import org.springframework.boot.loader.net.protocol.nested.NestedLocation; +import org.springframework.boot.loader.net.util.UrlDecoder; + +/** + * Factory used by {@link UrlJarFiles} to create {@link JarFile} instances. + * + * @author Phillip Webb + * @see UrlJarFile + * @see UrlNestedJarFile + */ +class UrlJarFileFactory { + + /** + * Create a new {@link UrlJarFile} or {@link UrlNestedJarFile} instance. + * @param jarFileUrl the jar file URL + * @param closeAction the action to call when the file is closed + * @return a new {@link JarFile} instance + * @throws IOException on I/O error + */ + JarFile createJarFile(URL jarFileUrl, Consumer closeAction) throws IOException { + Runtime.Version version = getVersion(jarFileUrl); + if (isLocalFileUrl(jarFileUrl)) { + return createJarFileForLocalFile(jarFileUrl, version, closeAction); + } + if (isNestedUrl(jarFileUrl)) { + return createJarFileForNested(jarFileUrl, version, closeAction); + } + return createJarFileForStream(jarFileUrl, version, closeAction); + } + + private Runtime.Version getVersion(URL url) { + return "runtime".equals(url.getRef()) ? JarFile.runtimeVersion() : JarFile.baseVersion(); + } + + private boolean isLocalFileUrl(URL url) { + return url.getProtocol().equalsIgnoreCase("file") && isLocal(url.getHost()); + } + + private boolean isLocal(String host) { + return host == null || host.isEmpty() || host.equals("~") || host.equalsIgnoreCase("localhost"); + } + + private JarFile createJarFileForLocalFile(URL url, Runtime.Version version, Consumer closeAction) + throws IOException { + String path = UrlDecoder.decode(url.getPath()); + return new UrlJarFile(new File(path), version, closeAction); + } + + private boolean isNestedUrl(URL url) { + return url.getProtocol().equalsIgnoreCase("nested"); + } + + private JarFile createJarFileForNested(URL url, Runtime.Version version, Consumer closeAction) + throws IOException { + NestedLocation location = NestedLocation.fromUrl(url); + return new UrlNestedJarFile(location.file(), location.nestedEntryName(), version, closeAction); + } + + private JarFile createJarFileForStream(URL url, Version version, Consumer closeAction) throws IOException { + try (InputStream in = url.openStream()) { + return createJarFileForStream(in, version, closeAction); + } + } + + private JarFile createJarFileForStream(InputStream in, Version version, Consumer closeAction) + throws IOException { + Path local = Files.createTempFile("jar_cache", null); + try { + Files.copy(in, local, StandardCopyOption.REPLACE_EXISTING); + JarFile jarFile = new UrlJarFile(local.toFile(), version, closeAction); + local.toFile().deleteOnExit(); + return jarFile; + } + catch (Throwable ex) { + deleteIfPossible(local, ex); + throw ex; + } + } + + private void deleteIfPossible(Path local, Throwable cause) { + try { + Files.delete(local); + } + catch (IOException ex) { + cause.addSuppressed(ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFiles.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFiles.java new file mode 100644 index 000000000000..145a51496054 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFiles.java @@ -0,0 +1,217 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.JarFile; + +/** + * Provides access to {@link UrlJarFile} and {@link UrlNestedJarFile} instances taking + * care of caching concerns when necessary. + *

    + * This class is thread-safe and designed to be shared by all {@link JarUrlConnection} + * instances. + * + * @author Phillip Webb + */ +class UrlJarFiles { + + private final UrlJarFileFactory factory; + + private final Cache cache = new Cache(); + + /** + * Create a new {@link UrlJarFiles} instance. + */ + UrlJarFiles() { + this(new UrlJarFileFactory()); + } + + /** + * Create a new {@link UrlJarFiles} instance. + * @param factory the {@link UrlJarFileFactory} to use. + */ + UrlJarFiles(UrlJarFileFactory factory) { + this.factory = factory; + } + + /** + * Get an existing {@link JarFile} instance from the cache, or create a new + * {@link JarFile} instance that can be {@link #cacheIfAbsent(boolean, URL, JarFile) + * cached later}. + * @param useCaches if caches can be used + * @param jarFileUrl the jar file URL + * @return a new or existing {@link JarFile} instance + * @throws IOException on I/O error + */ + JarFile getOrCreate(boolean useCaches, URL jarFileUrl) throws IOException { + if (useCaches) { + JarFile cached = getCached(jarFileUrl); + if (cached != null) { + return cached; + } + } + return this.factory.createJarFile(jarFileUrl, this::onClose); + } + + /** + * Return the cached {@link JarFile} if available. + * @param jarFileUrl the jar file URL + * @return the cached jar or {@code null} + */ + JarFile getCached(URL jarFileUrl) { + return this.cache.get(jarFileUrl); + } + + /** + * Cache the given {@link JarFile} if caching can be used and there is no existing + * entry. + * @param useCaches if caches can be used + * @param jarFileUrl the jar file URL + * @param jarFile the jar file + * @return {@code true} if that file was added to the cache + */ + boolean cacheIfAbsent(boolean useCaches, URL jarFileUrl, JarFile jarFile) { + if (!useCaches) { + return false; + } + return this.cache.putIfAbsent(jarFileUrl, jarFile); + } + + /** + * Close the given {@link JarFile} only if it is not contained in the cache. + * @param jarFileUrl the jar file URL + * @param jarFile the jar file + * @throws IOException on I/O error + */ + void closeIfNotCached(URL jarFileUrl, JarFile jarFile) throws IOException { + JarFile cached = getCached(jarFileUrl); + if (cached != jarFile) { + jarFile.close(); + } + } + + /** + * Reconnect to the {@link JarFile}, returning a replacement {@link URLConnection}. + * @param jarFile the jar file + * @param existingConnection the existing connection + * @return a newly opened connection inhering the same {@code useCaches} value as the + * existing connection + * @throws IOException on I/O error + */ + URLConnection reconnect(JarFile jarFile, URLConnection existingConnection) throws IOException { + Boolean useCaches = (existingConnection != null) ? existingConnection.getUseCaches() : null; + URLConnection connection = openConnection(jarFile); + if (useCaches != null && connection != null) { + connection.setUseCaches(useCaches); + } + return connection; + } + + private URLConnection openConnection(JarFile jarFile) throws IOException { + URL url = this.cache.get(jarFile); + return (url != null) ? url.openConnection() : null; + } + + private void onClose(JarFile jarFile) { + this.cache.remove(jarFile); + } + + void clearCache() { + this.cache.clear(); + } + + /** + * Internal cache. + */ + private static class Cache { + + private final Map jarFileUrlToJarFile = new HashMap<>(); + + private final Map jarFileToJarFileUrl = new HashMap<>(); + + /** + * Get a {@link JarFile} from the cache given a jar file URL. + * @param jarFileUrl the jar file URL + * @return the cached {@link JarFile} or {@code null} + */ + JarFile get(URL jarFileUrl) { + String urlKey = JarFileUrlKey.get(jarFileUrl); + synchronized (this) { + return this.jarFileUrlToJarFile.get(urlKey); + } + } + + /** + * Get a jar file URL from the cache given a jar file. + * @param jarFile the jar file + * @return the cached {@link URL} or {@code null} + */ + URL get(JarFile jarFile) { + synchronized (this) { + return this.jarFileToJarFileUrl.get(jarFile); + } + } + + /** + * Put the given jar file URL and jar file into the cache if they aren't already + * there. + * @param jarFileUrl the jar file URL + * @param jarFile the jar file + * @return {@code true} if the items were added to the cache or {@code false} if + * they were already there + */ + boolean putIfAbsent(URL jarFileUrl, JarFile jarFile) { + String urlKey = JarFileUrlKey.get(jarFileUrl); + synchronized (this) { + JarFile cached = this.jarFileUrlToJarFile.get(urlKey); + if (cached == null) { + this.jarFileUrlToJarFile.put(urlKey, jarFile); + this.jarFileToJarFileUrl.put(jarFile, jarFileUrl); + return true; + } + return false; + } + } + + /** + * Remove the given jar and any related URL file from the cache. + * @param jarFile the jar file to remove + */ + void remove(JarFile jarFile) { + synchronized (this) { + URL removedUrl = this.jarFileToJarFileUrl.remove(jarFile); + if (removedUrl != null) { + this.jarFileUrlToJarFile.remove(JarFileUrlKey.get(removedUrl)); + } + } + } + + void clear() { + synchronized (this) { + this.jarFileToJarFileUrl.clear(); + this.jarFileUrlToJarFile.clear(); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarManifest.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarManifest.java new file mode 100644 index 000000000000..70c372855d50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarManifest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.io.IOException; +import java.util.Map; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.Manifest; + +/** + * Provides access {@link Manifest} content that can be safely returned from + * {@link UrlJarFile} or {@link UrlNestedJarFile}. + * + * @author Phillip Webb + */ +class UrlJarManifest { + + private static final Object NONE = new Object(); + + private final ManifestSupplier supplier; + + private volatile Object supplied; + + UrlJarManifest(ManifestSupplier supplier) { + this.supplier = supplier; + } + + Manifest get() throws IOException { + Manifest manifest = supply(); + if (manifest == null) { + return null; + } + Manifest copy = new Manifest(); + copy.getMainAttributes().putAll((Map) manifest.getMainAttributes().clone()); + manifest.getEntries().forEach((key, value) -> copy.getEntries().put(key, cloneAttributes(value))); + return copy; + } + + Attributes getEntryAttributes(JarEntry entry) throws IOException { + Manifest manifest = supply(); + if (manifest == null) { + return null; + } + Attributes attributes = manifest.getEntries().get(entry.getName()); + return cloneAttributes(attributes); + } + + private Attributes cloneAttributes(Attributes attributes) { + return (attributes != null) ? (Attributes) attributes.clone() : null; + } + + private Manifest supply() throws IOException { + Object supplied = this.supplied; + if (supplied == null) { + supplied = this.supplier.getManifest(); + this.supplied = (supplied != null) ? supplied : NONE; + } + return (supplied != NONE) ? (Manifest) supplied : null; + } + + /** + * Interface used to supply the actual manifest. + */ + @FunctionalInterface + interface ManifestSupplier { + + Manifest getManifest() throws IOException; + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlNestedJarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlNestedJarFile.java new file mode 100644 index 000000000000..33bbed2e8375 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlNestedJarFile.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.io.File; +import java.io.IOException; +import java.lang.Runtime.Version; +import java.util.function.Consumer; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +import org.springframework.boot.loader.jar.NestedJarFile; + +/** + * {@link NestedJarFile} subclass returned from a {@link JarUrlConnection}. + * + * @author Phillip Webb + */ +class UrlNestedJarFile extends NestedJarFile { + + private final UrlJarManifest manifest; + + private final Consumer closeAction; + + UrlNestedJarFile(File file, String nestedEntryName, Version version, Consumer closeAction) + throws IOException { + super(file, nestedEntryName, version); + this.manifest = new UrlJarManifest(super::getManifest); + this.closeAction = closeAction; + } + + @Override + public Manifest getManifest() throws IOException { + return this.manifest.get(); + } + + @Override + public JarEntry getEntry(String name) { + return UrlJarEntry.of(super.getEntry(name), this.manifest); + } + + @Override + public void close() throws IOException { + this.closeAction.accept(this); + super.close(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/package-info.java new file mode 100644 index 000000000000..980f4230226f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * JAR URL support, including support for nested jars. + * + * @see org.springframework.boot.loader.net.protocol.jar.JarUrl + * @see org.springframework.boot.loader.net.protocol.jar.Handler + */ +package org.springframework.boot.loader.net.protocol.jar; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/Handler.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/Handler.java new file mode 100644 index 000000000000..0a05596e2831 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/Handler.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.nested; + +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +/** + * {@link URLStreamHandler} to support {@code nested:} URLs. See {@link NestedLocation} + * for details of the URL format. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public class Handler extends URLStreamHandler { + + // NOTE: in order to be found as a URL protocol handler, this class must be public, + // must be named Handler and must be in a package ending '.nested' + + private static final String PREFIX = "nested:"; + + @Override + protected URLConnection openConnection(URL url) throws IOException { + return new NestedUrlConnection(url); + } + + /** + * Assert that the specified URL is a valid "nested" URL. + * @param url the URL to check + */ + public static void assertUrlIsNotMalformed(String url) { + if (url == null || !url.startsWith(PREFIX)) { + throw new IllegalArgumentException("'url' must not be null and must use 'nested' protocol"); + } + NestedLocation.parse(url.substring(PREFIX.length())); + } + + /** + * Clear any internal caches. + */ + public static void clearCache() { + NestedLocation.clearCache(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java new file mode 100644 index 000000000000..3f0a016a6a6a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.nested; + +import java.io.File; +import java.net.URL; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.boot.loader.net.util.UrlDecoder; + +/** + * A location obtained from a {@code nested:} {@link URL} consisting of a jar file and a + * nested entry. + *

    + * The syntax of a nested JAR URL is:

    + * nestedjar:<path>/!{entry}
    + * 
    + *

    + * for example: + *

    + * {@code nested:/home/example/my.jar/!BOOT-INF/lib/my-nested.jar} + *

    + * or: + *

    + * {@code nested:/home/example/my.jar/!BOOT-INF/classes/} + *

    + * The path must refer to a jar file on the file system. The entry refers to either an + * uncompressed entry that contains the nested jar, or a directory entry. The entry must + * not start with a {@code '/'}. + * + * @param file the zip file that contains the nested entry + * @param nestedEntryName the nested entry name + * @author Phillip Webb + * @since 3.2.0 + */ +public record NestedLocation(File file, String nestedEntryName) { + + private static final Map cache = new ConcurrentHashMap<>(); + + public NestedLocation { + if (file == null) { + throw new IllegalArgumentException("'file' must not be null"); + } + if (nestedEntryName == null || nestedEntryName.trim().isEmpty()) { + throw new IllegalArgumentException("'nestedEntryName' must not be empty"); + } + } + + /** + * Create a new {@link NestedLocation} from the given URL. + * @param url the nested URL + * @return a new {@link NestedLocation} instance + * @throws IllegalArgumentException if the URL is not valid + */ + public static NestedLocation fromUrl(URL url) { + if (url == null || !"nested".equalsIgnoreCase(url.getProtocol())) { + throw new IllegalArgumentException("'url' must not be null and must use 'nested' protocol"); + } + return parse(UrlDecoder.decode(url.getPath())); + } + + static NestedLocation parse(String path) { + if (path == null || path.isEmpty()) { + throw new IllegalArgumentException("'path' must not be empty"); + } + int index = path.lastIndexOf("/!"); + if (index == -1) { + throw new IllegalArgumentException("'path' must contain '/!'"); + } + return cache.computeIfAbsent(path, (l) -> create(index, l)); + } + + private static NestedLocation create(int index, String location) { + String file = location.substring(0, index); + String nestedEntryName = location.substring(index + 2); + return new NestedLocation((!file.isEmpty()) ? new File(file) : null, nestedEntryName); + } + + static void clearCache() { + cache.clear(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnection.java new file mode 100644 index 000000000000..308b32116d47 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnection.java @@ -0,0 +1,155 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.nested; + +import java.io.FilePermission; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.lang.ref.Cleaner.Cleanable; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.security.Permission; + +import org.springframework.boot.loader.ref.Cleaner; + +/** + * {@link URLConnection} to support {@code nested:} URLs. See {@link NestedLocation} for + * details of the URL format. + * + * @author Phillip Webb + */ +class NestedUrlConnection extends URLConnection { + + private static final String CONTENT_TYPE = "x-java/jar"; + + private final NestedUrlConnectionResources resources; + + private final Cleanable cleanup; + + private long lastModified; + + private FilePermission permission; + + NestedUrlConnection(URL url) throws MalformedURLException { + this(url, Cleaner.instance); + } + + NestedUrlConnection(URL url, Cleaner cleaner) throws MalformedURLException { + super(url); + NestedLocation location = parseNestedLocation(url); + this.resources = new NestedUrlConnectionResources(location); + this.cleanup = cleaner.register(this, this.resources); + } + + private NestedLocation parseNestedLocation(URL url) throws MalformedURLException { + try { + return NestedLocation.parse(url.getPath()); + } + catch (IllegalArgumentException ex) { + throw new MalformedURLException(ex.getMessage()); + } + } + + @Override + public int getContentLength() { + long contentLength = getContentLengthLong(); + return (contentLength <= Integer.MAX_VALUE) ? (int) contentLength : -1; + } + + @Override + public long getContentLengthLong() { + try { + connect(); + return this.resources.getContentLength(); + } + catch (IOException ex) { + return -1; + } + } + + @Override + public String getContentType() { + return CONTENT_TYPE; + } + + @Override + public long getLastModified() { + if (this.lastModified == 0) { + this.lastModified = this.resources.getLocation().file().lastModified(); + } + return this.lastModified; + } + + @Override + public Permission getPermission() throws IOException { + if (this.permission == null) { + this.permission = new FilePermission(this.resources.getLocation().file().getCanonicalPath(), "read"); + } + return this.permission; + } + + @Override + public InputStream getInputStream() throws IOException { + connect(); + return new ConnectionInputStream(this.resources.getInputStream()); + } + + @Override + public void connect() throws IOException { + if (this.connected) { + return; + } + this.resources.connect(); + this.connected = true; + } + + /** + * Connection {@link InputStream}. + */ + class ConnectionInputStream extends FilterInputStream { + + private volatile boolean closing; + + ConnectionInputStream(InputStream in) { + super(in); + } + + @Override + public void close() throws IOException { + if (this.closing) { + return; + } + this.closing = true; + try { + super.close(); + } + finally { + try { + NestedUrlConnection.this.cleanup.clean(); + } + catch (UncheckedIOException ex) { + throw ex.getCause(); + } + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionResources.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionResources.java new file mode 100644 index 000000000000..4582e197c464 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionResources.java @@ -0,0 +1,128 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.nested; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; + +import org.springframework.boot.loader.ref.Cleaner; +import org.springframework.boot.loader.zip.CloseableDataBlock; +import org.springframework.boot.loader.zip.ZipContent; + +/** + * Resources created managed and cleaned by a {@link NestedUrlConnection} instance and + * suitable for registration with a {@link Cleaner}. + * + * @author Phillip Webb + */ +class NestedUrlConnectionResources implements Runnable { + + private final NestedLocation location; + + private volatile ZipContent zipContent; + + private volatile long size = -1; + + private volatile InputStream inputStream; + + NestedUrlConnectionResources(NestedLocation location) { + this.location = location; + } + + NestedLocation getLocation() { + return this.location; + } + + void connect() throws IOException { + synchronized (this) { + if (this.zipContent == null) { + this.zipContent = ZipContent.open(this.location.file().toPath(), this.location.nestedEntryName()); + try { + connectData(); + } + catch (IOException | RuntimeException ex) { + this.zipContent.close(); + this.zipContent = null; + throw ex; + } + } + } + } + + private void connectData() throws IOException { + CloseableDataBlock data = this.zipContent.openRawZipData(); + try { + this.size = data.size(); + this.inputStream = data.asInputStream(); + } + catch (IOException | RuntimeException ex) { + data.close(); + } + } + + InputStream getInputStream() throws IOException { + synchronized (this) { + if (this.inputStream == null) { + throw new IOException("Nested location not found " + this.location); + } + return this.inputStream; + } + } + + long getContentLength() { + return this.size; + } + + @Override + public void run() { + releaseAll(); + } + + private void releaseAll() { + synchronized (this) { + if (this.zipContent != null) { + IOException exceptionChain = null; + try { + this.inputStream.close(); + } + catch (IOException ex) { + exceptionChain = addToExceptionChain(exceptionChain, ex); + } + try { + this.zipContent.close(); + } + catch (IOException ex) { + exceptionChain = addToExceptionChain(exceptionChain, ex); + } + this.size = -1; + if (exceptionChain != null) { + throw new UncheckedIOException(exceptionChain); + } + } + } + } + + private IOException addToExceptionChain(IOException exceptionChain, IOException ex) { + if (exceptionChain != null) { + exceptionChain.addSuppressed(ex); + return exceptionChain; + } + return ex; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/package-info.java new file mode 100644 index 000000000000..1e0426e2a976 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Nested URL support. + * + * @see org.springframework.boot.loader.net.protocol.nested.NestedLocation + * @see org.springframework.boot.loader.net.protocol.nested.Handler + */ +package org.springframework.boot.loader.net.protocol.nested; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/package-info.java similarity index 75% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/package-info.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/package-info.java index 27ce99b006f0..fa1a2cfb7a4c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/archive/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/package-info.java @@ -15,9 +15,6 @@ */ /** - * Abstraction over logical Archives be they backed by a JAR file or unpacked into a - * directory. - * - * @see org.springframework.boot.loader.archive.Archive + * {@link java.net.URL} protocol support. */ -package org.springframework.boot.loader.archive; +package org.springframework.boot.loader.net.protocol; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/util/UrlDecoder.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/util/UrlDecoder.java new file mode 100644 index 000000000000..999c55140e04 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/util/UrlDecoder.java @@ -0,0 +1,109 @@ +/* + * Copyright 2012-2023 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.loader.net.util; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; + +/** + * Utility to decode URL strings. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public final class UrlDecoder { + + private UrlDecoder() { + } + + /** + * Decode the given string by decoding URL {@code '%'} escapes. This method should be + * identical in behavior to the {@code decode} method in the internal + * {@code sun.net.www.ParseUtil} JDK class. + * @param string the string to decode + * @return the decoded string + */ + public static String decode(String string) { + int length = string.length(); + if ((length == 0) || (string.indexOf('%') < 0)) { + return string; + } + StringBuilder result = new StringBuilder(length); + ByteBuffer byteBuffer = ByteBuffer.allocate(length); + CharBuffer charBuffer = CharBuffer.allocate(length); + CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT); + int index = 0; + while (index < length) { + char ch = string.charAt(index); + if (ch != '%') { + result.append(ch); + if (index + 1 >= length) { + return result.toString(); + } + index++; + continue; + } + index = fillByteBuffer(byteBuffer, string, index, length); + decodeToCharBuffer(byteBuffer, charBuffer, decoder); + result.append(charBuffer.flip()); + + } + return result.toString(); + } + + private static int fillByteBuffer(ByteBuffer byteBuffer, String string, int index, int length) { + byteBuffer.clear(); + while (true) { + byteBuffer.put(unescape(string, index)); + index += 3; + if (index >= length || string.charAt(index) != '%') { + break; + } + } + byteBuffer.flip(); + return index; + } + + private static byte unescape(String string, int index) { + try { + return (byte) Integer.parseInt(string, index + 1, index + 3, 16); + } + catch (NumberFormatException ex) { + throw new IllegalArgumentException(); + } + } + + private static void decodeToCharBuffer(ByteBuffer byteBuffer, CharBuffer charBuffer, CharsetDecoder decoder) { + decoder.reset(); + charBuffer.clear(); + assertNoError(decoder.decode(byteBuffer, charBuffer, true)); + assertNoError(decoder.flush(charBuffer)); + } + + private static void assertNoError(CoderResult result) { + if (result.isError()) { + throw new IllegalArgumentException("Error decoding percent encoded characters"); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/util/package-info.java similarity index 87% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/package-info.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/util/package-info.java index d3d7eef2d9db..231571bee07a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/package-info.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/util/package-info.java @@ -15,6 +15,6 @@ */ /** - * Utilities used by Spring Boot's JAR loading. + * Net utilities. */ -package org.springframework.boot.loader.util; +package org.springframework.boot.loader.net.util; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/package-info.java deleted file mode 100644 index 4b32f644f542..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/package-info.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2012-2023 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. - */ - -/** - * System that allows self-contained JAR/WAR archives to be launched using - * {@code java -jar}. Archives can include nested packaged dependency JARs (there is no - * need to create shade style jars) and are executed without unpacking. The only - * constraint is that nested JARs must be stored in the archive uncompressed. - * - * @see org.springframework.boot.loader.JarLauncher - * @see org.springframework.boot.loader.WarLauncher - */ -package org.springframework.boot.loader; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ref/Cleaner.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ref/Cleaner.java new file mode 100644 index 000000000000..4b053b78d9fb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ref/Cleaner.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2023 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.loader.ref; + +import java.lang.ref.Cleaner.Cleanable; + +/** + * Wrapper for {@link java.lang.ref.Cleaner} providing registration support. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public interface Cleaner { + + /** + * Provides access to the default clean instance which delegates to + * {@link java.lang.ref.Cleaner}. + */ + Cleaner instance = DefaultCleaner.instance; + + /** + * Registers an object and the clean action to run when the object becomes phantom + * reachable. + * @param obj the object to monitor + * @param action the cleanup action to run + * @return a {@link Cleanable} instance + * @see java.lang.ref.Cleaner#register(Object, Runnable) + */ + Cleanable register(Object obj, Runnable action); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ref/DefaultCleaner.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ref/DefaultCleaner.java new file mode 100644 index 000000000000..e592de5c85c5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ref/DefaultCleaner.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2023 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.loader.ref; + +import java.lang.ref.Cleaner.Cleanable; +import java.util.function.Consumer; + +/** + * Default {@link Cleaner} implementation that delegates to {@link java.lang.ref.Cleaner}. + * + * @author Phillip Webb + */ +class DefaultCleaner implements Cleaner { + + static final DefaultCleaner instance = new DefaultCleaner(); + + static Consumer tracker; + + private final java.lang.ref.Cleaner cleaner = java.lang.ref.Cleaner.create(); + + @Override + public Cleanable register(Object obj, Runnable action) { + Cleanable cleanable = this.cleaner.register(obj, action); + if (tracker != null) { + tracker.accept(cleanable); + } + return cleanable; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ref/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ref/package-info.java new file mode 100644 index 000000000000..4cb63bb4a64d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ref/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Support for {@link java.lang.ref.Cleaner}. + */ +package org.springframework.boot.loader.ref; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java deleted file mode 100644 index df00705e9eec..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/util/SystemPropertyUtils.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.util; - -import java.util.HashSet; -import java.util.Locale; -import java.util.Properties; -import java.util.Set; - -/** - * Helper class for resolving placeholders in texts. Usually applied to file paths. - *

    - * A text may contain {@code $ ...} placeholders, to be resolved as system properties: - * e.g. {@code $ user.dir}. Default values can be supplied using the ":" separator between - * key and value. - *

    - * Adapted from Spring. - * - * @author Juergen Hoeller - * @author Rob Harrop - * @author Dave Syer - * @since 1.0.0 - * @see System#getProperty(String) - */ -public abstract class SystemPropertyUtils { - - /** - * Prefix for system property placeholders: "${". - */ - public static final String PLACEHOLDER_PREFIX = "${"; - - /** - * Suffix for system property placeholders: "}". - */ - public static final String PLACEHOLDER_SUFFIX = "}"; - - /** - * Value separator for system property placeholders: ":". - */ - public static final String VALUE_SEPARATOR = ":"; - - private static final String SIMPLE_PREFIX = PLACEHOLDER_PREFIX.substring(1); - - /** - * Resolve ${...} placeholders in the given text, replacing them with corresponding - * system property values. - * @param text the String to resolve - * @return the resolved String - * @throws IllegalArgumentException if there is an unresolvable placeholder - * @see #PLACEHOLDER_PREFIX - * @see #PLACEHOLDER_SUFFIX - */ - public static String resolvePlaceholders(String text) { - if (text == null) { - return text; - } - return parseStringValue(null, text, text, new HashSet<>()); - } - - /** - * Resolve ${...} placeholders in the given text, replacing them with corresponding - * system property values. - * @param properties a properties instance to use in addition to System - * @param text the String to resolve - * @return the resolved String - * @throws IllegalArgumentException if there is an unresolvable placeholder - * @see #PLACEHOLDER_PREFIX - * @see #PLACEHOLDER_SUFFIX - */ - public static String resolvePlaceholders(Properties properties, String text) { - if (text == null) { - return text; - } - return parseStringValue(properties, text, text, new HashSet<>()); - } - - private static String parseStringValue(Properties properties, String value, String current, - Set visitedPlaceholders) { - - StringBuilder buf = new StringBuilder(current); - - int startIndex = current.indexOf(PLACEHOLDER_PREFIX); - while (startIndex != -1) { - int endIndex = findPlaceholderEndIndex(buf, startIndex); - if (endIndex != -1) { - String placeholder = buf.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex); - String originalPlaceholder = placeholder; - if (!visitedPlaceholders.add(originalPlaceholder)) { - throw new IllegalArgumentException( - "Circular placeholder reference '" + originalPlaceholder + "' in property definitions"); - } - // Recursive invocation, parsing placeholders contained in the - // placeholder - // key. - placeholder = parseStringValue(properties, value, placeholder, visitedPlaceholders); - // Now obtain the value for the fully resolved key... - String propVal = resolvePlaceholder(properties, value, placeholder); - if (propVal == null) { - int separatorIndex = placeholder.indexOf(VALUE_SEPARATOR); - if (separatorIndex != -1) { - String actualPlaceholder = placeholder.substring(0, separatorIndex); - String defaultValue = placeholder.substring(separatorIndex + VALUE_SEPARATOR.length()); - propVal = resolvePlaceholder(properties, value, actualPlaceholder); - if (propVal == null) { - propVal = defaultValue; - } - } - } - if (propVal != null) { - // Recursive invocation, parsing placeholders contained in the - // previously resolved placeholder value. - propVal = parseStringValue(properties, value, propVal, visitedPlaceholders); - buf.replace(startIndex, endIndex + PLACEHOLDER_SUFFIX.length(), propVal); - startIndex = buf.indexOf(PLACEHOLDER_PREFIX, startIndex + propVal.length()); - } - else { - // Proceed with unprocessed value. - startIndex = buf.indexOf(PLACEHOLDER_PREFIX, endIndex + PLACEHOLDER_SUFFIX.length()); - } - visitedPlaceholders.remove(originalPlaceholder); - } - else { - startIndex = -1; - } - } - - return buf.toString(); - } - - private static String resolvePlaceholder(Properties properties, String text, String placeholderName) { - String propVal = getProperty(placeholderName, null, text); - if (propVal != null) { - return propVal; - } - return (properties != null) ? properties.getProperty(placeholderName) : null; - } - - public static String getProperty(String key) { - return getProperty(key, null, ""); - } - - public static String getProperty(String key, String defaultValue) { - return getProperty(key, defaultValue, ""); - } - - /** - * Search the System properties and environment variables for a value with the - * provided key. Environment variables in {@code UPPER_CASE} style are allowed where - * System properties would normally be {@code lower.case}. - * @param key the key to resolve - * @param defaultValue the default value - * @param text optional extra context for an error message if the key resolution fails - * (e.g. if System properties are not accessible) - * @return a static property value or null of not found - */ - public static String getProperty(String key, String defaultValue, String text) { - try { - String propVal = System.getProperty(key); - if (propVal == null) { - // Fall back to searching the system environment. - propVal = System.getenv(key); - } - if (propVal == null) { - // Try with underscores. - String name = key.replace('.', '_'); - propVal = System.getenv(name); - } - if (propVal == null) { - // Try uppercase with underscores as well. - String name = key.toUpperCase(Locale.ENGLISH).replace('.', '_'); - propVal = System.getenv(name); - } - if (propVal != null) { - return propVal; - } - } - catch (Throwable ex) { - System.err.println("Could not resolve key '" + key + "' in '" + text - + "' as system property or in environment: " + ex); - } - return defaultValue; - } - - private static int findPlaceholderEndIndex(CharSequence buf, int startIndex) { - int index = startIndex + PLACEHOLDER_PREFIX.length(); - int withinNestedPlaceholder = 0; - while (index < buf.length()) { - if (substringMatch(buf, index, PLACEHOLDER_SUFFIX)) { - if (withinNestedPlaceholder > 0) { - withinNestedPlaceholder--; - index = index + PLACEHOLDER_SUFFIX.length(); - } - else { - return index; - } - } - else if (substringMatch(buf, index, SIMPLE_PREFIX)) { - withinNestedPlaceholder++; - index = index + SIMPLE_PREFIX.length(); - } - else { - index++; - } - } - return -1; - } - - private static boolean substringMatch(CharSequence str, int index, CharSequence substring) { - for (int j = 0; j < substring.length(); j++) { - int i = index + j; - if (i >= str.length() || str.charAt(i) != substring.charAt(j)) { - return false; - } - } - return true; - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ByteArrayDataBlock.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ByteArrayDataBlock.java new file mode 100644 index 000000000000..577cb2dc3b29 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ByteArrayDataBlock.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * {@link DataBlock} backed by a byte array . + * + * @author Phillip Webb + */ +class ByteArrayDataBlock implements DataBlock { + + private final byte[] bytes; + + /** + * Create a new {@link ByteArrayDataBlock} backed by the given bytes. + * @param bytes the bytes to use + */ + ByteArrayDataBlock(byte... bytes) { + this.bytes = bytes; + } + + @Override + public long size() throws IOException { + return this.bytes.length; + } + + @Override + public int read(ByteBuffer dst, long pos) throws IOException { + return read(dst, (int) pos); + } + + private int read(ByteBuffer dst, int pos) { + int remaining = dst.remaining(); + int length = Math.min(this.bytes.length - pos, remaining); + dst.put(this.bytes, pos, length); + return length; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/CloseableDataBlock.java similarity index 65% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/CloseableDataBlock.java index d46a22555dcb..6303daf4dcd6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Bytes.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/CloseableDataBlock.java @@ -14,24 +14,16 @@ * limitations under the License. */ -package org.springframework.boot.loader.jar; +package org.springframework.boot.loader.zip; + +import java.io.Closeable; /** - * Utilities for dealing with bytes from ZIP files. + * A {@link Closeable} {@link DataBlock}. * * @author Phillip Webb + * @since 3.2.0 */ -final class Bytes { - - private Bytes() { - } - - static long littleEndianValue(byte[] bytes, int offset, int length) { - long value = 0; - for (int i = length - 1; i >= 0; i--) { - value = ((value << 8) | (bytes[offset + i] & 0xFF)); - } - return value; - } +public interface CloseableDataBlock extends DataBlock, Closeable { } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/DataBlock.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/DataBlock.java new file mode 100644 index 000000000000..7475b67173b2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/DataBlock.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +/** + * Provides read access to a block of data contained somewhere in a zip file. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public interface DataBlock { + + /** + * Return the size of this block. + * @return the block size + * @throws IOException on I/O error + */ + long size() throws IOException; + + /** + * Read a sequence of bytes from this channel into the given buffer, starting at the + * given block position. + * @param dst the buffer into which bytes are to be transferred + * @param pos the position within the block at which the transfer is to begin + * @return the number of bytes read, possibly zero, or {@code -1} if the given + * position is greater than or equal to the block size + * @throws IOException on I/O error + * @see #readFully(ByteBuffer, long) + * @see FileChannel#read(ByteBuffer, long) + */ + int read(ByteBuffer dst, long pos) throws IOException; + + /** + * Fully read a sequence of bytes from this channel into the given buffer, starting at + * the given block position and filling {@link ByteBuffer#remaining() remaining} bytes + * in the buffer. + * @param dst the buffer into which bytes are to be transferred + * @param pos the position within the block at which the transfer is to begin + * @throws EOFException if an attempt is made to read past the end of the block + * @throws IOException on I/O error + */ + default void readFully(ByteBuffer dst, long pos) throws IOException { + do { + int count = read(dst, pos); + if (count <= 0) { + throw new EOFException(); + } + pos += count; + } + while (dst.hasRemaining()); + } + + /** + * Return this {@link DataBlock} as an {@link InputStream}. + * @return an {@link InputStream} to read the data block content + */ + default InputStream asInputStream() { + return new DataBlockInputStream(this); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/DataBlockInputStream.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/DataBlockInputStream.java new file mode 100644 index 000000000000..a05ae60f3e44 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/DataBlockInputStream.java @@ -0,0 +1,110 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.zip.ZipException; + +/** + * {@link InputStream} backed by a {@link DataBlock}. + * + * @author Phillip Webb + */ +class DataBlockInputStream extends InputStream { + + private final DataBlock dataBlock; + + private long pos; + + private long remaining; + + private volatile boolean closing; + + DataBlockInputStream(DataBlock dataBlock) { + this.dataBlock = dataBlock; + } + + @Override + public int read() throws IOException { + byte[] b = new byte[1]; + return (read(b, 0, 1) == 1) ? b[0] & 0xFF : -1; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int result; + ensureOpen(); + ByteBuffer dst = ByteBuffer.wrap(b, off, len); + int count = this.dataBlock.read(dst, this.pos); + if (count > 0) { + this.pos += count; + this.remaining -= count; + } + result = count; + if (this.remaining == 0) { + close(); + } + return result; + } + + @Override + public long skip(long n) throws IOException { + long result; + result = (n > 0) ? maxForwardSkip(n) : maxBackwardSkip(n); + this.pos += result; + this.remaining -= result; + if (this.remaining == 0) { + close(); + } + return result; + } + + private long maxForwardSkip(long n) { + boolean willCauseOverflow = (this.pos + n) < 0; + return (willCauseOverflow || n > this.remaining) ? this.remaining : n; + } + + private long maxBackwardSkip(long n) { + return Math.max(-this.pos, n); + } + + @Override + public int available() { + return (this.remaining < Integer.MAX_VALUE) ? (int) this.remaining : Integer.MAX_VALUE; + } + + private void ensureOpen() throws ZipException { + if (this.closing) { + throw new ZipException("InputStream closed"); + } + } + + @Override + public void close() throws IOException { + if (this.closing) { + return; + } + this.closing = true; + if (this.dataBlock instanceof Closeable closeable) { + closeable.close(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/FileChannelDataBlock.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/FileChannelDataBlock.java new file mode 100644 index 000000000000..1824281ede34 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/FileChannelDataBlock.java @@ -0,0 +1,258 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.function.Supplier; + +import org.springframework.boot.loader.log.DebugLogger; + +/** + * Reference counted {@link DataBlock} implementation backed by a {@link FileChannel} with + * support for slicing. + * + * @author Phillip Webb + */ +class FileChannelDataBlock implements CloseableDataBlock { + + private static final DebugLogger debug = DebugLogger.get(FileChannelDataBlock.class); + + static Tracker tracker; + + private final ManagedFileChannel channel; + + private final long offset; + + private final long size; + + FileChannelDataBlock(Path path) throws IOException { + this.channel = new ManagedFileChannel(path); + this.offset = 0; + this.size = Files.size(path); + } + + FileChannelDataBlock(ManagedFileChannel channel, long offset, long size) { + this.channel = channel; + this.offset = offset; + this.size = size; + } + + @Override + public long size() throws IOException { + return this.size; + } + + @Override + public int read(ByteBuffer dst, long pos) throws IOException { + if (pos < 0) { + throw new IllegalArgumentException("Position must not be negative"); + } + ensureOpen(ClosedChannelException::new); + int remaining = (int) (this.size - pos); + if (remaining <= 0) { + return -1; + } + int originalDestinationLimit = -1; + if (dst.remaining() > remaining) { + originalDestinationLimit = dst.limit(); + dst.limit(dst.position() + remaining); + } + int result = this.channel.read(dst, this.offset + pos); + if (originalDestinationLimit != -1) { + dst.limit(originalDestinationLimit); + } + return result; + } + + /** + * Open a connection to this block, increasing the reference count and re-opening the + * underlying file channel if necessary. + * @throws IOException on I/O error + */ + void open() throws IOException { + this.channel.open(); + } + + /** + * Close a connection to this block, decreasing the reference count and closing the + * underlying file channel if necessary. + * @throws IOException on I/O error + */ + @Override + public void close() throws IOException { + this.channel.close(); + } + + /** + * Ensure that the underlying file channel is currently open. + * @param exceptionSupplier a supplier providing the exception to throw + * @param the exception type + * @throws E if the channel is closed + */ + void ensureOpen(Supplier exceptionSupplier) throws E { + this.channel.ensureOpen(exceptionSupplier); + } + + /** + * Return a new {@link FileChannelDataBlock} slice providing access to a subset of the + * data. The caller is responsible for calling {@link #open()} and {@link #close()} on + * the returned block. + * @param offset the start offset for the slice relative to this block + * @return a new {@link FileChannelDataBlock} instance + * @throws IOException on I/O error + */ + FileChannelDataBlock slice(long offset) throws IOException { + return slice(offset, this.size - offset); + } + + /** + * Return a new {@link FileChannelDataBlock} slice providing access to a subset of the + * data. The caller is responsible for calling {@link #open()} and {@link #close()} on + * the returned block. + * @param offset the start offset for the slice relative to this block + * @param size the size of the new slice + * @return a new {@link FileChannelDataBlock} instance + */ + FileChannelDataBlock slice(long offset, long size) { + if (offset == 0 && size == this.size) { + return this; + } + if (offset < 0) { + throw new IllegalArgumentException("Offset must not be negative"); + } + if (size < 0 || offset + size > this.size) { + throw new IllegalArgumentException("Size must not be negative and must be within bounds"); + } + debug.log("Slicing %s at %s with size %s", this.channel, offset, size); + return new FileChannelDataBlock(this.channel, this.offset + offset, size); + } + + /** + * Manages access to underlying {@link FileChannel}. + */ + static class ManagedFileChannel { + + static final int BUFFER_SIZE = 1024 * 10; + + private final Path path; + + private int referenceCount; + + private FileChannel fileChannel; + + private ByteBuffer buffer; + + private long bufferPosition = -1; + + private int bufferSize; + + private final Object lock = new Object(); + + ManagedFileChannel(Path path) { + if (!Files.isRegularFile(path)) { + throw new IllegalArgumentException(path + " must be a regular file"); + } + this.path = path; + } + + int read(ByteBuffer dst, long position) throws IOException { + synchronized (this.lock) { + if (position < this.bufferPosition || position >= this.bufferPosition + this.bufferSize) { + this.buffer.clear(); + this.bufferSize = this.fileChannel.read(this.buffer, position); + this.bufferPosition = position; + } + if (this.bufferSize <= 0) { + return this.bufferSize; + } + int offset = (int) (position - this.bufferPosition); + int length = Math.min(this.bufferSize - offset, dst.remaining()); + dst.put(dst.position(), this.buffer, offset, length); + dst.position(dst.position() + length); + return length; + } + } + + void open() throws IOException { + synchronized (this.lock) { + if (this.referenceCount == 0) { + debug.log("Opening '%s'", this.path); + this.fileChannel = FileChannel.open(this.path, StandardOpenOption.READ); + this.buffer = ByteBuffer.allocateDirect(BUFFER_SIZE); + if (tracker != null) { + tracker.openedFileChannel(this.path, this.fileChannel); + } + } + this.referenceCount++; + debug.log("Reference count for '%s' incremented to %s", this.path, this.referenceCount); + } + } + + void close() throws IOException { + synchronized (this.lock) { + if (this.referenceCount == 0) { + return; + } + this.referenceCount--; + if (this.referenceCount == 0) { + debug.log("Closing '%s'", this.path); + this.buffer = null; + this.bufferPosition = -1; + this.bufferSize = 0; + this.fileChannel.close(); + if (tracker != null) { + tracker.closedFileChannel(this.path, this.fileChannel); + } + this.fileChannel = null; + } + debug.log("Reference count for '%s' decremented to %s", this.path, this.referenceCount); + } + } + + void ensureOpen(Supplier exceptionSupplier) throws E { + synchronized (this.lock) { + if (this.referenceCount == 0) { + throw exceptionSupplier.get(); + } + } + } + + @Override + public String toString() { + return this.path.toString(); + } + + } + + /** + * Internal tracker used to check open and closing of files in tests. + */ + interface Tracker { + + void openedFileChannel(Path path, FileChannel fileChannel); + + void closedFileChannel(Path path, FileChannel fileChannel); + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/NameOffsetLookups.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/NameOffsetLookups.java new file mode 100644 index 000000000000..d3014448c570 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/NameOffsetLookups.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.util.BitSet; + +/** + * Tracks entries that have a name that should be offset by a specific amount. This class + * is used with nested directory zip files so that entries under the directory are offset + * correctly. META-INF entries are copied directly and have no offset. + * + * @author Phillip Webb + */ +class NameOffsetLookups { + + public static final NameOffsetLookups NONE = new NameOffsetLookups(0, 0); + + private final int offset; + + private final BitSet enabled; + + NameOffsetLookups(int offset, int size) { + this.offset = offset; + this.enabled = (size != 0) ? new BitSet(size) : null; + } + + void swap(int i, int j) { + if (this.enabled != null) { + boolean temp = this.enabled.get(i); + this.enabled.set(i, this.enabled.get(j)); + this.enabled.set(j, temp); + } + } + + int get(int index) { + return isEnabled(index) ? this.offset : 0; + } + + int enable(int index, boolean enable) { + if (this.enabled != null) { + this.enabled.set(index, enable); + } + return (!enable) ? 0 : this.offset; + } + + boolean isEnabled(int index) { + return (this.enabled != null && this.enabled.get(index)); + } + + boolean hasAnyEnabled() { + return this.enabled != null && this.enabled.cardinality() > 0; + } + + NameOffsetLookups emptyCopy() { + return new NameOffsetLookups(this.offset, this.enabled.size()); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualDataBlock.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualDataBlock.java new file mode 100644 index 000000000000..e8d8838700ca --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualDataBlock.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.List; + +/** + * A virtual {@link DataBlock} build from a collection of other {@link DataBlock} + * instances. + * + * @author Phillip Webb + */ +class VirtualDataBlock implements DataBlock { + + private List parts; + + private long size; + + /** + * Create a new {@link VirtualDataBlock} instance. The {@link #setParts(Collection)} + * method must be called before the data block can be used. + */ + protected VirtualDataBlock() { + } + + /** + * Create a new {@link VirtualDataBlock} backed by the given parts. + * @param parts the parts that make up the virtual data block + * @throws IOException in I/O error + */ + VirtualDataBlock(Collection parts) throws IOException { + setParts(parts); + } + + /** + * Set the parts that make up the virtual data block. + * @param parts the data block parts + * @throws IOException on I/O error + */ + protected void setParts(Collection parts) throws IOException { + this.parts = List.copyOf(parts); + long size = 0; + for (DataBlock part : parts) { + size += part.size(); + } + this.size = size; + } + + @Override + public long size() throws IOException { + return this.size; + } + + @Override + public int read(ByteBuffer dst, long pos) throws IOException { + if (pos < 0 || pos >= this.size) { + return -1; + } + long offset = 0; + int result = 0; + for (DataBlock part : this.parts) { + while (pos >= offset && pos < offset + part.size()) { + int count = part.read(dst, pos - offset); + result += Math.max(count, 0); + if (count <= 0 || !dst.hasRemaining()) { + return result; + } + pos += count; + } + offset += part.size(); + } + return result; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualZipDataBlock.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualZipDataBlock.java new file mode 100644 index 000000000000..21021da25e53 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualZipDataBlock.java @@ -0,0 +1,140 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.FileSystem; +import java.util.ArrayList; +import java.util.List; + +/** + * {@link DataBlock} that creates a virtual zip. This class allows us to create virtual + * zip files that can be parsed by regular JDK classes such as the zip {@link FileSystem}. + * + * @author Phillip Webb + */ +class VirtualZipDataBlock extends VirtualDataBlock implements CloseableDataBlock { + + private final FileChannelDataBlock data; + + /** + * Create a new {@link VirtualZipDataBlock} for the given entries. + * @param data the source zip data + * @param nameOffsetLookups the name offsets to apply + * @param centralRecords the records that should be copied to the virtual zip + * @param centralRecordPositions the record positions in the data block. + * @throws IOException on I/O error + */ + VirtualZipDataBlock(FileChannelDataBlock data, NameOffsetLookups nameOffsetLookups, + ZipCentralDirectoryFileHeaderRecord[] centralRecords, long[] centralRecordPositions) throws IOException { + this.data = data; + List parts = new ArrayList<>(); + List centralParts = new ArrayList<>(); + long offset = 0; + long sizeOfCentralDirectory = 0; + for (int i = 0; i < centralRecords.length; i++) { + ZipCentralDirectoryFileHeaderRecord centralRecord = centralRecords[i]; + int nameOffset = nameOffsetLookups.get(i); + long centralRecordPos = centralRecordPositions[i]; + DataBlock name = new DataPart( + centralRecordPos + ZipCentralDirectoryFileHeaderRecord.FILE_NAME_OFFSET + nameOffset, + (centralRecord.fileNameLength() & 0xFFFF) - nameOffset); + ZipLocalFileHeaderRecord localRecord = ZipLocalFileHeaderRecord.load(this.data, + centralRecord.offsetToLocalHeader()); + DataBlock content = new DataPart(centralRecord.offsetToLocalHeader() + localRecord.size(), + centralRecord.compressedSize()); + sizeOfCentralDirectory += addToCentral(centralParts, centralRecord, centralRecordPos, name, (int) offset); + offset += addToLocal(parts, localRecord, name, content); + } + parts.addAll(centralParts); + ZipEndOfCentralDirectoryRecord eocd = new ZipEndOfCentralDirectoryRecord((short) centralRecords.length, + (int) sizeOfCentralDirectory, (int) offset); + parts.add(new ByteArrayDataBlock(eocd.asByteArray())); + setParts(parts); + } + + private long addToCentral(List parts, ZipCentralDirectoryFileHeaderRecord originalRecord, + long originalRecordPos, DataBlock name, int offsetToLocalHeader) throws IOException { + ZipCentralDirectoryFileHeaderRecord record = originalRecord.withFileNameLength((short) (name.size() & 0xFFFF)) + .withOffsetToLocalHeader(offsetToLocalHeader); + int originalExtraFieldLength = originalRecord.extraFieldLength() & 0xFFFF; + int originalFileCommentLength = originalRecord.fileCommentLength() & 0xFFFF; + DataBlock extraFieldAndComment = new DataPart( + originalRecordPos + originalRecord.size() - originalExtraFieldLength - originalFileCommentLength, + originalExtraFieldLength + originalFileCommentLength); + parts.add(new ByteArrayDataBlock(record.asByteArray())); + parts.add(name); + parts.add(extraFieldAndComment); + return record.size(); + } + + private long addToLocal(List parts, ZipLocalFileHeaderRecord originalRecord, DataBlock name, + DataBlock content) throws IOException { + ZipLocalFileHeaderRecord record = originalRecord.withExtraFieldLength((short) 0) + .withFileNameLength((short) (name.size() & 0xFFFF)); + parts.add(new ByteArrayDataBlock(record.asByteArray())); + parts.add(name); + parts.add(content); + return record.size() + content.size(); + } + + @Override + public void close() throws IOException { + this.data.close(); + } + + /** + * {@link DataBlock} that points to part of the original data block. + */ + final class DataPart implements DataBlock { + + private final long offset; + + private final long size; + + DataPart(long offset, long size) { + this.offset = offset; + this.size = size; + } + + @Override + public long size() throws IOException { + return this.size; + } + + @Override + public int read(ByteBuffer dst, long pos) throws IOException { + int remaining = (int) (this.size - pos); + if (remaining <= 0) { + return -1; + } + int originalLimit = -1; + if (dst.remaining() > remaining) { + originalLimit = dst.limit(); + dst.limit(dst.position() + remaining); + } + int result = VirtualZipDataBlock.this.data.read(dst, this.offset + pos); + if (originalLimit != -1) { + dst.limit(originalLimit); + } + return result; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/Zip64EndOfCentralDirectoryLocator.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/Zip64EndOfCentralDirectoryLocator.java new file mode 100644 index 000000000000..078c5ad81d95 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/Zip64EndOfCentralDirectoryLocator.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import org.springframework.boot.loader.log.DebugLogger; + +/** + * A Zip64 end of central directory locator. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @param pos the position where this record begins in the source {@link DataBlock} + * @param numberOfThisDisk the number of the disk with the start of the zip64 end of + * central directory + * @param offsetToZip64EndOfCentralDirectoryRecord the relative offset of the zip64 end of + * central directory record + * @param totalNumberOfDisks the total number of disks + * @see Chapter + * 4.3.15 of the Zip File Format Specification + */ +record Zip64EndOfCentralDirectoryLocator(long pos, int numberOfThisDisk, long offsetToZip64EndOfCentralDirectoryRecord, + int totalNumberOfDisks) { + + private static final DebugLogger debug = DebugLogger.get(Zip64EndOfCentralDirectoryLocator.class); + + private static final int SIGNATURE = 0x07064b50; + + /** + * The size of this record. + */ + static final int SIZE = 20; + + /** + * Return the {@link Zip64EndOfCentralDirectoryLocator} or {@code null} if this is not + * a Zip64 file. + * @param dataBlock the source data block + * @param endOfCentralDirectoryPos the {@link ZipEndOfCentralDirectoryRecord} position + * @return a {@link Zip64EndOfCentralDirectoryLocator} instance or null + * @throws IOException on I/O error + */ + static Zip64EndOfCentralDirectoryLocator find(DataBlock dataBlock, long endOfCentralDirectoryPos) + throws IOException { + debug.log("Finding Zip64EndOfCentralDirectoryLocator from EOCD at %s", endOfCentralDirectoryPos); + long pos = endOfCentralDirectoryPos - SIZE; + if (pos < 0) { + debug.log("No Zip64EndOfCentralDirectoryLocator due to negative position %s", pos); + return null; + } + ByteBuffer buffer = ByteBuffer.allocate(SIZE); + buffer.order(ByteOrder.LITTLE_ENDIAN); + dataBlock.read(buffer, pos); + buffer.rewind(); + int signature = buffer.getInt(); + if (signature != SIGNATURE) { + debug.log("Found incorrect Zip64EndOfCentralDirectoryLocator signature %s at position %s", signature, pos); + return null; + } + debug.log("Found Zip64EndOfCentralDirectoryLocator at position %s", pos); + return new Zip64EndOfCentralDirectoryLocator(pos, buffer.getInt(), buffer.getLong(), buffer.getInt()); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/Zip64EndOfCentralDirectoryRecord.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/Zip64EndOfCentralDirectoryRecord.java new file mode 100644 index 000000000000..c593624ff94d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/Zip64EndOfCentralDirectoryRecord.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import org.springframework.boot.loader.log.DebugLogger; + +/** + * A Zip64 end of central directory record. + * + * @author Phillip Webb + * @param size the size of this record + * @param sizeOfZip64EndOfCentralDirectoryRecord the size of zip64 end of central + * directory record + * @param versionMadeBy the version that made the zip + * @param versionNeededToExtract the version needed to extract the zip + * @param numberOfThisDisk the number of this disk + * @param diskWhereCentralDirectoryStarts the disk where central directory starts + * @param numberOfCentralDirectoryEntriesOnThisDisk the number of central directory + * entries on this disk + * @param totalNumberOfCentralDirectoryEntries the total number of central directory + * entries + * @param sizeOfCentralDirectory the size of central directory (bytes) + * @param offsetToStartOfCentralDirectory the offset of start of central directory, + * relative to start of archive + * @see Chapter + * 4.3.14 of the Zip File Format Specification + */ +record Zip64EndOfCentralDirectoryRecord(long size, long sizeOfZip64EndOfCentralDirectoryRecord, short versionMadeBy, + short versionNeededToExtract, int numberOfThisDisk, int diskWhereCentralDirectoryStarts, + long numberOfCentralDirectoryEntriesOnThisDisk, long totalNumberOfCentralDirectoryEntries, + long sizeOfCentralDirectory, long offsetToStartOfCentralDirectory) { + + private static final DebugLogger debug = DebugLogger.get(Zip64EndOfCentralDirectoryRecord.class); + + private static final int SIGNATURE = 0x06064b50; + + private static final int MINIMUM_SIZE = 56; + + /** + * Load the {@link Zip64EndOfCentralDirectoryRecord} from the given data block based + * on the offset given in the locator. + * @param dataBlock the source data block + * @param locator the {@link Zip64EndOfCentralDirectoryLocator} or {@code null} + * @return a new {@link ZipCentralDirectoryFileHeaderRecord} instance or {@code null} + * if the locator is {@code null} + * @throws IOException on I/O error + */ + static Zip64EndOfCentralDirectoryRecord load(DataBlock dataBlock, Zip64EndOfCentralDirectoryLocator locator) + throws IOException { + if (locator == null) { + return null; + } + ByteBuffer buffer = ByteBuffer.allocate(MINIMUM_SIZE); + buffer.order(ByteOrder.LITTLE_ENDIAN); + long size = locator.pos() - locator.offsetToZip64EndOfCentralDirectoryRecord(); + long pos = locator.pos() - size; + debug.log("Loading Zip64EndOfCentralDirectoryRecord from position %s size %s", pos, size); + dataBlock.readFully(buffer, pos); + buffer.rewind(); + int signature = buffer.getInt(); + if (signature != SIGNATURE) { + debug.log("Found incorrect Zip64EndOfCentralDirectoryRecord signature %s at position %s", signature, pos); + throw new IOException("Zip64 'End Of Central Directory Record' not found at position " + pos + + ". Zip file is corrupt or includes prefixed bytes which are not supported with Zip64 files"); + } + return new Zip64EndOfCentralDirectoryRecord(size, buffer.getLong(), buffer.getShort(), buffer.getShort(), + buffer.getInt(), buffer.getInt(), buffer.getLong(), buffer.getLong(), buffer.getLong(), + buffer.getLong()); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipCentralDirectoryFileHeaderRecord.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipCentralDirectoryFileHeaderRecord.java new file mode 100644 index 000000000000..27f03587ae84 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipCentralDirectoryFileHeaderRecord.java @@ -0,0 +1,211 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.time.temporal.ValueRange; +import java.util.zip.ZipEntry; + +import org.springframework.boot.loader.log.DebugLogger; + +/** + * A ZIP File "Central directory file header record" (CDFH). + * + * @author Phillip Webb + * @param versionMadeBy the version that made the zip + * @param versionNeededToExtract the version needed to extract the zip + * @param generalPurposeBitFlag the general purpose bit flag + * @param compressionMethod the compression method used for this entry + * @param lastModFileTime the last modified file time + * @param lastModFileDate the last modified file date + * @param crc32 the CRC32 checksum + * @param compressedSize the size of the entry when compressed + * @param uncompressedSize the size of the entry when uncompressed + * @param fileNameLength the file name length + * @param extraFieldLength the extra field length + * @param fileCommentLength the comment length + * @param diskNumberStart the disk number where the entry starts + * @param internalFileAttributes the internal file attributes + * @param externalFileAttributes the external file attributes + * @param offsetToLocalHeader the relative offset to the local file header + * @see Chapter + * 4.3.12 of the Zip File Format Specification + */ +record ZipCentralDirectoryFileHeaderRecord(short versionMadeBy, short versionNeededToExtract, + short generalPurposeBitFlag, short compressionMethod, short lastModFileTime, short lastModFileDate, int crc32, + int compressedSize, int uncompressedSize, short fileNameLength, short extraFieldLength, short fileCommentLength, + short diskNumberStart, short internalFileAttributes, int externalFileAttributes, int offsetToLocalHeader) { + + private static final DebugLogger debug = DebugLogger.get(ZipCentralDirectoryFileHeaderRecord.class); + + private static final int SIGNATURE = 0x02014b50; + + private static final int MINIMUM_SIZE = 46; + + /** + * The offset of the file name relative to the record start position. + */ + static final int FILE_NAME_OFFSET = MINIMUM_SIZE; + + /** + * Return the size of this record. + * @return the record size + */ + long size() { + return MINIMUM_SIZE + fileNameLength() + extraFieldLength() + fileCommentLength(); + } + + /** + * Copy values from this block to the given {@link ZipEntry}. + * @param dataBlock the source data block + * @param pos the position of this {@link ZipCentralDirectoryFileHeaderRecord} + * @param zipEntry the destination zip entry + * @throws IOException on I/O error + */ + void copyTo(DataBlock dataBlock, long pos, ZipEntry zipEntry) throws IOException { + int fileNameLength = fileNameLength() & 0xFFFF; + int extraLength = extraFieldLength() & 0xFFFF; + int commentLength = fileCommentLength() & 0xFFFF; + zipEntry.setMethod(compressionMethod() & 0xFFFF); + zipEntry.setTime(decodeMsDosFormatDateTime(lastModFileDate(), lastModFileTime())); + zipEntry.setCrc(crc32() & 0xFFFFFFFFL); + zipEntry.setCompressedSize(compressedSize() & 0xFFFFFFFFL); + zipEntry.setSize(uncompressedSize() & 0xFFFFFFFFL); + if (extraLength > 0) { + long extraPos = pos + MINIMUM_SIZE + fileNameLength; + ByteBuffer buffer = ByteBuffer.allocate(extraLength); + dataBlock.readFully(buffer, extraPos); + zipEntry.setExtra(buffer.array()); + } + if ((fileCommentLength() & 0xFFFF) > 0) { + long commentPos = MINIMUM_SIZE + fileNameLength + extraLength; + zipEntry.setComment(ZipString.readString(dataBlock, commentPos, commentLength)); + } + } + + /** + * Decode MS-DOS Date Time details. See + * Microsoft's documentation for more details of the format. + * @param date the date + * @param time the time + * @return the date and time as milliseconds since the epoch + */ + private long decodeMsDosFormatDateTime(short date, short time) { + int year = getChronoValue(((date >> 9) & 0x7f) + 1980, ChronoField.YEAR); + int month = getChronoValue((date >> 5) & 0x0f, ChronoField.MONTH_OF_YEAR); + int day = getChronoValue(date & 0x1f, ChronoField.DAY_OF_MONTH); + int hour = getChronoValue((time >> 11) & 0x1f, ChronoField.HOUR_OF_DAY); + int minute = getChronoValue((time >> 5) & 0x3f, ChronoField.MINUTE_OF_HOUR); + int second = getChronoValue((time << 1) & 0x3e, ChronoField.SECOND_OF_MINUTE); + return ZonedDateTime.of(year, month, day, hour, minute, second, 0, ZoneId.systemDefault()) + .toInstant() + .truncatedTo(ChronoUnit.SECONDS) + .toEpochMilli(); + } + + private static int getChronoValue(long value, ChronoField field) { + ValueRange range = field.range(); + return Math.toIntExact(Math.min(Math.max(value, range.getMinimum()), range.getMaximum())); + } + + /** + * Return a new {@link ZipCentralDirectoryFileHeaderRecord} with a new + * {@link #fileNameLength()}. + * @param fileNameLength the new file name length + * @return a new {@link ZipCentralDirectoryFileHeaderRecord} instance + */ + ZipCentralDirectoryFileHeaderRecord withFileNameLength(short fileNameLength) { + return (this.fileNameLength != fileNameLength) ? new ZipCentralDirectoryFileHeaderRecord(this.versionMadeBy, + this.versionNeededToExtract, this.generalPurposeBitFlag, this.compressionMethod, this.lastModFileTime, + this.lastModFileDate, this.crc32, this.compressedSize, this.uncompressedSize, fileNameLength, + this.extraFieldLength, this.fileCommentLength, this.diskNumberStart, this.internalFileAttributes, + this.externalFileAttributes, this.offsetToLocalHeader) : this; + } + + /** + * Return a new {@link ZipCentralDirectoryFileHeaderRecord} with a new + * {@link #offsetToLocalHeader()}. + * @param offsetToLocalHeader the new offset to local header + * @return a new {@link ZipCentralDirectoryFileHeaderRecord} instance + */ + ZipCentralDirectoryFileHeaderRecord withOffsetToLocalHeader(int offsetToLocalHeader) { + return (this.offsetToLocalHeader != offsetToLocalHeader) ? new ZipCentralDirectoryFileHeaderRecord( + this.versionMadeBy, this.versionNeededToExtract, this.generalPurposeBitFlag, this.compressionMethod, + this.lastModFileTime, this.lastModFileDate, this.crc32, this.compressedSize, this.uncompressedSize, + this.fileNameLength, this.extraFieldLength, this.fileCommentLength, this.diskNumberStart, + this.internalFileAttributes, this.externalFileAttributes, offsetToLocalHeader) : this; + } + + /** + * Return the contents of this record as a byte array suitable for writing to a zip. + * @return the record as a byte array + */ + byte[] asByteArray() { + ByteBuffer buffer = ByteBuffer.allocate(MINIMUM_SIZE); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.putInt(SIGNATURE); + buffer.putShort(this.versionMadeBy); + buffer.putShort(this.versionNeededToExtract); + buffer.putShort(this.generalPurposeBitFlag); + buffer.putShort(this.compressionMethod); + buffer.putShort(this.lastModFileTime); + buffer.putShort(this.lastModFileDate); + buffer.putInt(this.crc32); + buffer.putInt(this.compressedSize); + buffer.putInt(this.uncompressedSize); + buffer.putShort(this.fileNameLength); + buffer.putShort(this.extraFieldLength); + buffer.putShort(this.fileCommentLength); + buffer.putShort(this.diskNumberStart); + buffer.putShort(this.internalFileAttributes); + buffer.putInt(this.externalFileAttributes); + buffer.putInt(this.offsetToLocalHeader); + return buffer.array(); + } + + /** + * Load the {@link ZipCentralDirectoryFileHeaderRecord} from the given data block. + * @param dataBlock the source data block + * @param pos the position of the record + * @return a new {@link ZipCentralDirectoryFileHeaderRecord} instance + * @throws IOException on I/O error + */ + static ZipCentralDirectoryFileHeaderRecord load(DataBlock dataBlock, long pos) throws IOException { + debug.log("Loading CentralDirectoryFileHeaderRecord from position %s", pos); + ByteBuffer buffer = ByteBuffer.allocate(MINIMUM_SIZE); + buffer.order(ByteOrder.LITTLE_ENDIAN); + dataBlock.readFully(buffer, pos); + buffer.rewind(); + int signature = buffer.getInt(); + if (signature != SIGNATURE) { + debug.log("Found incorrect CentralDirectoryFileHeaderRecord signature %s at position %s", signature, pos); + throw new IOException("Zip 'Central Directory File Header Record' not found at position " + pos); + } + return new ZipCentralDirectoryFileHeaderRecord(buffer.getShort(), buffer.getShort(), buffer.getShort(), + buffer.getShort(), buffer.getShort(), buffer.getShort(), buffer.getInt(), buffer.getInt(), + buffer.getInt(), buffer.getShort(), buffer.getShort(), buffer.getShort(), buffer.getShort(), + buffer.getShort(), buffer.getInt(), buffer.getInt()); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java new file mode 100644 index 000000000000..2130b0fc1e29 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java @@ -0,0 +1,811 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.io.Closeable; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.ref.Cleaner.Cleanable; +import java.lang.ref.SoftReference; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.zip.ZipEntry; + +import org.springframework.boot.loader.log.DebugLogger; + +/** + * Provides raw access to content from a regular or nested zip file. This class performs + * the low level parsing of a zip file and provide access to raw entry data that it + * contains. Unlike {@link java.util.zip.ZipFile}, this implementation can load content + * from a zip file nested inside another file as long as the entry is not compressed. + *

    + * In order to reduce memory consumption, this implementation stores only the the hash of + * the entry names, the central directory offsets and the original positions. Entries are + * stored internally in {@code hashCode} order so that a binary search can be used to + * quickly find an entry by name or determine if the zip file doesn't have a given entry. + *

    + * {@link ZipContent} for a typical Spring Boot application JAR will have somewhere in the + * region of 10,500 entries which should consume about 122K. + *

    + * {@link ZipContent} results are cached and it is assumed that zip content will not + * change once loaded. Entries and Strings are not cached and will be recreated on each + * access which may produce a lot of garbage. + *

    + * This implementation does not use {@link Cleanable} so care must be taken to release + * {@link ZipContent} resources. The {@link #close()} method should be called explicitly + * or by try-with-resources. Care must be take to only call close once. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @since 3.2.0 + */ +public final class ZipContent implements Closeable { + + private static final String META_INF = "META-INF/"; + + private static final byte[] SIGNATURE_SUFFIX = ".DSA".getBytes(StandardCharsets.UTF_8); + + private static final DebugLogger debug = DebugLogger.get(ZipContent.class); + + private static final Map cache = new ConcurrentHashMap<>(); + + private final Source source; + + private final FileChannelDataBlock data; + + private final long centralDirectoryPos; + + private final long commentPos; + + private final long commentLength; + + private final int[] lookupIndexes; + + private final int[] nameHashLookups; + + private final int[] relativeCentralDirectoryOffsetLookups; + + private final NameOffsetLookups nameOffsetLookups; + + private final boolean hasJarSignatureFile; + + private SoftReference virtualData; + + private SoftReference, Object>> info; + + private ZipContent(Source source, FileChannelDataBlock data, long centralDirectoryPos, long commentPos, + long commentLength, int[] lookupIndexes, int[] nameHashLookups, int[] relativeCentralDirectoryOffsetLookups, + NameOffsetLookups nameOffsetLookups, boolean hasJarSignatureFile) { + this.source = source; + this.data = data; + this.centralDirectoryPos = centralDirectoryPos; + this.commentPos = commentPos; + this.commentLength = commentLength; + this.lookupIndexes = lookupIndexes; + this.nameHashLookups = nameHashLookups; + this.relativeCentralDirectoryOffsetLookups = relativeCentralDirectoryOffsetLookups; + this.nameOffsetLookups = nameOffsetLookups; + this.hasJarSignatureFile = hasJarSignatureFile; + } + + /** + * Open a {@link DataBlock} containing the raw zip data. For container zip files, this + * may be smaller than the original file since additional bytes are permitted at the + * front of a zip file. For nested zip files, this will be only the contents of the + * nest zip. + *

    + * For nested directory zip files, a virtual data block will be created containing + * only the relevant content. + *

    + * To release resources, the {@link #close()} method of the data block should be + * called explicitly or by try-with-resources. + *

    + * The returned data block should not be accessed once {@link #close()} has been + * called. + * @return the zip data + * @throws IOException on I/O error + */ + public CloseableDataBlock openRawZipData() throws IOException { + this.data.open(); + return (!this.nameOffsetLookups.hasAnyEnabled()) ? this.data : getVirtualData(); + } + + private CloseableDataBlock getVirtualData() throws IOException { + CloseableDataBlock virtualData = (this.virtualData != null) ? this.virtualData.get() : null; + if (virtualData != null) { + return virtualData; + } + virtualData = createVirtualData(); + this.virtualData = new SoftReference<>(virtualData); + return virtualData; + } + + private CloseableDataBlock createVirtualData() throws IOException { + int size = size(); + NameOffsetLookups nameOffsetLookups = this.nameOffsetLookups.emptyCopy(); + ZipCentralDirectoryFileHeaderRecord[] centralRecords = new ZipCentralDirectoryFileHeaderRecord[size]; + long[] centralRecordPositions = new long[size]; + for (int i = 0; i < size; i++) { + int lookupIndex = ZipContent.this.lookupIndexes[i]; + long pos = getCentralDirectoryFileHeaderRecordPos(lookupIndex); + nameOffsetLookups.enable(i, this.nameOffsetLookups.isEnabled(lookupIndex)); + centralRecords[i] = ZipCentralDirectoryFileHeaderRecord.load(this.data, pos); + centralRecordPositions[i] = pos; + } + return new VirtualZipDataBlock(this.data, nameOffsetLookups, centralRecords, centralRecordPositions); + } + + /** + * Returns the number of entries in the ZIP file. + * @return the number of entries + */ + public int size() { + return this.lookupIndexes.length; + } + + /** + * Return the zip comment, if any. + * @return the comment or {@code null} + */ + public String getComment() { + try { + return ZipString.readString(this.data, this.commentPos, this.commentLength); + } + catch (UncheckedIOException ex) { + if (ex.getCause() instanceof ClosedChannelException) { + throw new IllegalStateException("Zip content closed", ex); + } + throw ex; + } + } + + /** + * Return the entry with the given name, if any. + * @param name the name of the entry to find + * @return the entry or {@code null} + */ + public Entry getEntry(CharSequence name) { + return getEntry(null, name); + } + + /** + * Return the entry with the given name, if any. + * @param namePrefix an optional prefix for the name + * @param name the name of the entry to find + * @return the entry or {@code null} + */ + public Entry getEntry(CharSequence namePrefix, CharSequence name) { + int nameHash = nameHash(namePrefix, name); + int lookupIndex = getFirstLookupIndex(nameHash); + int size = size(); + while (lookupIndex >= 0 && lookupIndex < size && this.nameHashLookups[lookupIndex] == nameHash) { + long pos = getCentralDirectoryFileHeaderRecordPos(lookupIndex); + ZipCentralDirectoryFileHeaderRecord centralRecord = loadZipCentralDirectoryFileHeaderRecord(pos); + if (hasName(lookupIndex, centralRecord, pos, namePrefix, name)) { + return new Entry(lookupIndex, centralRecord); + } + lookupIndex++; + } + return null; + } + + /** + * Return if an entry with the given name exists. + * @param namePrefix an optional prefix for the name + * @param name the name of the entry to find + * @return the entry or {@code null} + */ + public boolean hasEntry(CharSequence namePrefix, CharSequence name) { + int nameHash = nameHash(namePrefix, name); + int lookupIndex = getFirstLookupIndex(nameHash); + int size = size(); + while (lookupIndex >= 0 && lookupIndex < size && this.nameHashLookups[lookupIndex] == nameHash) { + long pos = getCentralDirectoryFileHeaderRecordPos(lookupIndex); + ZipCentralDirectoryFileHeaderRecord centralRecord = loadZipCentralDirectoryFileHeaderRecord(pos); + if (hasName(lookupIndex, centralRecord, pos, namePrefix, name)) { + return true; + } + lookupIndex++; + } + return false; + } + + /** + * Return the entry at the specified index. + * @param index the entry index + * @return the entry + * @throws IndexOutOfBoundsException if the index is out of bounds + */ + public Entry getEntry(int index) { + int lookupIndex = ZipContent.this.lookupIndexes[index]; + long pos = getCentralDirectoryFileHeaderRecordPos(lookupIndex); + ZipCentralDirectoryFileHeaderRecord centralRecord = loadZipCentralDirectoryFileHeaderRecord(pos); + return new Entry(lookupIndex, centralRecord); + } + + private ZipCentralDirectoryFileHeaderRecord loadZipCentralDirectoryFileHeaderRecord(long pos) { + try { + return ZipCentralDirectoryFileHeaderRecord.load(this.data, pos); + } + catch (IOException ex) { + if (ex instanceof ClosedChannelException) { + throw new IllegalStateException("Zip content closed", ex); + } + throw new UncheckedIOException(ex); + } + } + + private int nameHash(CharSequence namePrefix, CharSequence name) { + int nameHash = 0; + nameHash = (namePrefix != null) ? ZipString.hash(nameHash, namePrefix, false) : nameHash; + nameHash = ZipString.hash(nameHash, name, true); + return nameHash; + } + + private int getFirstLookupIndex(int nameHash) { + int lookupIndex = Arrays.binarySearch(this.nameHashLookups, 0, this.nameHashLookups.length, nameHash); + if (lookupIndex < 0) { + return -1; + } + while (lookupIndex > 0 && this.nameHashLookups[lookupIndex - 1] == nameHash) { + lookupIndex--; + } + return lookupIndex; + } + + private long getCentralDirectoryFileHeaderRecordPos(int lookupIndex) { + return this.centralDirectoryPos + this.relativeCentralDirectoryOffsetLookups[lookupIndex]; + } + + private boolean hasName(int lookupIndex, ZipCentralDirectoryFileHeaderRecord centralRecord, long pos, + CharSequence namePrefix, CharSequence name) { + int offset = this.nameOffsetLookups.get(lookupIndex); + pos += ZipCentralDirectoryFileHeaderRecord.FILE_NAME_OFFSET + offset; + int len = centralRecord.fileNameLength() - offset; + ByteBuffer buffer = ByteBuffer.allocate(ZipString.BUFFER_SIZE); + if (namePrefix != null) { + int startsWithNamePrefix = ZipString.startsWith(buffer, this.data, pos, len, namePrefix); + if (startsWithNamePrefix == -1) { + return false; + } + pos += startsWithNamePrefix; + len -= startsWithNamePrefix; + } + return ZipString.matches(buffer, this.data, pos, len, name, true); + } + + /** + * Get or compute information based on the {@link ZipContent}. + * @param the info type to get or compute + * @param type the info type to get or compute + * @param function the function used to compute the information + * @return the computed or existing information + */ + @SuppressWarnings("unchecked") + public I getInfo(Class type, Function function) { + Map, Object> info = (this.info != null) ? this.info.get() : null; + if (info == null) { + info = new ConcurrentHashMap<>(); + this.info = new SoftReference<>(info); + } + return (I) info.computeIfAbsent(type, (key) -> { + debug.log("Getting %s info from zip '%s'", type.getName(), this); + return function.apply(this); + }); + } + + /** + * Returns {@code true} if this zip contains a jar signature file + * ({@code META-INF/*.DSA}). + * @return if the zip contains a jar signature file + */ + public boolean hasJarSignatureFile() { + return this.hasJarSignatureFile; + } + + /** + * Close this jar file, releasing the underlying file if this was the last reference. + * @see java.io.Closeable#close() + */ + @Override + public void close() throws IOException { + this.data.close(); + } + + @Override + public String toString() { + return this.source.toString(); + } + + /** + * Open {@link ZipContent} from the specified path. The resulting {@link ZipContent} + * must be {@link #close() closed} by the caller. + * @param path the zip path + * @return a {@link ZipContent} instance + * @throws IOException on I/O error + */ + public static ZipContent open(Path path) throws IOException { + return open(new Source(path.toAbsolutePath(), null)); + } + + /** + * Open nested {@link ZipContent} from the specified path. The resulting + * {@link ZipContent} must be {@link #close() closed} by the caller. + * @param path the zip path + * @param nestedEntryName the nested entry name to open + * @return a {@link ZipContent} instance + * @throws IOException on I/O error + */ + public static ZipContent open(Path path, String nestedEntryName) throws IOException { + return open(new Source(path.toAbsolutePath(), nestedEntryName)); + } + + private static ZipContent open(Source source) throws IOException { + ZipContent zipContent = cache.get(source); + if (zipContent != null) { + debug.log("Opening existing cached zip content for %s", zipContent); + zipContent.data.open(); + return zipContent; + } + debug.log("Loading zip content from %s", source); + zipContent = Loader.load(source); + ZipContent previouslyCached = cache.putIfAbsent(source, zipContent); + if (previouslyCached != null) { + debug.log("Closing zip content from %s since cache was populated from another thread", source); + zipContent.close(); + previouslyCached.data.open(); + return previouslyCached; + } + return zipContent; + } + + /** + * The source of {@link ZipContent}. Used as a cache key. + * + * @param path the path of the zip or container zip + * @param nestedEntryName the name of the nested entry to use or {@code null} + */ + private record Source(Path path, String nestedEntryName) { + + /** + * Return if this is the source of a nested zip. + * @return if this is for a nested zip + */ + boolean isNested() { + return this.nestedEntryName != null; + } + + @Override + public String toString() { + return (!isNested()) ? path().toString() : path() + "[" + nestedEntryName() + "]"; + } + + } + + /** + * Internal class used to load the zip content create a new {@link ZipContent} + * instance. + */ + private static final class Loader { + + private final ByteBuffer buffer = ByteBuffer.allocate(ZipString.BUFFER_SIZE); + + private final Source source; + + private final FileChannelDataBlock data; + + private final long centralDirectoryPos; + + private final int[] index; + + private int[] nameHashLookups; + + private int[] relativeCentralDirectoryOffsetLookups; + + private final NameOffsetLookups nameOffsetLookups; + + private int cursor; + + private Loader(Source source, Entry directoryEntry, FileChannelDataBlock data, long centralDirectoryPos, + int maxSize) { + this.source = source; + this.data = data; + this.centralDirectoryPos = centralDirectoryPos; + this.index = new int[maxSize]; + this.nameHashLookups = new int[maxSize]; + this.relativeCentralDirectoryOffsetLookups = new int[maxSize]; + this.nameOffsetLookups = (directoryEntry != null) + ? new NameOffsetLookups(directoryEntry.getName().length(), maxSize) : NameOffsetLookups.NONE; + } + + private void add(ZipCentralDirectoryFileHeaderRecord centralRecord, long pos, boolean enableNameOffset) + throws IOException { + int nameOffset = this.nameOffsetLookups.enable(this.cursor, enableNameOffset); + int hash = ZipString.hash(this.buffer, this.data, + pos + ZipCentralDirectoryFileHeaderRecord.FILE_NAME_OFFSET + nameOffset, + centralRecord.fileNameLength() - nameOffset, true); + this.nameHashLookups[this.cursor] = hash; + this.relativeCentralDirectoryOffsetLookups[this.cursor] = (int) ((pos - this.centralDirectoryPos)); + this.index[this.cursor] = this.cursor; + this.cursor++; + } + + private ZipContent finish(long commentPos, long commentLength, boolean hasJarSignatureFile) { + if (this.cursor != this.nameHashLookups.length) { + this.nameHashLookups = Arrays.copyOf(this.nameHashLookups, this.cursor); + this.relativeCentralDirectoryOffsetLookups = Arrays.copyOf(this.relativeCentralDirectoryOffsetLookups, + this.cursor); + } + int size = this.nameHashLookups.length; + sort(0, size - 1); + int[] lookupIndexes = new int[size]; + for (int i = 0; i < size; i++) { + lookupIndexes[this.index[i]] = i; + } + return new ZipContent(this.source, this.data, this.centralDirectoryPos, commentPos, commentLength, + lookupIndexes, this.nameHashLookups, this.relativeCentralDirectoryOffsetLookups, + this.nameOffsetLookups, hasJarSignatureFile); + } + + private void sort(int left, int right) { + // Quick sort algorithm, uses nameHashCode as the source but sorts all arrays + if (left < right) { + int pivot = this.nameHashLookups[left + (right - left) / 2]; + int i = left; + int j = right; + while (i <= j) { + while (this.nameHashLookups[i] < pivot) { + i++; + } + while (this.nameHashLookups[j] > pivot) { + j--; + } + if (i <= j) { + swap(i, j); + i++; + j--; + } + } + if (left < j) { + sort(left, j); + } + if (right > i) { + sort(i, right); + } + } + } + + private void swap(int i, int j) { + swap(this.index, i, j); + swap(this.nameHashLookups, i, j); + swap(this.relativeCentralDirectoryOffsetLookups, i, j); + this.nameOffsetLookups.swap(i, j); + } + + private static void swap(int[] array, int i, int j) { + int temp = array[i]; + array[i] = array[j]; + array[j] = temp; + } + + static ZipContent load(Source source) throws IOException { + if (!source.isNested()) { + return loadNonNested(source); + } + try (ZipContent zip = open(source.path())) { + Entry entry = zip.getEntry(source.nestedEntryName()); + if (entry == null) { + throw new IOException("Nested entry '%s' not found in container zip '%s'" + .formatted(source.nestedEntryName(), source.path())); + } + return (!entry.isDirectory()) ? loadNestedZip(source, entry) : loadNestedDirectory(source, zip, entry); + } + } + + private static ZipContent loadNonNested(Source source) throws IOException { + debug.log("Loading non-nested zip '%s'", source.path()); + return openAndLoad(source, new FileChannelDataBlock(source.path())); + } + + private static ZipContent loadNestedZip(Source source, Entry entry) throws IOException { + if (entry.centralRecord.compressionMethod() != ZipEntry.STORED) { + throw new IOException("Nested entry '%s' in container zip '%s' must not be compressed" + .formatted(source.nestedEntryName(), source.path())); + } + debug.log("Loading nested zip entry '%s' from '%s'", source.nestedEntryName(), source.path()); + return openAndLoad(source, entry.getContent()); + } + + private static ZipContent openAndLoad(Source source, FileChannelDataBlock data) throws IOException { + try { + data.open(); + return loadContent(source, data); + } + catch (IOException | RuntimeException ex) { + data.close(); + throw ex; + } + } + + private static ZipContent loadContent(Source source, FileChannelDataBlock data) throws IOException { + ZipEndOfCentralDirectoryRecord.Located locatedEocd = ZipEndOfCentralDirectoryRecord.load(data); + ZipEndOfCentralDirectoryRecord eocd = locatedEocd.endOfCentralDirectoryRecord(); + long eocdPos = locatedEocd.pos(); + Zip64EndOfCentralDirectoryLocator zip64Locator = Zip64EndOfCentralDirectoryLocator.find(data, eocdPos); + Zip64EndOfCentralDirectoryRecord zip64Eocd = Zip64EndOfCentralDirectoryRecord.load(data, zip64Locator); + data = data.slice(getStartOfZipContent(data, eocd, zip64Eocd)); + long centralDirectoryPos = (zip64Eocd != null) ? zip64Eocd.offsetToStartOfCentralDirectory() + : eocd.offsetToStartOfCentralDirectory(); + long numberOfEntries = (zip64Eocd != null) ? zip64Eocd.totalNumberOfCentralDirectoryEntries() + : eocd.totalNumberOfCentralDirectoryEntries(); + if (numberOfEntries > 0xFFFFFFFFL) { + throw new IllegalStateException("Too many zip entries in " + source); + } + Loader loader = new Loader(source, null, data, centralDirectoryPos, (int) (numberOfEntries & 0xFFFFFFFFL)); + ByteBuffer signatureNameSuffixBuffer = ByteBuffer.allocate(SIGNATURE_SUFFIX.length); + boolean hasJarSignatureFile = false; + long pos = centralDirectoryPos; + for (int i = 0; i < numberOfEntries; i++) { + ZipCentralDirectoryFileHeaderRecord centralRecord = ZipCentralDirectoryFileHeaderRecord.load(data, pos); + if (!hasJarSignatureFile) { + long filenamePos = pos + ZipCentralDirectoryFileHeaderRecord.FILE_NAME_OFFSET; + if (centralRecord.fileNameLength() > SIGNATURE_SUFFIX.length && ZipString.startsWith(loader.buffer, + data, filenamePos, centralRecord.fileNameLength(), META_INF) >= 0) { + signatureNameSuffixBuffer.clear(); + data.readFully(signatureNameSuffixBuffer, + filenamePos + centralRecord.fileNameLength() - SIGNATURE_SUFFIX.length); + hasJarSignatureFile = Arrays.equals(SIGNATURE_SUFFIX, signatureNameSuffixBuffer.array()); + } + } + loader.add(centralRecord, pos, false); + pos += centralRecord.size(); + } + long commentPos = locatedEocd.pos() + ZipEndOfCentralDirectoryRecord.COMMENT_OFFSET; + return loader.finish(commentPos, eocd.commentLength(), hasJarSignatureFile); + } + + /** + * Returns the location in the data that the archive actually starts. For most + * files the archive data will start at 0, however, it is possible to have + * prefixed bytes (often used for startup scripts) at the beginning of the data. + * @param data the source data + * @param eocd the end of central directory record + * @param zip64Eocd the zip64 end of central directory record or {@code null} + * @return the offset within the data where the archive begins + * @throws IOException on I/O error + */ + private static long getStartOfZipContent(FileChannelDataBlock data, ZipEndOfCentralDirectoryRecord eocd, + Zip64EndOfCentralDirectoryRecord zip64Eocd) throws IOException { + long specifiedOffsetToStartOfCentralDirectory = (zip64Eocd != null) + ? zip64Eocd.offsetToStartOfCentralDirectory() : eocd.offsetToStartOfCentralDirectory(); + long sizeOfCentralDirectoryAndEndRecords = getSizeOfCentralDirectoryAndEndRecords(eocd, zip64Eocd); + long actualOffsetToStartOfCentralDirectory = data.size() - sizeOfCentralDirectoryAndEndRecords; + return actualOffsetToStartOfCentralDirectory - specifiedOffsetToStartOfCentralDirectory; + } + + private static long getSizeOfCentralDirectoryAndEndRecords(ZipEndOfCentralDirectoryRecord eocd, + Zip64EndOfCentralDirectoryRecord zip64Eocd) { + long result = 0; + result += eocd.size(); + if (zip64Eocd != null) { + result += Zip64EndOfCentralDirectoryLocator.SIZE; + result += zip64Eocd.size(); + } + result += (zip64Eocd != null) ? zip64Eocd.sizeOfCentralDirectory() : eocd.sizeOfCentralDirectory(); + return result; + } + + private static ZipContent loadNestedDirectory(Source source, ZipContent zip, Entry directoryEntry) + throws IOException { + debug.log("Loading nested directry entry '%s' from '%s'", source.nestedEntryName(), source.path()); + if (!source.nestedEntryName().endsWith("/")) { + throw new IllegalArgumentException("Nested entry name must end with '/'"); + } + String directoryName = directoryEntry.getName(); + zip.data.open(); + try { + Loader loader = new Loader(source, directoryEntry, zip.data, zip.centralDirectoryPos, zip.size()); + for (int cursor = 0; cursor < zip.size(); cursor++) { + int index = zip.lookupIndexes[cursor]; + if (index != directoryEntry.getLookupIndex()) { + long pos = zip.getCentralDirectoryFileHeaderRecordPos(index); + ZipCentralDirectoryFileHeaderRecord centralRecord = ZipCentralDirectoryFileHeaderRecord + .load(zip.data, pos); + long namePos = pos + ZipCentralDirectoryFileHeaderRecord.FILE_NAME_OFFSET; + short nameLen = centralRecord.fileNameLength(); + if (ZipString.startsWith(loader.buffer, zip.data, namePos, nameLen, META_INF) != -1) { + loader.add(centralRecord, pos, false); + } + else if (ZipString.startsWith(loader.buffer, zip.data, namePos, nameLen, directoryName) != -1) { + loader.add(centralRecord, pos, true); + } + } + } + return loader.finish(zip.commentPos, zip.commentLength, zip.hasJarSignatureFile); + } + catch (IOException | RuntimeException ex) { + zip.data.close(); + throw ex; + } + } + + } + + /** + * A single zip content entry. + */ + public class Entry { + + private final int lookupIndex; + + private final ZipCentralDirectoryFileHeaderRecord centralRecord; + + private volatile String name; + + private volatile FileChannelDataBlock content; + + /** + * Create a new {@link Entry} instance. + * @param lookupIndex the lookup index of the entry + * @param centralRecord the {@link ZipCentralDirectoryFileHeaderRecord} for the + * entry + */ + Entry(int lookupIndex, ZipCentralDirectoryFileHeaderRecord centralRecord) { + this.lookupIndex = lookupIndex; + this.centralRecord = centralRecord; + } + + /** + * Return the lookup index of the entry. Each entry has a unique lookup index but + * they aren't the same as the order that the entry was loaded. + * @return the entry lookup index + */ + public int getLookupIndex() { + return this.lookupIndex; + } + + /** + * Return {@code true} if this is a directory entry. + * @return if the entry is a directory + */ + public boolean isDirectory() { + return getName().endsWith("/"); + } + + /** + * Returns {@code true} if this entry has a name starting with the given prefix. + * @param prefix the required prefix + * @return if the entry name starts with the prefix + */ + public boolean hasNameStartingWith(CharSequence prefix) { + String name = this.name; + if (name != null) { + return name.startsWith(prefix.toString()); + } + long pos = getCentralDirectoryFileHeaderRecordPos(this.lookupIndex) + + ZipCentralDirectoryFileHeaderRecord.FILE_NAME_OFFSET; + return ZipString.startsWith(null, ZipContent.this.data, pos, this.centralRecord.fileNameLength(), + prefix) != -1; + } + + /** + * Return the name of this entry. + * @return the entry name + */ + public String getName() { + String name = this.name; + if (name == null) { + int offset = ZipContent.this.nameOffsetLookups.get(this.lookupIndex); + long pos = getCentralDirectoryFileHeaderRecordPos(this.lookupIndex) + + ZipCentralDirectoryFileHeaderRecord.FILE_NAME_OFFSET + offset; + name = ZipString.readString(ZipContent.this.data, pos, this.centralRecord.fileNameLength() - offset); + this.name = name; + } + return name; + } + + /** + * Return the compression method for this entry. + * @return the compression method + * @see ZipEntry#STORED + * @see ZipEntry#DEFLATED + */ + public int getCompressionMethod() { + return this.centralRecord.compressionMethod(); + } + + /** + * Return the uncompressed size of this entry. + * @return the uncompressed size + */ + public int getUncompressedSize() { + return this.centralRecord.uncompressedSize(); + } + + /** + * Open a {@link DataBlock} providing access to raw contents of the entry (not + * including the local file header). + *

    + * To release resources, the {@link #close()} method of the data block should be + * called explicitly or by try-with-resources. + * @return the contents of the entry + * @throws IOException on I/O error + */ + public CloseableDataBlock openContent() throws IOException { + FileChannelDataBlock content = getContent(); + content.open(); + return content; + } + + private FileChannelDataBlock getContent() throws IOException { + FileChannelDataBlock content = this.content; + if (content == null) { + int pos = this.centralRecord.offsetToLocalHeader(); + checkNotZip64Extended(pos); + ZipLocalFileHeaderRecord localHeader = ZipLocalFileHeaderRecord.load(ZipContent.this.data, pos); + int size = this.centralRecord.compressedSize(); + checkNotZip64Extended(size); + content = ZipContent.this.data.slice(pos + localHeader.size(), size); + this.content = content; + } + return content; + } + + private void checkNotZip64Extended(int value) throws IOException { + if (value == 0xFFFFFFFF) { + throw new IOException("Zip64 extended information extra fields are not supported"); + } + } + + /** + * Adapt the raw entry into a {@link ZipEntry} or {@link ZipEntry} subclass. + * @param the entry type + * @param factory the factory used to create the {@link ZipEntry} + * @return a fully populated zip entry + */ + public E as(Function factory) { + return as((entry, name) -> factory.apply(name)); + } + + /** + * Adapt the raw entry into a {@link ZipEntry} or {@link ZipEntry} subclass. + * @param the entry type + * @param factory the factory used to create the {@link ZipEntry} + * @return a fully populated zip entry + */ + public E as(BiFunction factory) { + try { + E result = factory.apply(this, getName()); + long pos = getCentralDirectoryFileHeaderRecordPos(this.lookupIndex); + this.centralRecord.copyTo(ZipContent.this.data, pos, result); + return result; + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipEndOfCentralDirectoryRecord.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipEndOfCentralDirectoryRecord.java new file mode 100644 index 000000000000..af2d8e57bf85 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipEndOfCentralDirectoryRecord.java @@ -0,0 +1,160 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import org.springframework.boot.loader.log.DebugLogger; + +/** + * A ZIP File "End of central directory record" (EOCD). + * + * @author Phillip Webb + * @param numberOfThisDisk the number of this disk (or 0xffff for Zip64) + * @param diskWhereCentralDirectoryStarts the disk where central directory starts (or + * 0xffff for Zip64) + * @param numberOfCentralDirectoryEntriesOnThisDisk the number of central directory + * entries on this disk (or 0xffff for Zip64) + * @param totalNumberOfCentralDirectoryEntries the total number of central directory + * entries (or 0xffff for Zip64) + * @param sizeOfCentralDirectory the size of central directory (bytes) (or 0xffffffff for + * Zip64) + * @param offsetToStartOfCentralDirectory the offset of start of central directory, + * relative to start of archive (or 0xffffffff for Zip64) + * @param commentLength the length of the comment field + * @see Chapter + * 4.3.16 of the Zip File Format Specification + */ +record ZipEndOfCentralDirectoryRecord(short numberOfThisDisk, short diskWhereCentralDirectoryStarts, + short numberOfCentralDirectoryEntriesOnThisDisk, short totalNumberOfCentralDirectoryEntries, + int sizeOfCentralDirectory, int offsetToStartOfCentralDirectory, short commentLength) { + + ZipEndOfCentralDirectoryRecord(short totalNumberOfCentralDirectoryEntries, int sizeOfCentralDirectory, + int offsetToStartOfCentralDirectory) { + this((short) 0, (short) 0, totalNumberOfCentralDirectoryEntries, totalNumberOfCentralDirectoryEntries, + sizeOfCentralDirectory, offsetToStartOfCentralDirectory, (short) 0); + } + + private static final DebugLogger debug = DebugLogger.get(ZipEndOfCentralDirectoryRecord.class); + + private static final int SIGNATURE = 0x06054b50; + + private static final int MAXIMUM_COMMENT_LENGTH = 0xFFFF; + + private static final int MINIMUM_SIZE = 22; + + private static final int MAXIMUM_SIZE = MINIMUM_SIZE + MAXIMUM_COMMENT_LENGTH; + + static final int BUFFER_SIZE = 256; + + /** + * The offset of the file comment relative to the record start position. + */ + static final int COMMENT_OFFSET = MINIMUM_SIZE; + + /** + * Return the size of this record. + * @return the record size + */ + long size() { + return MINIMUM_SIZE + this.commentLength; + } + + /** + * Return the contents of this record as a byte array suitable for writing to a zip. + * @return the record as a byte array + */ + byte[] asByteArray() { + ByteBuffer buffer = ByteBuffer.allocate(MINIMUM_SIZE); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.putInt(SIGNATURE); + buffer.putShort(this.numberOfThisDisk); + buffer.putShort(this.diskWhereCentralDirectoryStarts); + buffer.putShort(this.numberOfCentralDirectoryEntriesOnThisDisk); + buffer.putShort(this.totalNumberOfCentralDirectoryEntries); + buffer.putInt(this.sizeOfCentralDirectory); + buffer.putInt(this.offsetToStartOfCentralDirectory); + buffer.putShort(this.commentLength); + return buffer.array(); + } + + /** + * Create a new {@link ZipEndOfCentralDirectoryRecord} instance from the specified + * {@link DataBlock} by searching backwards from the end until a valid record is + * located. + * @param dataBlock the source data block + * @return the {@link Located located} {@link ZipEndOfCentralDirectoryRecord} + * @throws IOException if the {@link ZipEndOfCentralDirectoryRecord} cannot be read + */ + static Located load(DataBlock dataBlock) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); + buffer.order(ByteOrder.LITTLE_ENDIAN); + long pos = locate(dataBlock, buffer); + return new Located(pos, new ZipEndOfCentralDirectoryRecord(buffer.getShort(), buffer.getShort(), + buffer.getShort(), buffer.getShort(), buffer.getInt(), buffer.getInt(), buffer.getShort())); + } + + private static long locate(DataBlock dataBlock, ByteBuffer buffer) throws IOException { + long endPos = dataBlock.size(); + debug.log("Finding EndOfCentralDirectoryRecord starting at end position %s", endPos); + while (endPos > 0) { + buffer.clear(); + long totalRead = dataBlock.size() - endPos; + if (totalRead > MAXIMUM_SIZE) { + throw new IOException( + "Zip 'End Of Central Directory Record' not found after reading " + totalRead + " bytes"); + } + long startPos = endPos - buffer.limit(); + if (startPos < 0) { + buffer.limit((int) startPos + buffer.limit()); + startPos = 0; + } + debug.log("Finding EndOfCentralDirectoryRecord from %s with limit %s", startPos, buffer.limit()); + dataBlock.readFully(buffer, startPos); + int offset = findInBuffer(buffer); + if (offset >= 0) { + debug.log("Found EndOfCentralDirectoryRecord at %s + %s", startPos, offset); + return startPos + offset; + } + endPos = endPos - BUFFER_SIZE + MINIMUM_SIZE; + } + throw new IOException("Zip 'End Of Central Directory Record' not found after reading entire data block"); + } + + private static int findInBuffer(ByteBuffer buffer) { + for (int pos = buffer.limit() - 4; pos >= 0; pos--) { + buffer.position(pos); + if (buffer.getInt() == SIGNATURE) { + return pos; + } + } + return -1; + } + + /** + * A located {@link ZipEndOfCentralDirectoryRecord}. + * + * @param pos the position of the record + * @param endOfCentralDirectoryRecord the located end of central directory record + */ + record Located(long pos, ZipEndOfCentralDirectoryRecord endOfCentralDirectoryRecord) { + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipLocalFileHeaderRecord.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipLocalFileHeaderRecord.java new file mode 100644 index 000000000000..8d77ca585aec --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipLocalFileHeaderRecord.java @@ -0,0 +1,124 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import org.springframework.boot.loader.log.DebugLogger; + +/** + * A ZIP File "Local file header record" (LFH). + * + * @param versionNeededToExtract the version needed to extract the zip + * @param generalPurposeBitFlag the general purpose bit flag + * @param compressionMethod the compression method used for this entry + * @param lastModFileTime the last modified file time + * @param lastModFileDate the last modified file date + * @param crc32 the CRC32 checksum + * @param compressedSize the size of the entry when compressed + * @param uncompressedSize the size of the entry when uncompressed + * @param fileNameLength the file name length + * @param extraFieldLength the extra field length + * @author Phillip Webb + * @see Chapter + * 4.3.7 of the Zip File Format Specification + */ +record ZipLocalFileHeaderRecord(short versionNeededToExtract, short generalPurposeBitFlag, short compressionMethod, + short lastModFileTime, short lastModFileDate, int crc32, int compressedSize, int uncompressedSize, + short fileNameLength, short extraFieldLength) { + + private static final DebugLogger debug = DebugLogger.get(ZipLocalFileHeaderRecord.class); + + private static final int SIGNATURE = 0x04034b50; + + private static final int MINIMUM_SIZE = 30; + + /** + * Return the size of this record. + * @return the record size + */ + long size() { + return MINIMUM_SIZE + fileNameLength() + extraFieldLength(); + } + + /** + * Return a new {@link ZipLocalFileHeaderRecord} with a new + * {@link #extraFieldLength()}. + * @param extraFieldLength the new extra field length + * @return a new {@link ZipLocalFileHeaderRecord} instance + */ + ZipLocalFileHeaderRecord withExtraFieldLength(short extraFieldLength) { + return new ZipLocalFileHeaderRecord(this.versionNeededToExtract, this.generalPurposeBitFlag, + this.compressionMethod, this.lastModFileTime, this.lastModFileDate, this.crc32, this.compressedSize, + this.uncompressedSize, this.fileNameLength, extraFieldLength); + } + + /** + * Return a new {@link ZipLocalFileHeaderRecord} with a new {@link #fileNameLength()}. + * @param fileNameLength the new file name length + * @return a new {@link ZipLocalFileHeaderRecord} instance + */ + ZipLocalFileHeaderRecord withFileNameLength(short fileNameLength) { + return new ZipLocalFileHeaderRecord(this.versionNeededToExtract, this.generalPurposeBitFlag, + this.compressionMethod, this.lastModFileTime, this.lastModFileDate, this.crc32, this.compressedSize, + this.uncompressedSize, fileNameLength, this.extraFieldLength); + } + + /** + * Return the contents of this record as a byte array suitable for writing to a zip. + * @return the record as a byte array + */ + byte[] asByteArray() { + ByteBuffer buffer = ByteBuffer.allocate(MINIMUM_SIZE); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.putInt(SIGNATURE); + buffer.putShort(this.versionNeededToExtract); + buffer.putShort(this.generalPurposeBitFlag); + buffer.putShort(this.compressionMethod); + buffer.putShort(this.lastModFileTime); + buffer.putShort(this.lastModFileDate); + buffer.putInt(this.crc32); + buffer.putInt(this.compressedSize); + buffer.putInt(this.uncompressedSize); + buffer.putShort(this.fileNameLength); + buffer.putShort(this.extraFieldLength); + return buffer.array(); + } + + /** + * Load the {@link ZipLocalFileHeaderRecord} from the given data block. + * @param dataBlock the source data block + * @param pos the position of the record + * @return a new {@link ZipLocalFileHeaderRecord} instance + * @throws IOException on I/O error + */ + static ZipLocalFileHeaderRecord load(DataBlock dataBlock, long pos) throws IOException { + debug.log("Loading LocalFileHeaderRecord from position %s", pos); + ByteBuffer buffer = ByteBuffer.allocate(MINIMUM_SIZE); + buffer.order(ByteOrder.LITTLE_ENDIAN); + dataBlock.readFully(buffer, pos); + buffer.rewind(); + if (buffer.getInt() != SIGNATURE) { + throw new IOException("Zip 'Local File Header Record' not found at position " + pos); + } + return new ZipLocalFileHeaderRecord(buffer.getShort(), buffer.getShort(), buffer.getShort(), buffer.getShort(), + buffer.getShort(), buffer.getInt(), buffer.getInt(), buffer.getInt(), buffer.getShort(), + buffer.getShort()); + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipString.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipString.java new file mode 100644 index 000000000000..bf246e0c7d60 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipString.java @@ -0,0 +1,320 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.io.EOFException; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; + +import org.springframework.boot.loader.log.DebugLogger; + +/** + * Internal utility class for working with the string content of zip records. Provides + * methods that work with raw bytes to save creating temporary strings. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +final class ZipString { + + private static final DebugLogger debug = DebugLogger.get(ZipString.class); + + static final int BUFFER_SIZE = 256; + + private static final int[] INITIAL_BYTE_BITMASK = { 0x7F, 0x1F, 0x0F, 0x07 }; + + private static final int SUBSEQUENT_BYTE_BITMASK = 0x3F; + + private static final int EMPTY_HASH = "".hashCode(); + + private static final int EMPTY_SLASH_HASH = "/".hashCode(); + + private ZipString() { + } + + /** + * Return a hash for a char sequence, optionally appending '/'. + * @param charSequence the source char sequence + * @param addEndSlash if slash should be added to the string if it's not already + * present + * @return the hash + */ + static int hash(CharSequence charSequence, boolean addEndSlash) { + return hash(0, charSequence, addEndSlash); + } + + /** + * Return a hash for a char sequence, optionally appending '/'. + * @param initialHash the initial hash value + * @param charSequence the source char sequence + * @param addEndSlash if slash should be added to the string if it's not already + * present + * @return the hash + */ + static int hash(int initialHash, CharSequence charSequence, boolean addEndSlash) { + if (charSequence == null || charSequence.isEmpty()) { + return (!addEndSlash) ? EMPTY_HASH : EMPTY_SLASH_HASH; + } + boolean endsWithSlash = charSequence.charAt(charSequence.length() - 1) == '/'; + int hash = initialHash; + if (charSequence instanceof String && initialHash == 0) { + // We're compatible with String.hashCode and it might be already calculated + hash = charSequence.hashCode(); + } + else { + for (int i = 0; i < charSequence.length(); i++) { + char ch = charSequence.charAt(i); + hash = 31 * hash + ch; + } + } + hash = (addEndSlash && !endsWithSlash) ? 31 * hash + '/' : hash; + debug.log("%s calculated for charsequence '%s' (addEndSlash=%s)", hash, charSequence, endsWithSlash); + return hash; + } + + /** + * Return a hash for bytes read from a {@link DataBlock}, optionally appending '/'. + * @param buffer the buffer to use or {@code null} + * @param dataBlock the source data block + * @param pos the position in the data block where the string starts + * @param len the number of bytes to read from the block + * @param addEndSlash if slash should be added to the string if it's not already + * present + * @return the hash + * @throws IOException on I/O error + */ + static int hash(ByteBuffer buffer, DataBlock dataBlock, long pos, int len, boolean addEndSlash) throws IOException { + if (len == 0) { + return (!addEndSlash) ? EMPTY_HASH : EMPTY_SLASH_HASH; + } + buffer = (buffer != null) ? buffer : ByteBuffer.allocate(BUFFER_SIZE); + byte[] bytes = buffer.array(); + int hash = 0; + char lastChar = 0; + while (len > 0) { + int count = readInBuffer(dataBlock, pos, buffer, len); + len -= count; + pos += count; + for (int byteIndex = 0; byteIndex < count;) { + int codePointSize = getCodePointSize(bytes, byteIndex); + if (!hasEnoughBytes(byteIndex, codePointSize, count)) { + pos--; + len++; + break; + } + int codePoint = getCodePoint(bytes, byteIndex, codePointSize); + byteIndex += codePointSize; + if (codePoint <= 0xFFFF) { + lastChar = (char) (codePoint & 0xFFFF); + hash = 31 * hash + lastChar; + } + else { + lastChar = 0; + hash = 31 * hash + Character.highSurrogate(codePoint); + hash = 31 * hash + Character.lowSurrogate(codePoint); + } + } + } + hash = (addEndSlash && lastChar != '/') ? 31 * hash + '/' : hash; + debug.log("%08X calculated for datablock position %s size %s (addEndSlash=%s)", hash, pos, len, addEndSlash); + return hash; + } + + /** + * Return if the bytes read from a {@link DataBlock} matches the give + * {@link CharSequence}. + * @param buffer the buffer to use or {@code null} + * @param dataBlock the source data block + * @param pos the position in the data block where the string starts + * @param len the number of bytes to read from the block + * @param charSequence the char sequence with which to compare + * @param addSlash also accept {@code charSequence + '/'} when it doesn't already end + * with one + * @return true if the contents are considered equal + */ + static boolean matches(ByteBuffer buffer, DataBlock dataBlock, long pos, int len, CharSequence charSequence, + boolean addSlash) { + if (charSequence.isEmpty()) { + return true; + } + buffer = (buffer != null) ? buffer : ByteBuffer.allocate(BUFFER_SIZE); + try { + return compare(buffer, dataBlock, pos, len, charSequence, + (!addSlash) ? CompareType.MATCHES : CompareType.MATCHES_ADDING_SLASH) != -1; + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + /** + * Returns if the bytes read from a {@link DataBlock} starts with the given + * {@link CharSequence}. + * @param buffer the buffer to use or {@code null} + * @param dataBlock the source data block + * @param pos the position in the data block where the string starts + * @param len the number of bytes to read from the block + * @param charSequence the required starting chars + * @return {@code -1} if the data block does not start with the char sequence, or a + * positive number indicating the number of bytes that contain the starting chars + */ + static int startsWith(ByteBuffer buffer, DataBlock dataBlock, long pos, int len, CharSequence charSequence) { + if (charSequence.isEmpty()) { + return 0; + } + buffer = (buffer != null) ? buffer : ByteBuffer.allocate(BUFFER_SIZE); + try { + return compare(buffer, dataBlock, pos, len, charSequence, CompareType.STARTS_WITH); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private static int compare(ByteBuffer buffer, DataBlock dataBlock, long pos, int len, CharSequence charSequence, + CompareType compareType) throws IOException { + if (charSequence.isEmpty()) { + return 0; + } + boolean addSlash = compareType == CompareType.MATCHES_ADDING_SLASH && !endsWith(charSequence, '/'); + int charSequenceIndex = 0; + int maxCharSequenceLength = (!addSlash) ? charSequence.length() : charSequence.length() + 1; + int result = 0; + byte[] bytes = buffer.array(); + while (len > 0) { + int count = readInBuffer(dataBlock, pos, buffer, len); + len -= count; + pos += count; + for (int byteIndex = 0; byteIndex < count;) { + int codePointSize = getCodePointSize(bytes, byteIndex); + if (!hasEnoughBytes(byteIndex, codePointSize, count)) { + pos--; + len++; + break; + } + int codePoint = getCodePoint(bytes, byteIndex, codePointSize); + result += codePointSize; + if (codePoint <= 0xFFFF) { + char ch = (char) (codePoint & 0xFFFF); + if (charSequenceIndex >= maxCharSequenceLength + || getChar(charSequence, charSequenceIndex++) != ch) { + return -1; + } + } + else { + char ch = Character.highSurrogate(codePoint); + if (charSequenceIndex >= maxCharSequenceLength + || getChar(charSequence, charSequenceIndex++) != ch) { + return -1; + } + ch = Character.lowSurrogate(codePoint); + if (charSequenceIndex >= charSequence.length() + || getChar(charSequence, charSequenceIndex++) != ch) { + return -1; + } + } + if (compareType == CompareType.STARTS_WITH && charSequenceIndex >= charSequence.length()) { + return result; + } + byteIndex += codePointSize; + } + } + return (charSequenceIndex >= charSequence.length()) ? result : -1; + } + + private static boolean hasEnoughBytes(int byteIndex, int codePointSize, int count) { + return (byteIndex + codePointSize - 1) < count; + } + + private static boolean endsWith(CharSequence charSequence, char ch) { + return !charSequence.isEmpty() && charSequence.charAt(charSequence.length() - 1) == ch; + } + + private static char getChar(CharSequence charSequence, int index) { + return (index != charSequence.length()) ? charSequence.charAt(index) : '/'; + } + + /** + * Read a string value from the given data block. + * @param data the source data + * @param pos the position to read from + * @param len the number of bytes to read + * @return the contents as a string + */ + static String readString(DataBlock data, long pos, long len) { + try { + if (len > Integer.MAX_VALUE) { + throw new IllegalStateException("String is too long to read"); + } + ByteBuffer buffer = ByteBuffer.allocate((int) len); + buffer.order(ByteOrder.LITTLE_ENDIAN); + data.readFully(buffer, pos); + return new String(buffer.array(), StandardCharsets.UTF_8); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private static int readInBuffer(DataBlock dataBlock, long pos, ByteBuffer buffer, int maxLen) throws IOException { + buffer.clear(); + if (buffer.remaining() > maxLen) { + buffer.limit(maxLen); + } + int count = dataBlock.read(buffer, pos); + if (count <= 0) { + throw new EOFException(); + } + return count; + } + + private static int getCodePointSize(byte[] bytes, int i) { + int b = bytes[i] & 0xFF; + if ((b & 0b1_0000000) == 0b0_0000000) { + return 1; + } + if ((b & 0b111_00000) == 0b110_00000) { + return 2; + } + if ((b & 0b1111_0000) == 0b1110_0000) { + return 3; + } + return 4; + } + + private static int getCodePoint(byte[] bytes, int i, int codePointSize) { + int codePoint = bytes[i] & 0xFF; + codePoint &= INITIAL_BYTE_BITMASK[codePointSize - 1]; + for (int j = 1; j < codePointSize; j++) { + codePoint = (codePoint << 6) + (bytes[i + j] & SUBSEQUENT_BYTE_BITMASK); + } + return codePoint; + } + + /** + * Supported compare types. + */ + private enum CompareType { + + MATCHES, MATCHES_ADDING_SLASH, STARTS_WITH + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/package-info.java new file mode 100644 index 000000000000..38bd93390b2e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Provides low-level support for handling zip content, including support for nested and + * virtual zip files. + */ +package org.springframework.boot.loader.zip; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java deleted file mode 100644 index 58084bba8ab6..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/LaunchedURLClassLoaderTests.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2012-2023 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.loader; - -import java.io.File; -import java.io.InputStream; -import java.net.JarURLConnection; -import java.net.URL; -import java.net.URLConnection; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import org.springframework.boot.loader.jar.JarFile; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link LaunchedURLClassLoader}. - * - * @author Dave Syer - * @author Phillip Webb - * @author Andy Wilkinson - */ -@SuppressWarnings("resource") -class LaunchedURLClassLoaderTests { - - @TempDir - File tempDir; - - @Test - void resolveResourceFromArchive() throws Exception { - LaunchedURLClassLoader loader = new LaunchedURLClassLoader( - new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") }, getClass().getClassLoader()); - assertThat(loader.getResource("demo/Application.java")).isNotNull(); - } - - @Test - void resolveResourcesFromArchive() throws Exception { - LaunchedURLClassLoader loader = new LaunchedURLClassLoader( - new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") }, getClass().getClassLoader()); - assertThat(loader.getResources("demo/Application.java").hasMoreElements()).isTrue(); - } - - @Test - void resolveRootPathFromArchive() throws Exception { - LaunchedURLClassLoader loader = new LaunchedURLClassLoader( - new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") }, getClass().getClassLoader()); - assertThat(loader.getResource("")).isNotNull(); - } - - @Test - void resolveRootResourcesFromArchive() throws Exception { - LaunchedURLClassLoader loader = new LaunchedURLClassLoader( - new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") }, getClass().getClassLoader()); - assertThat(loader.getResources("").hasMoreElements()).isTrue(); - } - - @Test - void resolveFromNested() throws Exception { - File file = new File(this.tempDir, "test.jar"); - TestJarCreator.createTestJar(file); - try (JarFile jarFile = new JarFile(file)) { - URL url = jarFile.getUrl(); - try (LaunchedURLClassLoader loader = new LaunchedURLClassLoader(new URL[] { url }, null)) { - URL resource = loader.getResource("nested.jar!/3.dat"); - assertThat(resource).hasToString(url + "nested.jar!/3.dat"); - try (InputStream input = resource.openConnection().getInputStream()) { - assertThat(input.read()).isEqualTo(3); - } - } - } - } - - @Test - void resolveFromNestedWhileThreadIsInterrupted() throws Exception { - File file = new File(this.tempDir, "test.jar"); - TestJarCreator.createTestJar(file); - try (JarFile jarFile = new JarFile(file)) { - URL url = jarFile.getUrl(); - try (LaunchedURLClassLoader loader = new LaunchedURLClassLoader(new URL[] { url }, null)) { - Thread.currentThread().interrupt(); - URL resource = loader.getResource("nested.jar!/3.dat"); - assertThat(resource).hasToString(url + "nested.jar!/3.dat"); - URLConnection connection = resource.openConnection(); - try (InputStream input = connection.getInputStream()) { - assertThat(input.read()).isEqualTo(3); - } - ((JarURLConnection) connection).getJarFile().close(); - } - finally { - Thread.interrupted(); - } - } - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java deleted file mode 100755 index 77d2ce185c44..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/ExplodedArchiveTests.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.archive; - -import java.io.File; -import java.io.FileOutputStream; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import org.springframework.boot.loader.TestJarCreator; -import org.springframework.boot.loader.archive.Archive.Entry; -import org.springframework.util.FileCopyUtils; -import org.springframework.util.StringUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link ExplodedArchive}. - * - * @author Phillip Webb - * @author Dave Syer - * @author Andy Wilkinson - */ -class ExplodedArchiveTests { - - @TempDir - File tempDir; - - private File rootDirectory; - - private ExplodedArchive archive; - - @BeforeEach - void setup() throws Exception { - createArchive(); - } - - @AfterEach - void tearDown() throws Exception { - if (this.archive != null) { - this.archive.close(); - } - } - - private void createArchive() throws Exception { - createArchive(null); - } - - private void createArchive(String directoryName) throws Exception { - File file = new File(this.tempDir, "test.jar"); - TestJarCreator.createTestJar(file); - this.rootDirectory = (StringUtils.hasText(directoryName) ? new File(this.tempDir, directoryName) - : new File(this.tempDir, UUID.randomUUID().toString())); - JarFile jarFile = new JarFile(file); - Enumeration entries = jarFile.entries(); - while (entries.hasMoreElements()) { - JarEntry entry = entries.nextElement(); - File destination = new File(this.rootDirectory.getAbsolutePath() + File.separator + entry.getName()); - destination.getParentFile().mkdirs(); - if (entry.isDirectory()) { - destination.mkdir(); - } - else { - FileCopyUtils.copy(jarFile.getInputStream(entry), new FileOutputStream(destination)); - } - } - this.archive = new ExplodedArchive(this.rootDirectory); - jarFile.close(); - } - - @Test - void getManifest() throws Exception { - assertThat(this.archive.getManifest().getMainAttributes().getValue("Built-By")).isEqualTo("j1"); - } - - @Test - void getEntries() { - Map entries = getEntriesMap(this.archive); - assertThat(entries).hasSize(12); - } - - @Test - void getUrl() throws Exception { - assertThat(this.archive.getUrl()).isEqualTo(this.rootDirectory.toURI().toURL()); - } - - @Test - void getUrlWithSpaceInPath() throws Exception { - createArchive("spaces in the name"); - assertThat(this.archive.getUrl()).isEqualTo(this.rootDirectory.toURI().toURL()); - } - - @Test - void getNestedArchive() throws Exception { - Entry entry = getEntriesMap(this.archive).get("nested.jar"); - Archive nested = this.archive.getNestedArchive(entry); - assertThat(nested.getUrl()).hasToString(this.rootDirectory.toURI() + "nested.jar"); - nested.close(); - } - - @Test - void nestedDirArchive() throws Exception { - Entry entry = getEntriesMap(this.archive).get("d/"); - Archive nested = this.archive.getNestedArchive(entry); - Map nestedEntries = getEntriesMap(nested); - assertThat(nestedEntries).hasSize(1); - assertThat(nested.getUrl()).hasToString("file:" + this.rootDirectory.toURI().getPath() + "d/"); - } - - @Test - void getNonRecursiveEntriesForRoot() throws Exception { - try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("/"), false)) { - Map entries = getEntriesMap(explodedArchive); - assertThat(entries).hasSizeGreaterThan(1); - } - } - - @Test - void getNonRecursiveManifest() throws Exception { - try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("src/test/resources/root"))) { - assertThat(explodedArchive.getManifest()).isNotNull(); - Map entries = getEntriesMap(explodedArchive); - assertThat(entries).hasSize(4); - } - } - - @Test - void getNonRecursiveManifestEvenIfNonRecursive() throws Exception { - try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("src/test/resources/root"), false)) { - assertThat(explodedArchive.getManifest()).isNotNull(); - Map entries = getEntriesMap(explodedArchive); - assertThat(entries).hasSize(3); - } - } - - @Test - void getResourceAsStream() throws Exception { - try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("src/test/resources/root"))) { - assertThat(explodedArchive.getManifest()).isNotNull(); - URLClassLoader loader = new URLClassLoader(new URL[] { explodedArchive.getUrl() }); - assertThat(loader.getResourceAsStream("META-INF/spring/application.xml")).isNotNull(); - loader.close(); - } - } - - @Test - void getResourceAsStreamNonRecursive() throws Exception { - try (ExplodedArchive explodedArchive = new ExplodedArchive(new File("src/test/resources/root"), false)) { - assertThat(explodedArchive.getManifest()).isNotNull(); - URLClassLoader loader = new URLClassLoader(new URL[] { explodedArchive.getUrl() }); - assertThat(loader.getResourceAsStream("META-INF/spring/application.xml")).isNotNull(); - loader.close(); - } - } - - private Map getEntriesMap(Archive archive) { - Map entries = new HashMap<>(); - for (Archive.Entry entry : archive) { - entries.put(entry.getName(), entry); - } - return entries; - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java deleted file mode 100755 index 4b2ce93af634..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/archive/JarFileArchiveTests.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.archive; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.net.URL; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.jar.JarEntry; -import java.util.jar.JarOutputStream; -import java.util.zip.CRC32; -import java.util.zip.ZipEntry; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import org.springframework.boot.loader.TestJarCreator; -import org.springframework.boot.loader.archive.Archive.Entry; -import org.springframework.boot.loader.jar.JarFile; -import org.springframework.util.FileCopyUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link JarFileArchive}. - * - * @author Phillip Webb - * @author Andy Wilkinson - * @author Camille Vienot - */ -class JarFileArchiveTests { - - @TempDir - File tempDir; - - private File rootJarFile; - - private JarFileArchive archive; - - private String rootJarFileUrl; - - @BeforeEach - void setup() throws Exception { - setup(false); - } - - @AfterEach - void tearDown() throws Exception { - this.archive.close(); - } - - private void setup(boolean unpackNested) throws Exception { - this.rootJarFile = new File(this.tempDir, "root.jar"); - this.rootJarFileUrl = this.rootJarFile.toURI().toString(); - TestJarCreator.createTestJar(this.rootJarFile, unpackNested); - if (this.archive != null) { - this.archive.close(); - } - this.archive = new JarFileArchive(this.rootJarFile); - } - - @Test - void getManifest() throws Exception { - assertThat(this.archive.getManifest().getMainAttributes().getValue("Built-By")).isEqualTo("j1"); - } - - @Test - void getEntries() { - Map entries = getEntriesMap(this.archive); - assertThat(entries).hasSize(12); - } - - @Test - void getUrl() throws Exception { - URL url = this.archive.getUrl(); - assertThat(url).hasToString(this.rootJarFileUrl); - } - - @Test - void getNestedArchive() throws Exception { - Entry entry = getEntriesMap(this.archive).get("nested.jar"); - try (Archive nested = this.archive.getNestedArchive(entry)) { - assertThat(nested.getUrl()).hasToString("jar:" + this.rootJarFileUrl + "!/nested.jar!/"); - } - } - - @Test - void getNestedUnpackedArchive() throws Exception { - setup(true); - Entry entry = getEntriesMap(this.archive).get("nested.jar"); - try (Archive nested = this.archive.getNestedArchive(entry)) { - assertThat(nested.getUrl().toString()).startsWith("file:"); - assertThat(nested.getUrl().toString()).endsWith("/nested.jar"); - } - } - - @Test - void unpackedLocationsAreUniquePerArchive() throws Exception { - setup(true); - Entry entry = getEntriesMap(this.archive).get("nested.jar"); - URL firstNestedUrl; - try (Archive firstNested = this.archive.getNestedArchive(entry)) { - firstNestedUrl = firstNested.getUrl(); - } - this.archive.close(); - setup(true); - entry = getEntriesMap(this.archive).get("nested.jar"); - try (Archive secondNested = this.archive.getNestedArchive(entry)) { - URL secondNestedUrl = secondNested.getUrl(); - assertThat(secondNestedUrl).isNotEqualTo(firstNestedUrl); - } - } - - @Test - void unpackedLocationsFromSameArchiveShareSameParent() throws Exception { - setup(true); - try (Archive nestedArchive = this.archive.getNestedArchive(getEntriesMap(this.archive).get("nested.jar")); - Archive anotherNestedArchive = this.archive - .getNestedArchive(getEntriesMap(this.archive).get("another-nested.jar"))) { - File nested = new File(nestedArchive.getUrl().toURI()); - File anotherNested = new File(anotherNestedArchive.getUrl().toURI()); - assertThat(nested).hasParent(anotherNested.getParent()); - } - } - - @Test - void filesInZip64ArchivesAreAllListed() throws IOException { - File file = new File(this.tempDir, "test.jar"); - FileCopyUtils.copy(writeZip64Jar(), file); - try (JarFileArchive zip64Archive = new JarFileArchive(file)) { - @SuppressWarnings("deprecation") - Iterator entries = zip64Archive.iterator(); - for (int i = 0; i < 65537; i++) { - assertThat(entries.hasNext()).as(i + "nth file is present").isTrue(); - entries.next(); - } - } - } - - @Test - void nestedZip64ArchivesAreHandledGracefully() throws Exception { - File file = new File(this.tempDir, "test.jar"); - try (JarOutputStream output = new JarOutputStream(new FileOutputStream(file))) { - JarEntry zip64JarEntry = new JarEntry("nested/zip64.jar"); - output.putNextEntry(zip64JarEntry); - byte[] zip64JarData = writeZip64Jar(); - zip64JarEntry.setSize(zip64JarData.length); - zip64JarEntry.setCompressedSize(zip64JarData.length); - zip64JarEntry.setMethod(ZipEntry.STORED); - CRC32 crc32 = new CRC32(); - crc32.update(zip64JarData); - zip64JarEntry.setCrc(crc32.getValue()); - output.write(zip64JarData); - output.closeEntry(); - } - try (JarFile jarFile = new JarFile(file)) { - ZipEntry nestedEntry = jarFile.getEntry("nested/zip64.jar"); - try (JarFile nestedJarFile = jarFile.getNestedJarFile(nestedEntry)) { - Iterator iterator = nestedJarFile.iterator(); - for (int i = 0; i < 65537; i++) { - assertThat(iterator.hasNext()).as(i + "nth file is present").isTrue(); - iterator.next(); - } - } - } - } - - private byte[] writeZip64Jar() throws IOException { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - try (JarOutputStream jarOutput = new JarOutputStream(bytes)) { - for (int i = 0; i < 65537; i++) { - jarOutput.putNextEntry(new JarEntry(i + ".dat")); - jarOutput.closeEntry(); - } - } - return bytes.toByteArray(); - } - - private Map getEntriesMap(Archive archive) { - Map entries = new HashMap<>(); - for (Archive.Entry entry : archive) { - entries.put(entry.getName(), entry); - } - return entries; - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java deleted file mode 100644 index 6713814def75..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/data/RandomAccessDataFileTests.java +++ /dev/null @@ -1,300 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.data; - -import java.io.EOFException; -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; - -/** - * Tests for {@link RandomAccessDataFile}. - * - * @author Phillip Webb - * @author Andy Wilkinson - */ -class RandomAccessDataFileTests { - - private static final byte[] BYTES; - - static { - BYTES = new byte[256]; - for (int i = 0; i < BYTES.length; i++) { - BYTES[i] = (byte) i; - } - } - - private File tempFile; - - private RandomAccessDataFile file; - - private InputStream inputStream; - - @BeforeEach - void setup(@TempDir File tempDir) throws Exception { - this.tempFile = new File(tempDir, "tempFile"); - FileOutputStream outputStream = new FileOutputStream(this.tempFile); - outputStream.write(BYTES); - outputStream.close(); - this.file = new RandomAccessDataFile(this.tempFile); - this.inputStream = this.file.getInputStream(); - } - - @AfterEach - void cleanup() throws Exception { - this.inputStream.close(); - this.file.close(); - } - - @Test - void fileNotNull() { - assertThatIllegalArgumentException().isThrownBy(() -> new RandomAccessDataFile(null)) - .withMessageContaining("File must not be null"); - } - - @Test - void fileExists() { - File file = new File("/does/not/exist"); - assertThatIllegalArgumentException().isThrownBy(() -> new RandomAccessDataFile(file)) - .withMessageContaining(String.format("File %s must exist", file.getAbsolutePath())); - } - - @Test - void readWithOffsetAndLengthShouldRead() throws Exception { - byte[] read = this.file.read(2, 3); - assertThat(read).isEqualTo(new byte[] { 2, 3, 4 }); - } - - @Test - void readWhenOffsetIsBeyondEOFShouldThrowException() { - assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.read(257, 0)); - } - - @Test - void readWhenOffsetIsBeyondEndOfSubsectionShouldThrowException() { - RandomAccessData subsection = this.file.getSubsection(0, 10); - assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> subsection.read(11, 0)); - } - - @Test - void readWhenOffsetPlusLengthGreaterThanEOFShouldThrowException() { - assertThatExceptionOfType(EOFException.class).isThrownBy(() -> this.file.read(256, 1)); - } - - @Test - void readWhenOffsetPlusLengthGreaterThanEndOfSubsectionShouldThrowException() { - RandomAccessData subsection = this.file.getSubsection(0, 10); - assertThatExceptionOfType(EOFException.class).isThrownBy(() -> subsection.read(10, 1)); - } - - @Test - void inputStreamRead() throws Exception { - for (int i = 0; i <= 255; i++) { - assertThat(this.inputStream.read()).isEqualTo(i); - } - } - - @Test - void inputStreamReadNullBytes() { - assertThatNullPointerException().isThrownBy(() -> this.inputStream.read(null)) - .withMessage("Bytes must not be null"); - } - - @Test - void inputStreamReadNullBytesWithOffset() { - assertThatNullPointerException().isThrownBy(() -> this.inputStream.read(null, 0, 1)) - .withMessage("Bytes must not be null"); - } - - @Test - void inputStreamReadBytes() throws Exception { - byte[] b = new byte[256]; - int amountRead = this.inputStream.read(b); - assertThat(b).isEqualTo(BYTES); - assertThat(amountRead).isEqualTo(256); - } - - @Test - void inputStreamReadOffsetBytes() throws Exception { - byte[] b = new byte[7]; - this.inputStream.skip(1); - int amountRead = this.inputStream.read(b, 2, 3); - assertThat(b).isEqualTo(new byte[] { 0, 0, 1, 2, 3, 0, 0 }); - assertThat(amountRead).isEqualTo(3); - } - - @Test - void inputStreamReadMoreBytesThanAvailable() throws Exception { - byte[] b = new byte[257]; - int amountRead = this.inputStream.read(b); - assertThat(b).startsWith(BYTES); - assertThat(amountRead).isEqualTo(256); - } - - @Test - void inputStreamReadPastEnd() throws Exception { - this.inputStream.skip(255); - assertThat(this.inputStream.read()).isEqualTo(0xFF); - assertThat(this.inputStream.read()).isEqualTo(-1); - assertThat(this.inputStream.read()).isEqualTo(-1); - } - - @Test - void inputStreamReadZeroLength() throws Exception { - byte[] b = new byte[] { 0x0F }; - int amountRead = this.inputStream.read(b, 0, 0); - assertThat(b).isEqualTo(new byte[] { 0x0F }); - assertThat(amountRead).isZero(); - assertThat(this.inputStream.read()).isZero(); - } - - @Test - void inputStreamSkip() throws Exception { - long amountSkipped = this.inputStream.skip(4); - assertThat(this.inputStream.read()).isEqualTo(4); - assertThat(amountSkipped).isEqualTo(4L); - } - - @Test - void inputStreamSkipMoreThanAvailable() throws Exception { - long amountSkipped = this.inputStream.skip(257); - assertThat(this.inputStream.read()).isEqualTo(-1); - assertThat(amountSkipped).isEqualTo(256L); - } - - @Test - void inputStreamSkipPastEnd() throws Exception { - this.inputStream.skip(256); - long amountSkipped = this.inputStream.skip(1); - assertThat(amountSkipped).isZero(); - } - - @Test - void inputStreamAvailable() throws Exception { - assertThat(this.inputStream.available()).isEqualTo(256); - this.inputStream.skip(56); - assertThat(this.inputStream.available()).isEqualTo(200); - this.inputStream.skip(200); - assertThat(this.inputStream.available()).isZero(); - } - - @Test - void subsectionNegativeOffset() { - assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.getSubsection(-1, 1)); - } - - @Test - void subsectionNegativeLength() { - assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.getSubsection(0, -1)); - } - - @Test - void subsectionZeroLength() throws Exception { - RandomAccessData subsection = this.file.getSubsection(0, 0); - assertThat(subsection.getInputStream().read()).isEqualTo(-1); - } - - @Test - void subsectionTooBig() { - this.file.getSubsection(0, 256); - assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.getSubsection(0, 257)); - } - - @Test - void subsectionTooBigWithOffset() { - this.file.getSubsection(1, 255); - assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> this.file.getSubsection(1, 256)); - } - - @Test - void subsection() throws Exception { - RandomAccessData subsection = this.file.getSubsection(1, 1); - assertThat(subsection.getInputStream().read()).isOne(); - } - - @Test - void inputStreamReadPastSubsection() throws Exception { - RandomAccessData subsection = this.file.getSubsection(1, 2); - InputStream inputStream = subsection.getInputStream(); - assertThat(inputStream.read()).isOne(); - assertThat(inputStream.read()).isEqualTo(2); - assertThat(inputStream.read()).isEqualTo(-1); - } - - @Test - void inputStreamReadBytesPastSubsection() throws Exception { - RandomAccessData subsection = this.file.getSubsection(1, 2); - InputStream inputStream = subsection.getInputStream(); - byte[] b = new byte[3]; - int amountRead = inputStream.read(b); - assertThat(b).isEqualTo(new byte[] { 1, 2, 0 }); - assertThat(amountRead).isEqualTo(2); - } - - @Test - void inputStreamSkipPastSubsection() throws Exception { - RandomAccessData subsection = this.file.getSubsection(1, 2); - InputStream inputStream = subsection.getInputStream(); - assertThat(inputStream.skip(3)).isEqualTo(2L); - assertThat(inputStream.read()).isEqualTo(-1); - } - - @Test - void inputStreamSkipNegative() throws Exception { - assertThat(this.inputStream.skip(-1)).isZero(); - } - - @Test - void getFile() { - assertThat(this.file.getFile()).isEqualTo(this.tempFile); - } - - @Test - void concurrentReads() throws Exception { - ExecutorService executorService = Executors.newFixedThreadPool(20); - List> results = new ArrayList<>(); - for (int i = 0; i < 100; i++) { - results.add(executorService.submit(() -> { - InputStream subsectionInputStream = RandomAccessDataFileTests.this.file.getSubsection(0, 256) - .getInputStream(); - byte[] b = new byte[256]; - subsectionInputStream.read(b); - return Arrays.equals(b, BYTES); - })); - } - for (Future future : results) { - assertThat(future.get()).isTrue(); - } - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java deleted file mode 100644 index dd2505016386..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/AsciiBytesTests.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.jar; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -/** - * Tests for {@link AsciiBytes}. - * - * @author Phillip Webb - * @author Andy Wilkinson - */ -class AsciiBytesTests { - - private static final char NO_SUFFIX = 0; - - @Test - void createFromBytes() { - AsciiBytes bytes = new AsciiBytes(new byte[] { 65, 66 }); - assertThat(bytes).hasToString("AB"); - } - - @Test - void createFromBytesWithOffset() { - AsciiBytes bytes = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2); - assertThat(bytes).hasToString("BC"); - } - - @Test - void createFromString() { - AsciiBytes bytes = new AsciiBytes("AB"); - assertThat(bytes).hasToString("AB"); - } - - @Test - void length() { - AsciiBytes b1 = new AsciiBytes(new byte[] { 65, 66 }); - AsciiBytes b2 = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2); - assertThat(b1.length()).isEqualTo(2); - assertThat(b2.length()).isEqualTo(2); - } - - @Test - void startWith() { - AsciiBytes abc = new AsciiBytes(new byte[] { 65, 66, 67 }); - AsciiBytes ab = new AsciiBytes(new byte[] { 65, 66 }); - AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67 }, 1, 2); - AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 }); - assertThat(abc.startsWith(abc)).isTrue(); - assertThat(abc.startsWith(ab)).isTrue(); - assertThat(abc.startsWith(bc)).isFalse(); - assertThat(abc.startsWith(abcd)).isFalse(); - } - - @Test - void endsWith() { - AsciiBytes abc = new AsciiBytes(new byte[] { 65, 66, 67 }); - AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67 }, 1, 2); - AsciiBytes ab = new AsciiBytes(new byte[] { 65, 66 }); - AsciiBytes aabc = new AsciiBytes(new byte[] { 65, 65, 66, 67 }); - assertThat(abc.endsWith(abc)).isTrue(); - assertThat(abc.endsWith(bc)).isTrue(); - assertThat(abc.endsWith(ab)).isFalse(); - assertThat(abc.endsWith(aabc)).isFalse(); - } - - @Test - void substringFromBeingIndex() { - AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 }); - assertThat(abcd.substring(0)).hasToString("ABCD"); - assertThat(abcd.substring(1)).hasToString("BCD"); - assertThat(abcd.substring(2)).hasToString("CD"); - assertThat(abcd.substring(3)).hasToString("D"); - assertThat(abcd.substring(4).toString()).isEmpty(); - assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> abcd.substring(5)); - } - - @Test - void substring() { - AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 }); - assertThat(abcd.substring(0, 4)).hasToString("ABCD"); - assertThat(abcd.substring(1, 3)).hasToString("BC"); - assertThat(abcd.substring(3, 4)).hasToString("D"); - assertThat(abcd.substring(3, 3).toString()).isEmpty(); - assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> abcd.substring(3, 5)); - } - - @Test - void hashCodeAndEquals() { - AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 }); - AsciiBytes bc = new AsciiBytes(new byte[] { 66, 67 }); - AsciiBytes bc_substring = new AsciiBytes(new byte[] { 65, 66, 67, 68 }).substring(1, 3); - AsciiBytes bc_string = new AsciiBytes("BC"); - assertThat(bc).hasSameHashCodeAs(bc); - assertThat(bc).hasSameHashCodeAs(bc_substring); - assertThat(bc).hasSameHashCodeAs(bc_string); - assertThat(bc).isEqualTo(bc); - assertThat(bc).isEqualTo(bc_substring); - assertThat(bc).isEqualTo(bc_string); - assertThat(bc.hashCode()).isNotEqualTo(abcd.hashCode()); - assertThat(bc).isNotEqualTo(abcd); - } - - @Test - void hashCodeSameAsString() { - hashCodeSameAsString("abcABC123xyz!"); - } - - @Test - void hashCodeSameAsStringWithSpecial() { - hashCodeSameAsString("special/\u00EB.dat"); - } - - @Test - void hashCodeSameAsStringWithCyrillicCharacters() { - hashCodeSameAsString("\u0432\u0435\u0441\u043D\u0430"); - } - - @Test - void hashCodeSameAsStringWithEmoji() { - hashCodeSameAsString("\ud83d\udca9"); - } - - private void hashCodeSameAsString(String input) { - assertThat(new AsciiBytes(input)).hasSameHashCodeAs(input); - } - - @Test - void matchesSameAsString() { - matchesSameAsString("abcABC123xyz!"); - } - - @Test - void matchesSameAsStringWithSpecial() { - matchesSameAsString("special/\u00EB.dat"); - } - - @Test - void matchesSameAsStringWithCyrillicCharacters() { - matchesSameAsString("\u0432\u0435\u0441\u043D\u0430"); - } - - @Test - void matchesDifferentLengths() { - assertThat(new AsciiBytes("abc").matches("ab", NO_SUFFIX)).isFalse(); - assertThat(new AsciiBytes("abc").matches("abcd", NO_SUFFIX)).isFalse(); - assertThat(new AsciiBytes("abc").matches("abc", NO_SUFFIX)).isTrue(); - assertThat(new AsciiBytes("abc").matches("a", 'b')).isFalse(); - assertThat(new AsciiBytes("abc").matches("abc", 'd')).isFalse(); - assertThat(new AsciiBytes("abc").matches("ab", 'c')).isTrue(); - } - - @Test - void matchesSuffix() { - assertThat(new AsciiBytes("ab").matches("a", 'b')).isTrue(); - } - - @Test - void matchesSameAsStringWithEmoji() { - matchesSameAsString("\ud83d\udca9"); - } - - @Test - void hashCodeFromInstanceMatchesHashCodeFromString() { - String name = "fonts/宋体/simsun.ttf"; - assertThat(new AsciiBytes(name).hashCode()).isEqualTo(AsciiBytes.hashCode(name)); - } - - @Test - void instanceCreatedFromCharSequenceMatchesSameCharSequence() { - String name = "fonts/宋体/simsun.ttf"; - assertThat(new AsciiBytes(name).matches(name, NO_SUFFIX)).isTrue(); - } - - private void matchesSameAsString(String input) { - assertThat(new AsciiBytes(input).matches(input, NO_SUFFIX)).isTrue(); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java deleted file mode 100644 index 4d15c21fe30e..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/CentralDirectoryParserTests.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.jar; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import org.springframework.boot.loader.TestJarCreator; -import org.springframework.boot.loader.data.RandomAccessData; -import org.springframework.boot.loader.data.RandomAccessDataFile; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link CentralDirectoryParser}. - * - * @author Phillip Webb - */ -class CentralDirectoryParserTests { - - private File jarFile; - - private RandomAccessDataFile jarData; - - @BeforeEach - void setup(@TempDir File tempDir) throws Exception { - this.jarFile = new File(tempDir, "test.jar"); - TestJarCreator.createTestJar(this.jarFile); - this.jarData = new RandomAccessDataFile(this.jarFile); - } - - @AfterEach - void tearDown() throws IOException { - this.jarData.close(); - } - - @Test - void visitsInOrder() throws Exception { - MockCentralDirectoryVisitor visitor = new MockCentralDirectoryVisitor(); - CentralDirectoryParser parser = new CentralDirectoryParser(); - parser.addVisitor(visitor); - parser.parse(this.jarData, false); - List invocations = visitor.getInvocations(); - assertThat(invocations).startsWith("visitStart").endsWith("visitEnd").contains("visitFileHeader"); - } - - @Test - void visitRecords() throws Exception { - Collector collector = new Collector(); - CentralDirectoryParser parser = new CentralDirectoryParser(); - parser.addVisitor(collector); - parser.parse(this.jarData, false); - Iterator headers = collector.getHeaders().iterator(); - assertThat(headers.next().getName()).hasToString("META-INF/"); - assertThat(headers.next().getName()).hasToString("META-INF/MANIFEST.MF"); - assertThat(headers.next().getName()).hasToString("1.dat"); - assertThat(headers.next().getName()).hasToString("2.dat"); - assertThat(headers.next().getName()).hasToString("d/"); - assertThat(headers.next().getName()).hasToString("d/9.dat"); - assertThat(headers.next().getName()).hasToString("special/"); - assertThat(headers.next().getName()).hasToString("special/\u00EB.dat"); - assertThat(headers.next().getName()).hasToString("nested.jar"); - assertThat(headers.next().getName()).hasToString("another-nested.jar"); - assertThat(headers.next().getName()).hasToString("space nested.jar"); - assertThat(headers.next().getName()).hasToString("multi-release.jar"); - assertThat(headers.hasNext()).isFalse(); - } - - static class Collector implements CentralDirectoryVisitor { - - private final List headers = new ArrayList<>(); - - @Override - public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) { - } - - @Override - public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) { - this.headers.add(fileHeader.clone()); - } - - @Override - public void visitEnd() { - } - - List getHeaders() { - return this.headers; - } - - } - - static class MockCentralDirectoryVisitor implements CentralDirectoryVisitor { - - private final List invocations = new ArrayList<>(); - - @Override - public void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData) { - this.invocations.add("visitStart"); - } - - @Override - public void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset) { - this.invocations.add("visitFileHeader"); - } - - @Override - public void visitEnd() { - this.invocations.add("visitEnd"); - } - - List getInvocations() { - return this.invocations; - } - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java deleted file mode 100644 index 1a64de64312c..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/HandlerTests.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.jar; - -import java.io.File; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLConnection; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.io.TempDir; - -import org.springframework.boot.loader.TestJarCreator; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link Handler}. - * - * @author Andy Wilkinson - */ -@ExtendWith(JarUrlProtocolHandler.class) -class HandlerTests { - - private final Handler handler = new Handler(); - - @Test - void parseUrlWithJarRootContextAndAbsoluteSpecThatUsesContext() throws MalformedURLException { - String spec = "/entry.txt"; - URL context = createUrl("file:example.jar!/"); - this.handler.parseURL(context, spec, 0, spec.length()); - assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt"); - } - - @Test - void parseUrlWithDirectoryEntryContextAndAbsoluteSpecThatUsesContext() throws MalformedURLException { - String spec = "/entry.txt"; - URL context = createUrl("file:example.jar!/dir/"); - this.handler.parseURL(context, spec, 0, spec.length()); - assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt"); - } - - @Test - void parseUrlWithJarRootContextAndRelativeSpecThatUsesContext() throws MalformedURLException { - String spec = "entry.txt"; - URL context = createUrl("file:example.jar!/"); - this.handler.parseURL(context, spec, 0, spec.length()); - assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt"); - } - - @Test - void parseUrlWithDirectoryEntryContextAndRelativeSpecThatUsesContext() throws MalformedURLException { - String spec = "entry.txt"; - URL context = createUrl("file:example.jar!/dir/"); - this.handler.parseURL(context, spec, 0, spec.length()); - assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/dir/entry.txt"); - } - - @Test - void parseUrlWithFileEntryContextAndRelativeSpecThatUsesContext() throws MalformedURLException { - String spec = "entry.txt"; - URL context = createUrl("file:example.jar!/dir/file"); - this.handler.parseURL(context, spec, 0, spec.length()); - assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/dir/entry.txt"); - } - - @Test - void parseUrlWithSpecThatIgnoresContext() throws MalformedURLException { - JarFile.registerUrlProtocolHandler(); - String spec = "jar:file:/other.jar!/nested!/entry.txt"; - URL context = createUrl("file:example.jar!/dir/file"); - this.handler.parseURL(context, spec, 0, spec.length()); - assertThat(context.toExternalForm()).isEqualTo("jar:jar:file:/other.jar!/nested!/entry.txt"); - } - - @Test - void sameFileReturnsFalseForUrlsWithDifferentProtocols() throws MalformedURLException { - assertThat(this.handler.sameFile(new URL("jar:file:foo.jar!/content.txt"), new URL("file:/foo.jar"))).isFalse(); - } - - @Test - void sameFileReturnsFalseForDifferentFileInSameJar() throws MalformedURLException { - assertThat(this.handler.sameFile(new URL("jar:file:foo.jar!/the/path/to/the/first/content.txt"), - new URL("jar:file:/foo.jar!/content.txt"))) - .isFalse(); - } - - @Test - void sameFileReturnsFalseForSameFileInDifferentJars() throws MalformedURLException { - assertThat(this.handler.sameFile(new URL("jar:file:/the/path/to/the/first.jar!/content.txt"), - new URL("jar:file:/second.jar!/content.txt"))) - .isFalse(); - } - - @Test - void sameFileReturnsTrueForSameFileInSameJar() throws MalformedURLException { - assertThat(this.handler.sameFile(new URL("jar:file:/the/path/to/the/first.jar!/content.txt"), - new URL("jar:file:/the/path/to/the/first.jar!/content.txt"))) - .isTrue(); - } - - @Test - void sameFileReturnsTrueForUrlsThatReferenceSameFileViaNestedArchiveAndFromRootOfJar() - throws MalformedURLException { - assertThat(this.handler.sameFile(new URL("jar:file:/test.jar!/BOOT-INF/classes!/foo.txt"), - new URL("jar:file:/test.jar!/BOOT-INF/classes/foo.txt"))) - .isTrue(); - } - - @Test - void hashCodesAreEqualForUrlsThatReferenceSameFileViaNestedArchiveAndFromRootOfJar() throws MalformedURLException { - assertThat(this.handler.hashCode(new URL("jar:file:/test.jar!/BOOT-INF/classes!/foo.txt"))) - .isEqualTo(this.handler.hashCode(new URL("jar:file:/test.jar!/BOOT-INF/classes/foo.txt"))); - } - - @Test - void urlWithSpecReferencingParentDirectory() throws MalformedURLException { - assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes!/xsd/directoryA/a.xsd", - "../directoryB/c/d/e.xsd"); - } - - @Test - void urlWithSpecReferencingAncestorDirectoryOutsideJarStopsAtJarRoot() throws MalformedURLException { - assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes!/xsd/directoryA/a.xsd", - "../../../../../../directoryB/b.xsd"); - } - - @Test - void urlWithSpecReferencingCurrentDirectory() throws MalformedURLException { - assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes!/xsd/directoryA/a.xsd", - "./directoryB/c/d/e.xsd"); - } - - @Test - void urlWithRef() throws MalformedURLException { - assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes", "!/foo.txt#alpha"); - } - - @Test - void urlWithQuery() throws MalformedURLException { - assertStandardAndCustomHandlerUrlsAreEqual("file:/test.jar!/BOOT-INF/classes", "!/foo.txt?alpha"); - } - - @Test - void fallbackToJdksJarUrlStreamHandler(@TempDir File tempDir) throws Exception { - File testJar = new File(tempDir, "test.jar"); - TestJarCreator.createTestJar(testJar); - URLConnection connection = new URL(null, "jar:" + testJar.toURI().toURL() + "!/nested.jar!/", this.handler) - .openConnection(); - assertThat(connection).isInstanceOf(JarURLConnection.class); - ((JarURLConnection) connection).getJarFile().close(); - URLConnection jdkConnection = new URL(null, "jar:file:" + testJar.toURI().toURL() + "!/nested.jar!/", - this.handler) - .openConnection(); - assertThat(jdkConnection).isNotInstanceOf(JarURLConnection.class); - assertThat(jdkConnection.getClass().getName()).endsWith(".JarURLConnection"); - } - - @Test - void whenJarHasAPlusInItsPathConnectionJarFileMatchesOriginalJarFile(@TempDir File tempDir) throws Exception { - File testJar = new File(tempDir, "t+e+s+t.jar"); - TestJarCreator.createTestJar(testJar); - URL url = new URL(null, "jar:" + testJar.toURI().toURL() + "!/nested.jar!/3.dat", this.handler); - JarURLConnection connection = (JarURLConnection) url.openConnection(); - try (JarFile jarFile = JarFileWrapper.unwrap(connection.getJarFile())) { - assertThat(jarFile.getRootJarFile().getFile()).isEqualTo(testJar); - } - } - - @Test - void whenJarHasASpaceInItsPathConnectionJarFileMatchesOriginalJarFile(@TempDir File tempDir) throws Exception { - File testJar = new File(tempDir, "t e s t.jar"); - TestJarCreator.createTestJar(testJar); - URL url = new URL(null, "jar:" + testJar.toURI().toURL() + "!/nested.jar!/3.dat", this.handler); - JarURLConnection connection = (JarURLConnection) url.openConnection(); - try (JarFile jarFile = JarFileWrapper.unwrap(connection.getJarFile())) { - assertThat(jarFile.getRootJarFile().getFile()).isEqualTo(testJar); - } - } - - private void assertStandardAndCustomHandlerUrlsAreEqual(String context, String spec) throws MalformedURLException { - URL standardUrl = new URL(new URL("jar:" + context), spec); - URL customHandlerUrl = new URL(new URL("jar", null, -1, context, this.handler), spec); - assertThat(customHandlerUrl).hasToString(standardUrl.toString()); - assertThat(customHandlerUrl.getFile()).isEqualTo(standardUrl.getFile()); - assertThat(customHandlerUrl.getPath()).isEqualTo(standardUrl.getPath()); - assertThat(customHandlerUrl.getQuery()).isEqualTo(standardUrl.getQuery()); - assertThat(customHandlerUrl.getRef()).isEqualTo(standardUrl.getRef()); - } - - private URL createUrl(String file) throws MalformedURLException { - return new URL("jar", null, -1, file, this.handler); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java deleted file mode 100644 index b37a99183a72..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java +++ /dev/null @@ -1,736 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.jar; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.FilePermission; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.attribute.FileTime; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Enumeration; -import java.util.Iterator; -import java.util.List; -import java.util.Random; -import java.util.jar.JarEntry; -import java.util.jar.JarInputStream; -import java.util.jar.JarOutputStream; -import java.util.jar.Manifest; -import java.util.stream.Stream; -import java.util.zip.CRC32; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -import org.assertj.core.api.ThrowableAssert.ThrowingCallable; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assumptions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.io.TempDir; - -import org.springframework.boot.loader.TestJarCreator; -import org.springframework.boot.loader.data.RandomAccessDataFile; -import org.springframework.util.FileCopyUtils; -import org.springframework.util.StopWatch; -import org.springframework.util.StreamUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIOException; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.spy; - -/** - * Tests for {@link JarFile}. - * - * @author Phillip Webb - * @author Martin Lau - * @author Andy Wilkinson - * @author Madhura Bhave - */ -@ExtendWith(JarUrlProtocolHandler.class) -class JarFileTests { - - private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs"; - - private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader"; - - @TempDir - File tempDir; - - private File rootJarFile; - - private JarFile jarFile; - - @BeforeEach - void setup() throws Exception { - this.rootJarFile = new File(this.tempDir, "root.jar"); - TestJarCreator.createTestJar(this.rootJarFile); - this.jarFile = new JarFile(this.rootJarFile); - } - - @AfterEach - void tearDown() throws Exception { - this.jarFile.close(); - } - - @Test - void jdkJarFile() throws Exception { - // Sanity checks to see how the default jar file operates - java.util.jar.JarFile jarFile = new java.util.jar.JarFile(this.rootJarFile); - assertThat(jarFile.getComment()).isEqualTo("outer"); - Enumeration entries = jarFile.entries(); - assertThat(entries.nextElement().getName()).isEqualTo("META-INF/"); - assertThat(entries.nextElement().getName()).isEqualTo("META-INF/MANIFEST.MF"); - assertThat(entries.nextElement().getName()).isEqualTo("1.dat"); - assertThat(entries.nextElement().getName()).isEqualTo("2.dat"); - assertThat(entries.nextElement().getName()).isEqualTo("d/"); - assertThat(entries.nextElement().getName()).isEqualTo("d/9.dat"); - assertThat(entries.nextElement().getName()).isEqualTo("special/"); - assertThat(entries.nextElement().getName()).isEqualTo("special/\u00EB.dat"); - assertThat(entries.nextElement().getName()).isEqualTo("nested.jar"); - assertThat(entries.nextElement().getName()).isEqualTo("another-nested.jar"); - assertThat(entries.nextElement().getName()).isEqualTo("space nested.jar"); - assertThat(entries.nextElement().getName()).isEqualTo("multi-release.jar"); - assertThat(entries.hasMoreElements()).isFalse(); - URL jarUrl = new URL("jar:" + this.rootJarFile.toURI() + "!/"); - URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { jarUrl }); - assertThat(urlClassLoader.getResource("special/\u00EB.dat")).isNotNull(); - assertThat(urlClassLoader.getResource("d/9.dat")).isNotNull(); - urlClassLoader.close(); - jarFile.close(); - } - - @Test - void createFromFile() throws Exception { - JarFile jarFile = new JarFile(this.rootJarFile); - assertThat(jarFile.getName()).isNotNull(); - jarFile.close(); - } - - @Test - void getManifest() throws Exception { - assertThat(this.jarFile.getManifest().getMainAttributes().getValue("Built-By")).isEqualTo("j1"); - } - - @Test - void getManifestEntry() throws Exception { - ZipEntry entry = this.jarFile.getJarEntry("META-INF/MANIFEST.MF"); - Manifest manifest = new Manifest(this.jarFile.getInputStream(entry)); - assertThat(manifest.getMainAttributes().getValue("Built-By")).isEqualTo("j1"); - } - - @Test - void getEntries() { - Enumeration entries = this.jarFile.entries(); - assertThat(entries.nextElement().getName()).isEqualTo("META-INF/"); - assertThat(entries.nextElement().getName()).isEqualTo("META-INF/MANIFEST.MF"); - assertThat(entries.nextElement().getName()).isEqualTo("1.dat"); - assertThat(entries.nextElement().getName()).isEqualTo("2.dat"); - assertThat(entries.nextElement().getName()).isEqualTo("d/"); - assertThat(entries.nextElement().getName()).isEqualTo("d/9.dat"); - assertThat(entries.nextElement().getName()).isEqualTo("special/"); - assertThat(entries.nextElement().getName()).isEqualTo("special/\u00EB.dat"); - assertThat(entries.nextElement().getName()).isEqualTo("nested.jar"); - assertThat(entries.nextElement().getName()).isEqualTo("another-nested.jar"); - assertThat(entries.nextElement().getName()).isEqualTo("space nested.jar"); - assertThat(entries.nextElement().getName()).isEqualTo("multi-release.jar"); - assertThat(entries.hasMoreElements()).isFalse(); - } - - @Test - void getSpecialResourceViaClassLoader() throws Exception { - URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { this.jarFile.getUrl() }); - assertThat(urlClassLoader.getResource("special/\u00EB.dat")).isNotNull(); - urlClassLoader.close(); - } - - @Test - void getJarEntry() { - java.util.jar.JarEntry entry = this.jarFile.getJarEntry("1.dat"); - assertThat(entry).isNotNull(); - assertThat(entry.getName()).isEqualTo("1.dat"); - } - - @Test - void getJarEntryWhenClosed() throws Exception { - this.jarFile.close(); - assertThatZipFileClosedIsThrownBy(() -> this.jarFile.getJarEntry("1.dat")); - } - - @Test - void getInputStream() throws Exception { - InputStream inputStream = this.jarFile.getInputStream(this.jarFile.getEntry("1.dat")); - assertThat(inputStream.available()).isOne(); - assertThat(inputStream.read()).isOne(); - assertThat(inputStream.available()).isZero(); - assertThat(inputStream.read()).isEqualTo(-1); - } - - @Test - void getInputStreamWhenClosed() throws Exception { - ZipEntry entry = this.jarFile.getEntry("1.dat"); - this.jarFile.close(); - assertThatZipFileClosedIsThrownBy(() -> this.jarFile.getInputStream(entry)); - } - - @Test - void getComment() { - assertThat(this.jarFile.getComment()).isEqualTo("outer"); - } - - @Test - void getCommentWhenClosed() throws Exception { - this.jarFile.close(); - assertThatZipFileClosedIsThrownBy(() -> this.jarFile.getComment()); - } - - @Test - void getName() { - assertThat(this.jarFile.getName()).isEqualTo(this.rootJarFile.getPath()); - } - - @Test - void size() throws Exception { - try (ZipFile zip = new ZipFile(this.rootJarFile)) { - assertThat(this.jarFile).hasSize(zip.size()); - } - } - - @Test - void sizeWhenClosed() throws Exception { - this.jarFile.close(); - assertThatZipFileClosedIsThrownBy(() -> this.jarFile.size()); - } - - @Test - void getEntryTime() throws Exception { - java.util.jar.JarFile jdkJarFile = new java.util.jar.JarFile(this.rootJarFile); - assertThat(this.jarFile.getEntry("META-INF/MANIFEST.MF").getTime()) - .isEqualTo(jdkJarFile.getEntry("META-INF/MANIFEST.MF").getTime()); - jdkJarFile.close(); - } - - @Test - void close() throws Exception { - RandomAccessDataFile randomAccessDataFile = spy(new RandomAccessDataFile(this.rootJarFile)); - JarFile jarFile = new JarFile(randomAccessDataFile); - jarFile.close(); - then(randomAccessDataFile).should().close(); - } - - @Test - void getUrl() throws Exception { - URL url = this.jarFile.getUrl(); - assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/"); - JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection(); - assertThat(JarFileWrapper.unwrap(jarURLConnection.getJarFile())).isSameAs(this.jarFile); - assertThat(jarURLConnection.getJarEntry()).isNull(); - assertThat(jarURLConnection.getContentLength()).isGreaterThan(1); - assertThat(JarFileWrapper.unwrap((java.util.jar.JarFile) jarURLConnection.getContent())).isSameAs(this.jarFile); - assertThat(jarURLConnection.getContentType()).isEqualTo("x-java/jar"); - assertThat(jarURLConnection.getJarFileURL().toURI()).isEqualTo(this.rootJarFile.toURI()); - } - - @Test - void createEntryUrl() throws Exception { - URL url = new URL(this.jarFile.getUrl(), "1.dat"); - assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/1.dat"); - JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection(); - assertThat(JarFileWrapper.unwrap(jarURLConnection.getJarFile())).isSameAs(this.jarFile); - assertThat(jarURLConnection.getJarEntry()).isSameAs(this.jarFile.getJarEntry("1.dat")); - assertThat(jarURLConnection.getContentLength()).isOne(); - assertThat(jarURLConnection.getContent()).isInstanceOf(InputStream.class); - assertThat(jarURLConnection.getContentType()).isEqualTo("content/unknown"); - assertThat(jarURLConnection.getPermission()).isInstanceOf(FilePermission.class); - FilePermission permission = (FilePermission) jarURLConnection.getPermission(); - assertThat(permission.getActions()).isEqualTo("read"); - assertThat(permission.getName()).isEqualTo(this.rootJarFile.getPath()); - } - - @Test - void getMissingEntryUrl() throws Exception { - URL url = new URL(this.jarFile.getUrl(), "missing.dat"); - assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/missing.dat"); - assertThatExceptionOfType(FileNotFoundException.class) - .isThrownBy(((JarURLConnection) url.openConnection())::getJarEntry); - } - - @Test - void getUrlStream() throws Exception { - URL url = this.jarFile.getUrl(); - url.openConnection(); - assertThatIOException().isThrownBy(url::openStream); - } - - @Test - void getEntryUrlStream() throws Exception { - URL url = new URL(this.jarFile.getUrl(), "1.dat"); - url.openConnection(); - try (InputStream stream = url.openStream()) { - assertThat(stream.read()).isOne(); - assertThat(stream.read()).isEqualTo(-1); - } - } - - @Test - void getNestedJarFile() throws Exception { - try (JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { - assertThat(nestedJarFile.getComment()).isEqualTo("nested"); - Enumeration entries = nestedJarFile.entries(); - assertThat(entries.nextElement().getName()).isEqualTo("META-INF/"); - assertThat(entries.nextElement().getName()).isEqualTo("META-INF/MANIFEST.MF"); - assertThat(entries.nextElement().getName()).isEqualTo("3.dat"); - assertThat(entries.nextElement().getName()).isEqualTo("4.dat"); - assertThat(entries.nextElement().getName()).isEqualTo("\u00E4.dat"); - assertThat(entries.hasMoreElements()).isFalse(); - - InputStream inputStream = nestedJarFile.getInputStream(nestedJarFile.getEntry("3.dat")); - assertThat(inputStream.read()).isEqualTo(3); - assertThat(inputStream.read()).isEqualTo(-1); - - URL url = nestedJarFile.getUrl(); - assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/nested.jar!/"); - JarURLConnection conn = (JarURLConnection) url.openConnection(); - assertThat(JarFileWrapper.unwrap(conn.getJarFile())).isSameAs(nestedJarFile); - assertThat(conn.getJarFileURL()).hasToString("jar:" + this.rootJarFile.toURI() + "!/nested.jar"); - assertThat(conn.getInputStream()).isNotNull(); - JarInputStream jarInputStream = new JarInputStream(conn.getInputStream()); - assertThat(jarInputStream.getNextJarEntry().getName()).isEqualTo("3.dat"); - assertThat(jarInputStream.getNextJarEntry().getName()).isEqualTo("4.dat"); - assertThat(jarInputStream.getNextJarEntry().getName()).isEqualTo("\u00E4.dat"); - jarInputStream.close(); - assertThat(conn.getPermission()).isInstanceOf(FilePermission.class); - FilePermission permission = (FilePermission) conn.getPermission(); - assertThat(permission.getActions()).isEqualTo("read"); - assertThat(permission.getName()).isEqualTo(this.rootJarFile.getPath()); - } - } - - @Test - void getNestedJarDirectory() throws Exception { - try (JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile.getEntry("d/"))) { - Enumeration entries = nestedJarFile.entries(); - assertThat(entries.nextElement().getName()).isEqualTo("9.dat"); - assertThat(entries.hasMoreElements()).isFalse(); - - try (InputStream inputStream = nestedJarFile.getInputStream(nestedJarFile.getEntry("9.dat"))) { - assertThat(inputStream.read()).isEqualTo(9); - assertThat(inputStream.read()).isEqualTo(-1); - } - - URL url = nestedJarFile.getUrl(); - assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/d!/"); - JarURLConnection connection = (JarURLConnection) url.openConnection(); - assertThat(JarFileWrapper.unwrap(connection.getJarFile())).isSameAs(nestedJarFile); - } - } - - @Test - void getNestedJarEntryUrl() throws Exception { - try (JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { - URL url = nestedJarFile.getJarEntry("3.dat").getUrl(); - assertThat(url).hasToString("jar:" + this.rootJarFile.toURI() + "!/nested.jar!/3.dat"); - try (InputStream inputStream = url.openStream()) { - assertThat(inputStream).isNotNull(); - assertThat(inputStream.read()).isEqualTo(3); - } - } - } - - @Test - void createUrlFromString() throws Exception { - String spec = "jar:" + this.rootJarFile.toURI() + "!/nested.jar!/3.dat"; - URL url = new URL(spec); - assertThat(url).hasToString(spec); - JarURLConnection connection = (JarURLConnection) url.openConnection(); - try (InputStream inputStream = connection.getInputStream()) { - assertThat(inputStream).isNotNull(); - assertThat(inputStream.read()).isEqualTo(3); - assertThat(connection.getURL()).hasToString(spec); - assertThat(connection.getJarFileURL()).hasToString("jar:" + this.rootJarFile.toURI() + "!/nested.jar"); - assertThat(connection.getEntryName()).isEqualTo("3.dat"); - connection.getJarFile().close(); - } - } - - @Test - void createNonNestedUrlFromString() throws Exception { - nonNestedJarFileFromString("jar:" + this.rootJarFile.toURI() + "!/2.dat"); - } - - @Test - void createNonNestedUrlFromPathString() throws Exception { - nonNestedJarFileFromString("jar:" + this.rootJarFile.toPath().toUri() + "!/2.dat"); - } - - private void nonNestedJarFileFromString(String spec) throws Exception { - JarFile.registerUrlProtocolHandler(); - URL url = new URL(spec); - assertThat(url).hasToString(spec); - JarURLConnection connection = (JarURLConnection) url.openConnection(); - try (InputStream inputStream = connection.getInputStream()) { - assertThat(inputStream).isNotNull(); - assertThat(inputStream.read()).isEqualTo(2); - assertThat(connection.getURL()).hasToString(spec); - assertThat(connection.getJarFileURL().toURI()).isEqualTo(this.rootJarFile.toURI()); - assertThat(connection.getEntryName()).isEqualTo("2.dat"); - } - connection.getJarFile().close(); - } - - @Test - void getDirectoryInputStream() throws Exception { - InputStream inputStream = this.jarFile.getInputStream(this.jarFile.getEntry("d/")); - assertThat(inputStream).isNotNull(); - assertThat(inputStream.read()).isEqualTo(-1); - } - - @Test - void getDirectoryInputStreamWithoutSlash() throws Exception { - InputStream inputStream = this.jarFile.getInputStream(this.jarFile.getEntry("d")); - assertThat(inputStream).isNotNull(); - assertThat(inputStream.read()).isEqualTo(-1); - } - - @Test - void sensibleToString() throws Exception { - assertThat(this.jarFile).hasToString(this.rootJarFile.getPath()); - try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { - assertThat(nested).hasToString(this.rootJarFile.getPath() + "!/nested.jar"); - } - } - - @Test - void verifySignedJar() throws Exception { - File signedJarFile = getSignedJarFile(); - assertThat(signedJarFile).exists(); - try (java.util.jar.JarFile expected = new java.util.jar.JarFile(signedJarFile)) { - try (JarFile actual = new JarFile(signedJarFile)) { - StopWatch stopWatch = new StopWatch(); - Enumeration actualEntries = actual.entries(); - while (actualEntries.hasMoreElements()) { - JarEntry actualEntry = actualEntries.nextElement(); - java.util.jar.JarEntry expectedEntry = expected.getJarEntry(actualEntry.getName()); - StreamUtils.drain(expected.getInputStream(expectedEntry)); - if (!actualEntry.getName().equals("META-INF/MANIFEST.MF")) { - assertThat(actualEntry.getCertificates()).as(actualEntry.getName()) - .isEqualTo(expectedEntry.getCertificates()); - assertThat(actualEntry.getCodeSigners()).as(actualEntry.getName()) - .isEqualTo(expectedEntry.getCodeSigners()); - } - } - assertThat(stopWatch.getTotalTimeSeconds()).isLessThan(3.0); - } - } - } - - private File getSignedJarFile() { - String[] entries = System.getProperty("java.class.path").split(System.getProperty("path.separator")); - for (String entry : entries) { - if (entry.contains("bcprov")) { - return new File(entry); - } - } - return null; - } - - @Test - void jarFileWithScriptAtTheStart() throws Exception { - File file = new File(this.tempDir, "test.jar"); - InputStream sourceJarContent = new FileInputStream(this.rootJarFile); - FileOutputStream outputStream = new FileOutputStream(file); - StreamUtils.copy("#/bin/bash", Charset.defaultCharset(), outputStream); - FileCopyUtils.copy(sourceJarContent, outputStream); - this.rootJarFile = file; - this.jarFile.close(); - this.jarFile = new JarFile(file); - // Call some other tests to verify - getEntries(); - getNestedJarFile(); - } - - @Test - void cannotLoadMissingJar() throws Exception { - // relates to gh-1070 - try (JarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { - URL nestedUrl = nestedJarFile.getUrl(); - URL url = new URL(nestedUrl, nestedJarFile.getUrl() + "missing.jar!/3.dat"); - assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(url.openConnection()::getInputStream); - } - } - - @Test - void registerUrlProtocolHandlerWithNoExistingRegistration() { - String original = System.getProperty(PROTOCOL_HANDLER); - try { - System.clearProperty(PROTOCOL_HANDLER); - JarFile.registerUrlProtocolHandler(); - String protocolHandler = System.getProperty(PROTOCOL_HANDLER); - assertThat(protocolHandler).isEqualTo(HANDLERS_PACKAGE); - } - finally { - if (original == null) { - System.clearProperty(PROTOCOL_HANDLER); - } - else { - System.setProperty(PROTOCOL_HANDLER, original); - } - } - } - - @Test - void registerUrlProtocolHandlerAddsToExistingRegistration() { - String original = System.getProperty(PROTOCOL_HANDLER); - try { - System.setProperty(PROTOCOL_HANDLER, "com.example"); - JarFile.registerUrlProtocolHandler(); - String protocolHandler = System.getProperty(PROTOCOL_HANDLER); - assertThat(protocolHandler).isEqualTo("com.example|" + HANDLERS_PACKAGE); - } - finally { - if (original == null) { - System.clearProperty(PROTOCOL_HANDLER); - } - else { - System.setProperty(PROTOCOL_HANDLER, original); - } - } - } - - @Test - void jarFileCanBeDeletedOnceItHasBeenClosed() throws Exception { - File jar = new File(this.tempDir, "test.jar"); - TestJarCreator.createTestJar(jar); - JarFile jf = new JarFile(jar); - jf.close(); - assertThat(jar.delete()).isTrue(); - } - - @Test - void createUrlFromStringWithContextWhenNotFound() throws Exception { - // gh-12483 - JarURLConnection.setUseFastExceptions(true); - try { - try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { - URL context = nested.getUrl(); - new URL(context, "jar:" + this.rootJarFile.toURI() + "!/nested.jar!/3.dat").openConnection() - .getInputStream() - .close(); - assertThatExceptionOfType(FileNotFoundException.class) - .isThrownBy(new URL(context, "jar:" + this.rootJarFile.toURI() + "!/no.dat") - .openConnection()::getInputStream); - } - } - finally { - JarURLConnection.setUseFastExceptions(false); - } - } - - @Test - void multiReleaseEntry() throws Exception { - try (JarFile multiRelease = this.jarFile.getNestedJarFile(this.jarFile.getEntry("multi-release.jar"))) { - ZipEntry entry = multiRelease.getEntry("multi-release.dat"); - assertThat(entry.getName()).isEqualTo("multi-release.dat"); - InputStream inputStream = multiRelease.getInputStream(entry); - assertThat(inputStream.available()).isOne(); - assertThat(inputStream.read()).isEqualTo(Runtime.version().feature()); - } - } - - @Test - void zip64JarThatExceedsZipEntryLimitCanBeRead() throws Exception { - File zip64Jar = new File(this.tempDir, "zip64.jar"); - FileCopyUtils.copy(zip64Jar(), zip64Jar); - try (JarFile zip64JarFile = new JarFile(zip64Jar)) { - List entries = Collections.list(zip64JarFile.entries()); - assertThat(entries).hasSize(65537); - for (int i = 0; i < entries.size(); i++) { - JarEntry entry = entries.get(i); - InputStream entryInput = zip64JarFile.getInputStream(entry); - assertThat(entryInput).hasContent("Entry " + (i + 1)); - } - } - } - - @Test - void zip64JarThatExceedsZipSizeLimitCanBeRead() throws Exception { - Assumptions.assumeTrue(this.tempDir.getFreeSpace() > 6 * 1024 * 1024 * 1024, "Insufficient disk space"); - File zip64Jar = new File(this.tempDir, "zip64.jar"); - File entry = new File(this.tempDir, "entry.dat"); - CRC32 crc32 = new CRC32(); - try (FileOutputStream entryOut = new FileOutputStream(entry)) { - byte[] data = new byte[1024 * 1024]; - new Random().nextBytes(data); - for (int i = 0; i < 1024; i++) { - entryOut.write(data); - crc32.update(data); - } - } - try (JarOutputStream jarOutput = new JarOutputStream(new FileOutputStream(zip64Jar))) { - for (int i = 0; i < 6; i++) { - JarEntry storedEntry = new JarEntry("huge-" + i); - storedEntry.setSize(entry.length()); - storedEntry.setCompressedSize(entry.length()); - storedEntry.setCrc(crc32.getValue()); - storedEntry.setMethod(ZipEntry.STORED); - jarOutput.putNextEntry(storedEntry); - try (FileInputStream entryIn = new FileInputStream(entry)) { - StreamUtils.copy(entryIn, jarOutput); - } - jarOutput.closeEntry(); - } - } - try (JarFile zip64JarFile = new JarFile(zip64Jar)) { - assertThat(Collections.list(zip64JarFile.entries())).hasSize(6); - } - } - - @Test - void nestedZip64JarCanBeRead() throws Exception { - File outer = new File(this.tempDir, "outer.jar"); - try (JarOutputStream jarOutput = new JarOutputStream(new FileOutputStream(outer))) { - JarEntry nestedEntry = new JarEntry("nested-zip64.jar"); - byte[] contents = zip64Jar(); - nestedEntry.setSize(contents.length); - nestedEntry.setCompressedSize(contents.length); - CRC32 crc32 = new CRC32(); - crc32.update(contents); - nestedEntry.setCrc(crc32.getValue()); - nestedEntry.setMethod(ZipEntry.STORED); - jarOutput.putNextEntry(nestedEntry); - jarOutput.write(contents); - jarOutput.closeEntry(); - } - try (JarFile outerJarFile = new JarFile(outer)) { - try (JarFile nestedZip64JarFile = outerJarFile - .getNestedJarFile(outerJarFile.getJarEntry("nested-zip64.jar"))) { - List entries = Collections.list(nestedZip64JarFile.entries()); - assertThat(entries).hasSize(65537); - for (int i = 0; i < entries.size(); i++) { - JarEntry entry = entries.get(i); - InputStream entryInput = nestedZip64JarFile.getInputStream(entry); - assertThat(entryInput).hasContent("Entry " + (i + 1)); - } - } - } - } - - private byte[] zip64Jar() throws IOException { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - JarOutputStream jarOutput = new JarOutputStream(bytes); - for (int i = 0; i < 65537; i++) { - jarOutput.putNextEntry(new JarEntry(i + ".dat")); - jarOutput.write(("Entry " + (i + 1)).getBytes(StandardCharsets.UTF_8)); - jarOutput.closeEntry(); - } - jarOutput.close(); - return bytes.toByteArray(); - } - - @Test - void jarFileEntryWithEpochTimeOfZeroShouldNotFail() throws Exception { - File file = createJarFileWithEpochTimeOfZero(); - try (JarFile jar = new JarFile(file)) { - Enumeration entries = jar.entries(); - JarEntry entry = entries.nextElement(); - assertThat(entry.getLastModifiedTime().toInstant()).isEqualTo(Instant.EPOCH); - assertThat(entry.getName()).isEqualTo("1.dat"); - } - } - - private File createJarFileWithEpochTimeOfZero() throws Exception { - File jarFile = new File(this.tempDir, "temp.jar"); - FileOutputStream fileOutputStream = new FileOutputStream(jarFile); - String comment = "outer"; - try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) { - jarOutputStream.setComment(comment); - JarEntry entry = new JarEntry("1.dat"); - entry.setLastModifiedTime(FileTime.from(Instant.EPOCH)); - jarOutputStream.putNextEntry(entry); - jarOutputStream.write(new byte[] { (byte) 1 }); - jarOutputStream.closeEntry(); - } - - byte[] data = Files.readAllBytes(jarFile.toPath()); - int headerPosition = data.length - ZipFile.ENDHDR - comment.getBytes().length; - int centralHeaderPosition = (int) Bytes.littleEndianValue(data, headerPosition + ZipFile.ENDOFF, 1); - int localHeaderPosition = (int) Bytes.littleEndianValue(data, centralHeaderPosition + ZipFile.CENOFF, 1); - writeTimeBlock(data, centralHeaderPosition + ZipFile.CENTIM, 0); - writeTimeBlock(data, localHeaderPosition + ZipFile.LOCTIM, 0); - - File jar = new File(this.tempDir, "zerotimed.jar"); - Files.write(jar.toPath(), data); - return jar; - } - - private static void writeTimeBlock(byte[] data, int pos, int value) { - data[pos] = (byte) (value & 0xff); - data[pos + 1] = (byte) ((value >> 8) & 0xff); - data[pos + 2] = (byte) ((value >> 16) & 0xff); - data[pos + 3] = (byte) ((value >> 24) & 0xff); - } - - @Test - void iterator() { - Iterator iterator = this.jarFile.iterator(); - List names = new ArrayList<>(); - while (iterator.hasNext()) { - names.add(iterator.next().getName()); - } - assertThat(names).hasSize(12).contains("1.dat"); - } - - @Test - void iteratorWhenClosed() throws IOException { - this.jarFile.close(); - assertThatZipFileClosedIsThrownBy(() -> this.jarFile.iterator()); - } - - @Test - void iteratorWhenClosedLater() throws IOException { - Iterator iterator = this.jarFile.iterator(); - iterator.next(); - this.jarFile.close(); - assertThatZipFileClosedIsThrownBy(() -> iterator.hasNext()); - } - - @Test - void stream() { - Stream stream = this.jarFile.stream().map(JarEntry::getName); - assertThat(stream).hasSize(12).contains("1.dat"); - - } - - private void assertThatZipFileClosedIsThrownBy(ThrowingCallable throwingCallable) { - assertThatIllegalStateException().isThrownBy(throwingCallable).withMessage("zip file closed"); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java deleted file mode 100644 index 8ae25b72e17a..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarFileWrapperTests.java +++ /dev/null @@ -1,281 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.jar; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.MalformedURLException; -import java.net.URL; -import java.security.Permission; -import java.util.EnumSet; -import java.util.Enumeration; -import java.util.Set; -import java.util.jar.JarOutputStream; -import java.util.jar.Manifest; -import java.util.stream.Stream; -import java.util.zip.ZipEntry; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import org.springframework.boot.loader.jar.JarFileWrapperTests.SpyJarFile.Call; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -/** - * Tests for {@link JarFileWrapper}. - * - * @author Phillip Webb - */ -class JarFileWrapperTests { - - private SpyJarFile parent; - - private JarFileWrapper wrapper; - - @BeforeEach - void setup(@TempDir File temp) throws Exception { - this.parent = new SpyJarFile(createTempJar(temp)); - this.wrapper = new JarFileWrapper(this.parent); - } - - @AfterEach - void cleanup() throws Exception { - this.parent.close(); - } - - private File createTempJar(File temp) throws IOException { - File file = new File(temp, "temp.jar"); - new JarOutputStream(new FileOutputStream(file)).close(); - return file; - } - - @Test - void getUrlDelegatesToParent() throws MalformedURLException { - this.wrapper.getUrl(); - this.parent.verify(Call.GET_URL); - } - - @Test - void getTypeDelegatesToParent() { - this.wrapper.getType(); - this.parent.verify(Call.GET_TYPE); - } - - @Test - void getPermissionDelegatesToParent() { - this.wrapper.getPermission(); - this.parent.verify(Call.GET_PERMISSION); - } - - @Test - void getManifestDelegatesToParent() throws IOException { - this.wrapper.getManifest(); - this.parent.verify(Call.GET_MANIFEST); - } - - @Test - void entriesDelegatesToParent() { - this.wrapper.entries(); - this.parent.verify(Call.ENTRIES); - } - - @Test - void getJarEntryDelegatesToParent() { - this.wrapper.getJarEntry("test"); - this.parent.verify(Call.GET_JAR_ENTRY); - } - - @Test - void getEntryDelegatesToParent() { - this.wrapper.getEntry("test"); - this.parent.verify(Call.GET_ENTRY); - } - - @Test - void getInputStreamDelegatesToParent() throws IOException { - this.wrapper.getInputStream(); - this.parent.verify(Call.GET_INPUT_STREAM); - } - - @Test - void getEntryInputStreamDelegatesToParent() throws IOException { - ZipEntry entry = new ZipEntry("test"); - this.wrapper.getInputStream(entry); - this.parent.verify(Call.GET_ENTRY_INPUT_STREAM); - } - - @Test - void getCommentDelegatesToParent() { - this.wrapper.getComment(); - this.parent.verify(Call.GET_COMMENT); - } - - @Test - void sizeDelegatesToParent() { - this.wrapper.size(); - this.parent.verify(Call.SIZE); - } - - @Test - void toStringDelegatesToParent() { - assertThat(this.wrapper.toString()).endsWith("temp.jar"); - } - - @Test // gh-22991 - void wrapperMustNotImplementClose() { - // If the wrapper overrides close then on Java 11 a FinalizableResource - // instance will be used to perform cleanup. This can result in a lot - // of additional memory being used since cleanup only occurs when the - // finalizer thread runs. See gh-22991 - assertThatExceptionOfType(NoSuchMethodException.class) - .isThrownBy(() -> JarFileWrapper.class.getDeclaredMethod("close")); - } - - @Test - void streamDelegatesToParent() { - this.wrapper.stream(); - this.parent.verify(Call.STREAM); - } - - /** - * {@link JarFile} that we can spy (even on Java 11+) - */ - static class SpyJarFile extends JarFile { - - private final Set calls = EnumSet.noneOf(Call.class); - - SpyJarFile(File file) throws IOException { - super(file); - } - - @Override - Permission getPermission() { - mark(Call.GET_PERMISSION); - return super.getPermission(); - } - - @Override - public Manifest getManifest() throws IOException { - mark(Call.GET_MANIFEST); - return super.getManifest(); - } - - @Override - public Enumeration entries() { - mark(Call.ENTRIES); - return super.entries(); - } - - @Override - public Stream stream() { - mark(Call.STREAM); - return super.stream(); - } - - @Override - public JarEntry getJarEntry(String name) { - mark(Call.GET_JAR_ENTRY); - return super.getJarEntry(name); - } - - @Override - public ZipEntry getEntry(String name) { - mark(Call.GET_ENTRY); - return super.getEntry(name); - } - - @Override - InputStream getInputStream() throws IOException { - mark(Call.GET_INPUT_STREAM); - return super.getInputStream(); - } - - @Override - InputStream getInputStream(String name) throws IOException { - mark(Call.GET_ENTRY_INPUT_STREAM); - return super.getInputStream(name); - } - - @Override - public String getComment() { - mark(Call.GET_COMMENT); - return super.getComment(); - } - - @Override - public int size() { - mark(Call.SIZE); - return super.size(); - } - - @Override - public URL getUrl() throws MalformedURLException { - mark(Call.GET_URL); - return super.getUrl(); - } - - @Override - JarFileType getType() { - mark(Call.GET_TYPE); - return super.getType(); - } - - private void mark(Call call) { - this.calls.add(call); - } - - void verify(Call call) { - assertThat(call).matches(this.calls::contains); - } - - enum Call { - - GET_URL, - - GET_TYPE, - - GET_PERMISSION, - - GET_MANIFEST, - - ENTRIES, - - GET_JAR_ENTRY, - - GET_ENTRY, - - GET_INPUT_STREAM, - - GET_ENTRY_INPUT_STREAM, - - GET_COMMENT, - - SIZE, - - STREAM - - } - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java deleted file mode 100644 index d962a72fc575..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarURLConnectionTests.java +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.jar; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.net.URL; -import java.util.List; -import java.util.jar.JarEntry; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import org.springframework.boot.loader.TestJarCreator; -import org.springframework.boot.loader.jar.JarURLConnection.JarEntryName; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -/** - * Tests for {@link JarURLConnection}. - * - * @author Andy Wilkinson - * @author Phillip Webb - * @author Rostyslav Dudka - */ -class JarURLConnectionTests { - - private File rootJarFile; - - private JarFile jarFile; - - @BeforeEach - void setup(@TempDir File tempDir) throws Exception { - this.rootJarFile = new File(tempDir, "root.jar"); - TestJarCreator.createTestJar(this.rootJarFile); - this.jarFile = new JarFile(this.rootJarFile); - } - - @AfterEach - void tearDown() throws Exception { - this.jarFile.close(); - } - - @Test - void connectionToRootUsingAbsoluteUrl() throws Exception { - URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/"); - Object content = JarURLConnection.get(url, this.jarFile).getContent(); - assertThat(JarFileWrapper.unwrap((java.util.jar.JarFile) content)).isSameAs(this.jarFile); - } - - @Test - void connectionToRootUsingRelativeUrl() throws Exception { - URL url = new URL("jar:file:" + getRelativePath() + "!/"); - Object content = JarURLConnection.get(url, this.jarFile).getContent(); - assertThat(JarFileWrapper.unwrap((java.util.jar.JarFile) content)).isSameAs(this.jarFile); - } - - @Test - void connectionToEntryUsingAbsoluteUrl() throws Exception { - URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/1.dat"); - try (InputStream input = JarURLConnection.get(url, this.jarFile).getInputStream()) { - assertThat(input).hasBinaryContent(new byte[] { 1 }); - } - } - - @Test - void connectionToEntryUsingRelativeUrl() throws Exception { - URL url = new URL("jar:file:" + getRelativePath() + "!/1.dat"); - try (InputStream input = JarURLConnection.get(url, this.jarFile).getInputStream()) { - assertThat(input).hasBinaryContent(new byte[] { 1 }); - } - } - - @Test - void connectionToEntryUsingAbsoluteUrlWithFileColonSlashSlashPrefix() throws Exception { - URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/1.dat"); - try (InputStream input = JarURLConnection.get(url, this.jarFile).getInputStream()) { - assertThat(input).hasBinaryContent(new byte[] { 1 }); - } - } - - @Test - void connectionToEntryUsingAbsoluteUrlForNestedEntry() throws Exception { - URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/nested.jar!/3.dat"); - JarURLConnection connection = JarURLConnection.get(url, this.jarFile); - try (InputStream input = connection.getInputStream()) { - assertThat(input).hasBinaryContent(new byte[] { 3 }); - } - connection.getJarFile().close(); - } - - @Test - void connectionToEntryUsingRelativeUrlForNestedEntry() throws Exception { - URL url = new URL("jar:file:" + getRelativePath() + "!/nested.jar!/3.dat"); - JarURLConnection connection = JarURLConnection.get(url, this.jarFile); - try (InputStream input = connection.getInputStream()) { - assertThat(input).hasBinaryContent(new byte[] { 3 }); - } - connection.getJarFile().close(); - } - - @Test - void connectionToEntryUsingAbsoluteUrlForEntryFromNestedJarFile() throws Exception { - URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/nested.jar!/3.dat"); - try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { - try (InputStream input = JarURLConnection.get(url, nested).getInputStream()) { - assertThat(input).hasBinaryContent(new byte[] { 3 }); - } - } - } - - @Test - void connectionToEntryUsingRelativeUrlForEntryFromNestedJarFile() throws Exception { - URL url = new URL("jar:file:" + getRelativePath() + "!/nested.jar!/3.dat"); - try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { - try (InputStream input = JarURLConnection.get(url, nested).getInputStream()) { - assertThat(input).hasBinaryContent(new byte[] { 3 }); - } - } - } - - @Test - void connectionToEntryInNestedJarFromUrlThatUsesExistingUrlAsContext() throws Exception { - URL url = new URL(new URL("jar", null, -1, this.rootJarFile.toURI().toURL() + "!/nested.jar!/", new Handler()), - "/3.dat"); - try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { - try (InputStream input = JarURLConnection.get(url, nested).getInputStream()) { - assertThat(input).hasBinaryContent(new byte[] { 3 }); - } - } - } - - @Test - void connectionToEntryWithSpaceNestedEntry() throws Exception { - URL url = new URL("jar:file:" + getRelativePath() + "!/space nested.jar!/3.dat"); - JarURLConnection connection = JarURLConnection.get(url, this.jarFile); - try (InputStream input = connection.getInputStream()) { - assertThat(input).hasBinaryContent(new byte[] { 3 }); - } - connection.getJarFile().close(); - } - - @Test - void connectionToEntryWithEncodedSpaceNestedEntry() throws Exception { - URL url = new URL("jar:file:" + getRelativePath() + "!/space%20nested.jar!/3.dat"); - JarURLConnection connection = JarURLConnection.get(url, this.jarFile); - try (InputStream input = connection.getInputStream()) { - assertThat(input).hasBinaryContent(new byte[] { 3 }); - } - connection.getJarFile().close(); - } - - @Test - void connectionToEntryUsingWrongAbsoluteUrlForEntryFromNestedJarFile() throws Exception { - URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/w.jar!/3.dat"); - try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { - assertThatExceptionOfType(FileNotFoundException.class) - .isThrownBy(JarURLConnection.get(url, nested)::getInputStream); - } - } - - @Test - void getContentLengthReturnsLengthOfUnderlyingEntry() throws Exception { - URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/nested.jar!/3.dat"); - try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { - JarURLConnection connection = JarURLConnection.get(url, nested); - assertThat(connection.getContentLength()).isOne(); - } - } - - @Test - void getContentLengthLongReturnsLengthOfUnderlyingEntry() throws Exception { - URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/nested.jar!/3.dat"); - try (JarFile nested = this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))) { - JarURLConnection connection = JarURLConnection.get(url, nested); - assertThat(connection.getContentLengthLong()).isOne(); - } - } - - @Test - void getLastModifiedReturnsLastModifiedTimeOfJarEntry() throws Exception { - URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/1.dat"); - JarURLConnection connection = JarURLConnection.get(url, this.jarFile); - assertThat(connection.getLastModified()).isEqualTo(connection.getJarEntry().getTime()); - } - - @Test - void entriesCanBeStreamedFromJarFileOfConnection() throws Exception { - URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/"); - JarURLConnection connection = JarURLConnection.get(url, this.jarFile); - List entryNames = connection.getJarFile().stream().map(JarEntry::getName).toList(); - assertThat(entryNames).hasSize(12); - } - - @Test - void jarEntryBasicName() { - assertThat(new JarEntryName(new StringSequence("a/b/C.class"))).hasToString("a/b/C.class"); - } - - @Test - void jarEntryNameWithSingleByteEncodedCharacters() { - assertThat(new JarEntryName(new StringSequence("%61/%62/%43.class"))).hasToString("a/b/C.class"); - } - - @Test - void jarEntryNameWithDoubleByteEncodedCharacters() { - assertThat(new JarEntryName(new StringSequence("%c3%a1/b/C.class"))).hasToString("\u00e1/b/C.class"); - } - - @Test - void jarEntryNameWithMixtureOfEncodedAndUnencodedDoubleByteCharacters() { - assertThat(new JarEntryName(new StringSequence("%c3%a1/b/\u00c7.class"))).hasToString("\u00e1/b/\u00c7.class"); - } - - @Test - void openConnectionCanBeClosedWithoutClosingSourceJar() throws Exception { - URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/"); - JarURLConnection connection = JarURLConnection.get(url, this.jarFile); - java.util.jar.JarFile connectionJarFile = connection.getJarFile(); - connectionJarFile.close(); - assertThat(this.jarFile.isClosed()).isFalse(); - } - - private String getRelativePath() { - return this.rootJarFile.getPath().replace('\\', '/'); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarUrlProtocolHandler.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarUrlProtocolHandler.java deleted file mode 100644 index d9e5eb281420..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/JarUrlProtocolHandler.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.jar; - -import java.io.File; -import java.lang.ref.SoftReference; -import java.util.Map; - -import org.junit.jupiter.api.extension.AfterEachCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; -import org.junit.jupiter.api.extension.Extension; -import org.junit.jupiter.api.extension.ExtensionContext; - -import org.springframework.test.util.ReflectionTestUtils; - -/** - * JUnit 5 {@link Extension} for tests that interact with Spring Boot's {@link Handler} - * for {@code jar:} URLs. Ensures that the handler is registered prior to test execution - * and cleans up the handler's root file cache afterwards. - * - * @author Andy Wilkinson - */ -class JarUrlProtocolHandler implements BeforeEachCallback, AfterEachCallback { - - @Override - public void beforeEach(ExtensionContext context) throws Exception { - JarFile.registerUrlProtocolHandler(); - } - - @Override - @SuppressWarnings("unchecked") - public void afterEach(ExtensionContext context) throws Exception { - Map rootFileCache = ((SoftReference>) ReflectionTestUtils - .getField(Handler.class, "rootFileCache")).get(); - if (rootFileCache != null) { - for (JarFile rootJarFile : rootFileCache.values()) { - rootJarFile.close(); - } - rootFileCache.clear(); - } - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/ManifestInfoTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/ManifestInfoTests.java new file mode 100644 index 000000000000..619080b175a7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/ManifestInfoTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.util.jar.Attributes.Name; +import java.util.jar.Manifest; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ManifestInfo}. + * + * @author Phillip Webb + */ +class ManifestInfoTests { + + @Test + void noneReturnsNoDetails() { + assertThat(ManifestInfo.NONE.getManifest()).isNull(); + assertThat(ManifestInfo.NONE.isMultiRelease()).isFalse(); + } + + @Test + void getManifestReturnsManifest() { + Manifest manifest = new Manifest(); + ManifestInfo info = new ManifestInfo(manifest); + assertThat(info.getManifest()).isSameAs(manifest); + } + + @Test + void isMultiReleaseWhenHasMultiReleaseAttributeReturnsTrue() { + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(new Name("Multi-Release"), "true"); + ManifestInfo info = new ManifestInfo(manifest); + assertThat(info.isMultiRelease()).isTrue(); + } + + @Test + void isMultiReleaseWhenHasNoMultiReleaseAttributeReturnsFalse() { + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(new Name("Random-Release"), "true"); + ManifestInfo info = new ManifestInfo(manifest); + assertThat(info.isMultiRelease()).isFalse(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/MetaInfVersionsInfoTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/MetaInfVersionsInfoTests.java new file mode 100644 index 000000000000..d556c9cbea57 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/MetaInfVersionsInfoTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.loader.zip.ZipContent; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link MetaInfVersionsInfo}. + * + * @author Phillip Webb + */ +class MetaInfVersionsInfoTests { + + @Test + void getParsesVersionsAndEntries() { + List entries = new ArrayList<>(); + entries.add(mockEntry("META-INF/")); + entries.add(mockEntry("META-INF/MANIFEST.MF")); + entries.add(mockEntry("META-INF/versions/")); + entries.add(mockEntry("META-INF/versions/9/")); + entries.add(mockEntry("META-INF/versions/9/Foo.class")); + entries.add(mockEntry("META-INF/versions/11/")); + entries.add(mockEntry("META-INF/versions/11/Foo.class")); + entries.add(mockEntry("META-INF/versions/10/")); + entries.add(mockEntry("META-INF/versions/10/Foo.class")); + MetaInfVersionsInfo info = MetaInfVersionsInfo.get(entries.size(), entries::get); + assertThat(info.versions()).containsExactly(9, 10, 11); + assertThat(info.directories()).containsExactly("META-INF/versions/9/", "META-INF/versions/10/", + "META-INF/versions/11/"); + } + + @Test + void getWhenHasBadEntryParsesGoodVersionsAndEntries() { + List entries = new ArrayList<>(); + entries.add(mockEntry("META-INF/versions/9/Foo.class")); + entries.add(mockEntry("META-INF/versions/0x11/Foo.class")); + MetaInfVersionsInfo info = MetaInfVersionsInfo.get(entries.size(), entries::get); + assertThat(info.versions()).containsExactly(9); + assertThat(info.directories()).containsExactly("META-INF/versions/9/"); + } + + @Test + void getWhenHasNoEntriesReturnsNone() { + List entries = new ArrayList<>(); + MetaInfVersionsInfo info = MetaInfVersionsInfo.get(entries.size(), entries::get); + assertThat(info.versions()).isEmpty(); + assertThat(info.directories()).isEmpty(); + assertThat(info).isSameAs(MetaInfVersionsInfo.NONE); + } + + private ZipContent.Entry mockEntry(String name) { + ZipContent.Entry entry = mock(ZipContent.Entry.class); + given(entry.getName()).willReturn(name); + given(entry.hasNameStartingWith(any())) + .willAnswer((invocation) -> name.startsWith(invocation.getArgument(0, CharSequence.class).toString())); + given(entry.isDirectory()).willAnswer((invocation) -> name.endsWith("/")); + return entry; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java new file mode 100644 index 000000000000..1944f30b9f4d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java @@ -0,0 +1,368 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.Cleaner.Cleanable; +import java.nio.charset.Charset; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.zip.ZipFile; + +import org.assertj.core.extractor.Extractors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.ArgumentCaptor; + +import org.springframework.boot.loader.ref.Cleaner; +import org.springframework.boot.loader.testsupport.TestJar; +import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed; +import org.springframework.boot.loader.zip.ZipContent; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StopWatch; +import org.springframework.util.StreamUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.atMostOnce; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link NestedJarFile}. + * + * @author Phillip Webb + * @author Martin Lau + * @author Andy Wilkinson + * @author Madhura Bhave + */ +@AssertFileChannelDataBlocksClosed +class NestedJarFileTests { + + @TempDir + File tempDir; + + private File file; + + @BeforeEach + void setup() throws Exception { + this.file = new File(this.tempDir, "test.jar"); + TestJar.create(this.file); + } + + @Test + void createOpensJar() throws IOException { + try (NestedJarFile jar = new NestedJarFile(this.file)) { + try (JarFile jdkJar = new JarFile(this.file)) { + assertThat(jar.size()).isEqualTo(jdkJar.size()); + assertThat(jar.getComment()).isEqualTo(jdkJar.getComment()); + Enumeration entries = jar.entries(); + Enumeration jdkEntries = jdkJar.entries(); + while (entries.hasMoreElements()) { + assertThat(entries.nextElement().getName()).isEqualTo(jdkEntries.nextElement().getName()); + } + assertThat(jdkEntries.hasMoreElements()).isFalse(); + try (InputStream in = jar.getInputStream(jar.getEntry("1.dat"))) { + assertThat(in.readAllBytes()).containsExactly(new byte[] { 1 }); + } + } + } + } + + @Test + void createWhenNestedJarFileOpensJar() throws IOException { + try (NestedJarFile jar = new NestedJarFile(this.file, "nested.jar")) { + assertThat(jar.size()).isEqualTo(5); + assertThat(jar.stream().map(JarEntry::getName)).containsExactly("META-INF/", "META-INF/MANIFEST.MF", + "3.dat", "4.dat", "\u00E4.dat"); + } + } + + @Test + void createWhenNestedJarDirectoryOpensJar() throws IOException { + try (NestedJarFile jar = new NestedJarFile(this.file, "d/")) { + assertThat(jar.getName()).isEqualTo(this.file.getAbsolutePath() + "!/d/"); + assertThat(jar.size()).isEqualTo(3); + assertThat(jar.stream().map(JarEntry::getName)).containsExactly("META-INF/", "META-INF/MANIFEST.MF", + "9.dat"); + } + } + + @Test + void createWhenJarHasFrontMatterOpensJar() throws IOException { + File file = new File(this.tempDir, "frontmatter.jar"); + InputStream sourceJarContent = new FileInputStream(this.file); + FileOutputStream outputStream = new FileOutputStream(file); + StreamUtils.copy("#/bin/bash", Charset.defaultCharset(), outputStream); + FileCopyUtils.copy(sourceJarContent, outputStream); + try (NestedJarFile jar = new NestedJarFile(file)) { + assertThat(jar.size()).isEqualTo(12); + } + try (NestedJarFile jar = new NestedJarFile(this.file, "nested.jar")) { + assertThat(jar.size()).isEqualTo(5); + } + } + + @Test + void getEntryReturnsEntry() throws IOException { + try (NestedJarFile jar = new NestedJarFile(this.file)) { + JarEntry entry = jar.getEntry("1.dat"); + assertEntryOne(entry); + } + } + + @Test + void getEntryWhenClosedThrowsException() throws IOException { + try (NestedJarFile jar = new NestedJarFile(this.file)) { + jar.close(); + assertThatIllegalStateException().isThrownBy(() -> jar.getEntry("1.dat")).withMessage("Zip file closed"); + } + } + + @Test + void getJarEntryReturnsEntry() throws IOException { + try (NestedJarFile jar = new NestedJarFile(this.file)) { + JarEntry entry = jar.getJarEntry("1.dat"); + assertEntryOne(entry); + } + } + + @Test + void getJarEntryWhenClosedThrowsException() throws IOException { + try (NestedJarFile jar = new NestedJarFile(this.file)) { + jar.close(); + assertThatIllegalStateException().isThrownBy(() -> jar.getJarEntry("1.dat")).withMessage("Zip file closed"); + } + } + + private void assertEntryOne(JarEntry entry) { + assertThat(entry.getName()).isEqualTo("1.dat"); + assertThat(entry.getRealName()).isEqualTo("1.dat"); + assertThat(entry.getSize()).isEqualTo(1); + assertThat(entry.getCompressedSize()).isEqualTo(3); + assertThat(entry.getCrc()).isEqualTo(2768625435L); + assertThat(entry.getMethod()).isEqualTo(8); + } + + @Test + void getEntryWhenMultiReleaseEntryReturnsEntry() throws IOException { + File multiReleaseFile = new File(this.tempDir, "mutli.zip"); + try (ZipContent zip = ZipContent.open(this.file.toPath(), "multi-release.jar")) { + try (InputStream in = zip.openRawZipData().asInputStream()) { + try (FileOutputStream out = new FileOutputStream(multiReleaseFile)) { + in.transferTo(out); + } + } + } + try (NestedJarFile jar = new NestedJarFile(this.file, "multi-release.jar", JarFile.runtimeVersion())) { + try (JarFile jdkJar = new JarFile(multiReleaseFile, true, ZipFile.OPEN_READ, JarFile.runtimeVersion())) { + JarEntry entry = jar.getJarEntry("multi-release.dat"); + JarEntry jdkEntry = jdkJar.getJarEntry("multi-release.dat"); + assertThat(entry.getName()).isEqualTo(jdkEntry.getName()); + assertThat(entry.getRealName()).isEqualTo(jdkEntry.getRealName()); + try (InputStream inputStream = jdkJar.getInputStream(entry)) { + assertThat(inputStream.available()).isOne(); + assertThat(inputStream.read()).isEqualTo(Runtime.version().feature()); + } + try (InputStream inputStream = jar.getInputStream(entry)) { + assertThat(inputStream.available()).isOne(); + assertThat(inputStream.read()).isEqualTo(Runtime.version().feature()); + } + } + } + } + + @Test + void getManifestReturnsManifest() throws IOException { + try (NestedJarFile jar = new NestedJarFile(this.file)) { + Manifest manifest = jar.getManifest(); + assertThat(manifest).isNotNull(); + assertThat(manifest.getEntries()).isEmpty(); + assertThat(manifest.getMainAttributes().getValue("Manifest-Version")).isEqualTo("1.0"); + } + } + + @Test + void getCommentReturnsComment() throws IOException { + try (NestedJarFile jar = new NestedJarFile(this.file)) { + assertThat(jar.getComment()).isEqualTo("outer"); + } + } + + @Test + void getCommentWhenClosedThrowsException() throws IOException { + try (NestedJarFile jar = new NestedJarFile(this.file)) { + jar.close(); + assertThatIllegalStateException().isThrownBy(() -> jar.getComment()).withMessage("Zip file closed"); + } + } + + @Test + void getNameReturnsName() throws IOException { + try (NestedJarFile jar = new NestedJarFile(this.file)) { + assertThat(jar.getName()).isEqualTo(this.file.getAbsolutePath()); + } + } + + @Test + void getNameWhenNestedReturnsName() throws IOException { + try (NestedJarFile jar = new NestedJarFile(this.file, "nested.jar")) { + assertThat(jar.getName()).isEqualTo(this.file.getAbsolutePath() + "!/nested.jar"); + } + } + + @Test + void sizeReturnsSize() throws IOException { + try (NestedJarFile jar = new NestedJarFile(this.file)) { + assertThat(jar.size()).isEqualByComparingTo(12); + } + } + + @Test + void sizeWhenClosedThowsException() throws Exception { + try (NestedJarFile jar = new NestedJarFile(this.file)) { + jar.close(); + assertThatIllegalStateException().isThrownBy(() -> jar.size()).withMessage("Zip file closed"); + } + } + + @Test + void getEntryTime() throws IOException { + try (NestedJarFile jar = new NestedJarFile(this.file)) { + try (JarFile jdkJar = new JarFile(this.file)) { + assertThat(jar.getEntry("META-INF/MANIFEST.MF").getTime()) + .isEqualTo(jar.getEntry("META-INF/MANIFEST.MF").getTime()); + } + } + } + + @Test + void closeTriggersCleanupOnlyOnce() throws IOException { + Cleaner cleaner = mock(Cleaner.class); + ArgumentCaptor action = ArgumentCaptor.forClass(Runnable.class); + Cleanable cleanable = mock(Cleanable.class); + given(cleaner.register(any(), action.capture())).willReturn(cleanable); + NestedJarFile jar = new NestedJarFile(this.file, null, null, false, cleaner); + jar.close(); + jar.close(); + then(cleanable).should(atMostOnce()).clean(); + action.getValue().run(); + } + + @Test + void cleanupFromReleasesResources() throws IOException { + Cleaner cleaner = mock(Cleaner.class); + ArgumentCaptor action = ArgumentCaptor.forClass(Runnable.class); + Cleanable cleanable = mock(Cleanable.class); + given(cleaner.register(any(), action.capture())).willReturn(cleanable); + try (NestedJarFile jar = new NestedJarFile(this.file, null, null, false, cleaner)) { + Object channel = Extractors.byName("resources.zipContent.data.channel").apply(jar); + assertThat(channel).extracting("referenceCount").isEqualTo(1); + action.getValue().run(); + assertThat(channel).extracting("referenceCount").isEqualTo(0); + } + } + + @Test + void getInputStreamReturnsInputStream() throws IOException { + try (NestedJarFile jarFile = new NestedJarFile(this.file)) { + JarEntry entry = jarFile.getJarEntry("2.dat"); + try (InputStream in = jarFile.getInputStream(entry)) { + assertThat(in).hasBinaryContent(new byte[] { 0x02 }); + } + } + } + + @Test + void getInputStreamWhenIsDirectory() throws IOException { + try (NestedJarFile jar = new NestedJarFile(this.file)) { + try (InputStream inputStream = jar.getInputStream(jar.getEntry("d/"))) { + assertThat(inputStream).isNotNull(); + assertThat(inputStream.read()).isEqualTo(-1); + } + } + } + + @Test + void getInputStreamWhenNameWithoutSlashAndIsDirectory() throws IOException { + try (NestedJarFile jar = new NestedJarFile(this.file)) { + try (InputStream inputStream = jar.getInputStream(jar.getEntry("d"))) { + assertThat(inputStream).isNotNull(); + assertThat(inputStream.read()).isEqualTo(-1); + } + } + } + + @Test + void verifySignedJar() throws Exception { + File signedJarFile = TestJar.getSigned(); + assertThat(signedJarFile).exists(); + try (JarFile expected = new JarFile(signedJarFile)) { + try (NestedJarFile actual = new NestedJarFile(signedJarFile)) { + StopWatch stopWatch = new StopWatch(); + Enumeration actualEntries = actual.entries(); + while (actualEntries.hasMoreElements()) { + JarEntry actualEntry = actualEntries.nextElement(); + JarEntry expectedEntry = expected.getJarEntry(actualEntry.getName()); + StreamUtils.drain(expected.getInputStream(expectedEntry)); + if (!actualEntry.getName().equals("META-INF/MANIFEST.MF")) { + assertThat(actualEntry.getCertificates()).as(actualEntry.getName()) + .isEqualTo(expectedEntry.getCertificates()); + assertThat(actualEntry.getCodeSigners()).as(actualEntry.getName()) + .isEqualTo(expectedEntry.getCodeSigners()); + } + } + assertThat(stopWatch.getTotalTimeSeconds()).isLessThan(3.0); + } + } + } + + @Test + void closeAllowsFileToBeDeleted() throws Exception { + new NestedJarFile(this.file).close(); + assertThat(this.file.delete()).isTrue(); + } + + @Test + void streamStreamsEnties() throws IOException { + try (NestedJarFile jar = new NestedJarFile(this.file, "multi-release.jar")) { + assertThat(jar.stream().map((entry) -> entry.getName() + ":" + entry.getRealName())).containsExactly( + "META-INF/:META-INF/", "META-INF/MANIFEST.MF:META-INF/MANIFEST.MF", + "multi-release.dat:multi-release.dat", + "META-INF/versions/17/multi-release.dat:META-INF/versions/17/multi-release.dat"); + } + } + + @Test + void versionedStreamStreamsEntries() throws IOException { + try (NestedJarFile jar = new NestedJarFile(this.file, "multi-release.jar", Runtime.version())) { + assertThat(jar.versionedStream().map((entry) -> entry.getName() + ":" + entry.getRealName())) + .containsExactly("META-INF/:META-INF/", "META-INF/MANIFEST.MF:META-INF/MANIFEST.MF", + "multi-release.dat:META-INF/versions/17/multi-release.dat"); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/SecurityInfoTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/SecurityInfoTests.java new file mode 100644 index 000000000000..c5de14ad2eb4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/SecurityInfoTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-2023 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.loader.jar; + +import java.io.File; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.testsupport.TestJar; +import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed; +import org.springframework.boot.loader.zip.ZipContent; +import org.springframework.boot.loader.zip.ZipContent.Entry; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SecurityInfo}. + * + * @author Phillip Webb + */ +@AssertFileChannelDataBlocksClosed +class SecurityInfoTests { + + @TempDir + File temp; + + @Test + void getWhenNoSignatureFileReturnsNone() throws Exception { + File file = new File(this.temp, "test.jar"); + TestJar.create(file); + try (ZipContent content = ZipContent.open(file.toPath())) { + SecurityInfo info = SecurityInfo.get(content); + assertThat(info).isSameAs(SecurityInfo.NONE); + for (int i = 0; i < content.size(); i++) { + Entry entry = content.getEntry(i); + assertThat(info.getCertificates(entry)).isNull(); + assertThat(info.getCodeSigners(entry)).isNull(); + } + } + } + + @Test + void getWhenHasSignatureFileButNoSecuityMaterialReturnsNone() throws Exception { + File file = new File(this.temp, "test.jar"); + TestJar.create(file, false, true); + try (ZipContent content = ZipContent.open(file.toPath())) { + assertThat(content.hasJarSignatureFile()).isTrue(); + SecurityInfo info = SecurityInfo.get(content); + assertThat(info).isSameAs(SecurityInfo.NONE); + } + } + + @Test + void getWhenJarIsSigned() throws Exception { + File file = TestJar.getSigned(); + try (ZipContent content = ZipContent.open(file.toPath())) { + assertThat(content.hasJarSignatureFile()).isTrue(); + SecurityInfo info = SecurityInfo.get(content); + for (int i = 0; i < content.size(); i++) { + Entry entry = content.getEntry(i); + if (entry.getName().endsWith(".class")) { + assertThat(info.getCertificates(entry)).isNotNull(); + assertThat(info.getCodeSigners(entry)).isNotNull(); + } + } + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java deleted file mode 100644 index ee7170f08c25..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/StringSequenceTests.java +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.jar; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; - -/** - * Tests for {@link StringSequence}. - * - * @author Phillip Webb - */ -class StringSequenceTests { - - @Test - void createWhenSourceIsNullShouldThrowException() { - assertThatNullPointerException().isThrownBy(() -> new StringSequence(null)) - .withMessage("Source must not be null"); - } - - @Test - void createWithIndexWhenSourceIsNullShouldThrowException() { - assertThatNullPointerException().isThrownBy(() -> new StringSequence(null, 0, 0)) - .withMessage("Source must not be null"); - } - - @Test - void createWhenStartIsLessThanZeroShouldThrowException() { - assertThatExceptionOfType(StringIndexOutOfBoundsException.class) - .isThrownBy(() -> new StringSequence("x", -1, 0)); - } - - @Test - void createWhenEndIsGreaterThanLengthShouldThrowException() { - assertThatExceptionOfType(StringIndexOutOfBoundsException.class) - .isThrownBy(() -> new StringSequence("x", 0, 2)); - } - - @Test - void createFromString() { - assertThat(new StringSequence("test")).hasToString("test"); - } - - @Test - void subSequenceWithJustStartShouldReturnSubSequence() { - assertThat(new StringSequence("smiles").subSequence(1)).hasToString("miles"); - } - - @Test - void subSequenceShouldReturnSubSequence() { - assertThat(new StringSequence("hamburger").subSequence(4, 8)).hasToString("urge"); - assertThat(new StringSequence("smiles").subSequence(1, 5)).hasToString("mile"); - } - - @Test - void subSequenceWhenCalledMultipleTimesShouldReturnSubSequence() { - assertThat(new StringSequence("hamburger").subSequence(4, 8).subSequence(1, 3)).hasToString("rg"); - } - - @Test - void subSequenceWhenEndPastExistingEndShouldThrowException() { - StringSequence sequence = new StringSequence("abcde").subSequence(1, 4); - assertThat(sequence).hasToString("bcd"); - assertThat(sequence.subSequence(2, 3)).hasToString("d"); - assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> sequence.subSequence(3, 4)); - } - - @Test - void subSequenceWhenStartPastExistingEndShouldThrowException() { - StringSequence sequence = new StringSequence("abcde").subSequence(1, 4); - assertThat(sequence).hasToString("bcd"); - assertThat(sequence.subSequence(2, 3)).hasToString("d"); - assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> sequence.subSequence(4, 3)); - } - - @Test - void isEmptyWhenEmptyShouldReturnTrue() { - assertThat(new StringSequence("").isEmpty()).isTrue(); - } - - @Test - void isEmptyWhenNotEmptyShouldReturnFalse() { - assertThat(new StringSequence("x").isEmpty()).isFalse(); - } - - @Test - void lengthShouldReturnLength() { - StringSequence sequence = new StringSequence("hamburger"); - assertThat(sequence).hasSize(9); - assertThat(sequence.subSequence(4, 8)).hasSize(4); - } - - @Test - void charAtShouldReturnChar() { - StringSequence sequence = new StringSequence("hamburger"); - assertThat(sequence.charAt(0)).isEqualTo('h'); - assertThat(sequence.charAt(1)).isEqualTo('a'); - assertThat(sequence.subSequence(4, 8).charAt(0)).isEqualTo('u'); - assertThat(sequence.subSequence(4, 8).charAt(1)).isEqualTo('r'); - } - - @Test - void indexOfCharShouldReturnIndexOf() { - StringSequence sequence = new StringSequence("aabbaacc"); - assertThat(sequence.indexOf('a')).isZero(); - assertThat(sequence.indexOf('b')).isEqualTo(2); - assertThat(sequence.subSequence(2).indexOf('a')).isEqualTo(2); - } - - @Test - void indexOfStringShouldReturnIndexOf() { - StringSequence sequence = new StringSequence("aabbaacc"); - assertThat(sequence.indexOf('a')).isZero(); - assertThat(sequence.indexOf('b')).isEqualTo(2); - assertThat(sequence.subSequence(2).indexOf('a')).isEqualTo(2); - } - - @Test - void indexOfStringFromIndexShouldReturnIndexOf() { - StringSequence sequence = new StringSequence("aabbaacc"); - assertThat(sequence.indexOf("a", 2)).isEqualTo(4); - assertThat(sequence.indexOf("b", 3)).isEqualTo(3); - assertThat(sequence.subSequence(2).indexOf("a", 3)).isEqualTo(3); - } - - @Test - void hashCodeShouldBeSameAsString() { - assertThat(new StringSequence("hamburger")).hasSameHashCodeAs("hamburger"); - assertThat(new StringSequence("hamburger").subSequence(4, 8)).hasSameHashCodeAs("urge"); - } - - @Test - void equalsWhenSameContentShouldMatch() { - StringSequence a = new StringSequence("hamburger").subSequence(4, 8); - StringSequence b = new StringSequence("urge"); - StringSequence c = new StringSequence("urgh"); - assertThat(a).isEqualTo(b).isNotEqualTo(c); - } - - @Test - void notEqualsWhenSequencesOfDifferentLength() { - StringSequence a = new StringSequence("abcd"); - StringSequence b = new StringSequence("ef"); - assertThat(a).isNotEqualTo(b); - } - - @Test - void startsWithWhenExactMatch() { - assertThat(new StringSequence("abc").startsWith("abc")).isTrue(); - } - - @Test - void startsWithWhenLongerAndStartsWith() { - assertThat(new StringSequence("abcd").startsWith("abc")).isTrue(); - } - - @Test - void startsWithWhenLongerAndDoesNotStartWith() { - assertThat(new StringSequence("abcd").startsWith("abx")).isFalse(); - } - - @Test - void startsWithWhenShorterAndDoesNotStartWith() { - assertThat(new StringSequence("ab").startsWith("abc")).isFalse(); - assertThat(new StringSequence("ab").startsWith("c")).isFalse(); - } - - @Test - void startsWithOffsetWhenExactMatch() { - assertThat(new StringSequence("xabc").startsWith("abc", 1)).isTrue(); - } - - @Test - void startsWithOffsetWhenLongerAndStartsWith() { - assertThat(new StringSequence("xabcd").startsWith("abc", 1)).isTrue(); - } - - @Test - void startsWithOffsetWhenLongerAndDoesNotStartWith() { - assertThat(new StringSequence("xabcd").startsWith("abx", 1)).isFalse(); - } - - @Test - void startsWithOffsetWhenShorterAndDoesNotStartWith() { - assertThat(new StringSequence("xab").startsWith("abc", 1)).isFalse(); - assertThat(new StringSequence("xab").startsWith("c", 1)).isFalse(); - } - - @Test - void startsWithOnSubstringTailWhenMatch() { - StringSequence subSequence = new StringSequence("xabc").subSequence(1); - assertThat(subSequence.startsWith("abc")).isTrue(); - assertThat(subSequence.startsWith("abcd")).isFalse(); - } - - @Test - void startsWithOnSubstringMiddleWhenMatch() { - StringSequence subSequence = new StringSequence("xabc").subSequence(1, 3); - assertThat(subSequence.startsWith("ab")).isTrue(); - assertThat(subSequence.startsWith("abc")).isFalse(); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/TestJarMode.java similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/TestJarMode.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/AbstractExecutableArchiveLauncherTests.java similarity index 90% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/AbstractExecutableArchiveLauncherTests.java index 60e3cb2765eb..efdc7012d2ea 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/AbstractExecutableArchiveLauncherTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/AbstractExecutableArchiveLauncherTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.loader; +package org.springframework.boot.loader.launch; import java.io.ByteArrayOutputStream; import java.io.File; @@ -27,9 +27,7 @@ import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Enumeration; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; @@ -39,7 +37,6 @@ import org.junit.jupiter.api.io.TempDir; -import org.springframework.boot.loader.archive.Archive; import org.springframework.util.FileCopyUtils; /** @@ -49,7 +46,7 @@ * @author Madhura Bhave * @author Scott Frederick */ -public abstract class AbstractExecutableArchiveLauncherTests { +abstract class AbstractExecutableArchiveLauncherTests { @TempDir File tempDir; @@ -58,13 +55,11 @@ protected File createJarArchive(String name, String entryPrefix) throws IOExcept return createJarArchive(name, entryPrefix, false, Collections.emptyList()); } - @SuppressWarnings("resource") protected File createJarArchive(String name, String entryPrefix, boolean indexed, List extraLibs) throws IOException { return createJarArchive(name, null, entryPrefix, indexed, extraLibs); } - @SuppressWarnings("resource") protected File createJarArchive(String name, Manifest manifest, String entryPrefix, boolean indexed, List extraLibs) throws IOException { File archive = new File(this.tempDir, name); @@ -129,14 +124,6 @@ protected File explode(File archive) throws IOException { return exploded; } - protected Set getUrls(List archives) throws MalformedURLException { - Set urls = new LinkedHashSet<>(archives.size()); - for (Archive archive : archives) { - urls.add(archive.getUrl()); - } - return urls; - } - protected final URL toUrl(File file) { try { return file.toURI().toURL(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/ArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/ArchiveTests.java new file mode 100644 index 000000000000..900511176f1f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/ArchiveTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2012-2023 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.loader.launch; + +import java.io.File; +import java.security.CodeSource; +import java.security.ProtectionDomain; +import java.util.function.Predicate; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.launch.Archive.Entry; +import org.springframework.boot.loader.testsupport.TestJar; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.withSettings; + +/** + * Tests for {@link Archive}. + * + * @author Phillip Webb + */ +class ArchiveTests { + + @TempDir + File temp; + + @Test + void getClassPathUrlsWithOnlyIncludeFilterSearchesAllDirectories() throws Exception { + Archive archive = mock(Archive.class, withSettings().defaultAnswer(CALLS_REAL_METHODS)); + Predicate includeFilter = (entry) -> false; + archive.getClassPathUrls(includeFilter); + then(archive).should().getClassPathUrls(includeFilter, Archive.ALL_ENTRIES); + } + + @Test + void isExplodedWhenHasRootDirectoryReturnsTrue() { + Archive archive = mock(Archive.class, withSettings().defaultAnswer(CALLS_REAL_METHODS)); + given(archive.getRootDirectory()).willReturn(this.temp); + assertThat(archive.isExploded()).isTrue(); + } + + @Test + void isExplodedWhenHasNoRootDirectoryReturnsFalse() { + Archive archive = mock(Archive.class, withSettings().defaultAnswer(CALLS_REAL_METHODS)); + given(archive.getRootDirectory()).willReturn(null); + assertThat(archive.isExploded()).isFalse(); + } + + @Test + void createFromProtectionDomainCreatesJarArchive() throws Exception { + File jarFile = new File(this.temp, "test.jar"); + TestJar.create(jarFile); + ProtectionDomain protectionDomain = mock(ProtectionDomain.class); + CodeSource codeSource = mock(CodeSource.class); + given(protectionDomain.getCodeSource()).willReturn(codeSource); + given(codeSource.getLocation()).willReturn(jarFile.toURI().toURL()); + Archive archive = Archive.create(protectionDomain); + assertThat(archive).isInstanceOf(JarFileArchive.class); + } + + @Test + void createFromProtectionDomainWhenNoLocationThrowsException() throws Exception { + File jarFile = new File(this.temp, "test.jar"); + TestJar.create(jarFile); + ProtectionDomain protectionDomain = mock(ProtectionDomain.class); + assertThatIllegalStateException().isThrownBy(() -> Archive.create(protectionDomain)) + .withMessage("Unable to determine code source archive"); + } + + @Test + void createFromFileWhenFileDoesNotExistThrowsException() { + File target = new File(this.temp, "missing"); + assertThatIllegalStateException().isThrownBy(() -> Archive.create(target)) + .withMessageContaining("Unable to determine code source archive"); + } + + @Test + void createFromFileWhenJarFileReturnsJarFileArchive() throws Exception { + File target = new File(this.temp, "missing"); + TestJar.create(target); + assertThat(Archive.create(target)).isInstanceOf(JarFileArchive.class); + } + + @Test + void createFromFileWhenDirectoryReturnsExplodedFileArchive() throws Exception { + File target = this.temp; + assertThat(Archive.create(target)).isInstanceOf(ExplodedArchive.class); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/ClassPathIndexFileTests.java similarity index 82% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/ClassPathIndexFileTests.java index 4cd1b4e8d280..4f175b2832b2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ClassPathIndexFileTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/ClassPathIndexFileTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.loader; +package org.springframework.boot.loader.launch; import java.io.File; import java.io.IOException; @@ -28,7 +28,6 @@ import org.junit.jupiter.api.io.TempDir; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** * Tests for {@link ClassPathIndexFile}. @@ -41,24 +40,17 @@ class ClassPathIndexFileTests { @TempDir File temp; - @Test - void loadIfPossibleWhenRootIsNotFileReturnsNull() { - assertThatIllegalArgumentException() - .isThrownBy(() -> ClassPathIndexFile.loadIfPossible(new URL("https://example.com/file"), "test.idx")) - .withMessage("URL does not reference a file"); - } - @Test void loadIfPossibleWhenRootDoesNotExistReturnsNull() throws Exception { File root = new File(this.temp, "missing"); - assertThat(ClassPathIndexFile.loadIfPossible(root.toURI().toURL(), "test.idx")).isNull(); + assertThat(ClassPathIndexFile.loadIfPossible(root, "test.idx")).isNull(); } @Test void loadIfPossibleWhenRootIsDirectoryThrowsException() throws Exception { File root = new File(this.temp, "directory"); root.mkdirs(); - assertThat(ClassPathIndexFile.loadIfPossible(root.toURI().toURL(), "test.idx")).isNull(); + assertThat(ClassPathIndexFile.loadIfPossible(root, "test.idx")).isNull(); } @Test @@ -97,7 +89,7 @@ private URL toUrl(File file) { private ClassPathIndexFile copyAndLoadTestIndexFile() throws IOException { copyTestIndexFile(); - ClassPathIndexFile indexFile = ClassPathIndexFile.loadIfPossible(this.temp.toURI().toURL(), "test.idx"); + ClassPathIndexFile indexFile = ClassPathIndexFile.loadIfPossible(this.temp, "test.idx"); return indexFile; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/ExplodedArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/ExplodedArchiveTests.java new file mode 100755 index 000000000000..96107512a33f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/ExplodedArchiveTests.java @@ -0,0 +1,159 @@ +/* + * Copyright 2012-2023 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.loader.launch; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Enumeration; +import java.util.Set; +import java.util.UUID; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.launch.Archive.Entry; +import org.springframework.boot.loader.testsupport.TestJar; +import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ExplodedArchive}. + * + * @author Phillip Webb + * @author Dave Syer + * @author Andy Wilkinson + */ +@AssertFileChannelDataBlocksClosed +class ExplodedArchiveTests { + + @TempDir + File tempDir; + + private File rootDirectory; + + private ExplodedArchive archive; + + @BeforeEach + void setup() throws Exception { + createArchive(); + } + + @AfterEach + void tearDown() throws Exception { + if (this.archive != null) { + this.archive.close(); + } + } + + @Test + void isExplodedReturnsTrue() { + assertThat(this.archive.isExploded()).isTrue(); + } + + @Test + void getRootDirectoryReturnsRootDirectory() { + assertThat(this.archive.getRootDirectory()).isEqualTo(this.rootDirectory); + } + + @Test + void getManifestReturnsManifest() throws Exception { + assertThat(this.archive.getManifest().getMainAttributes().getValue("Built-By")).isEqualTo("j1"); + } + + @Test + void getClassPathUrlsWhenNoPredicartesReturnsUrls() throws Exception { + Set urls = this.archive.getClassPathUrls(Archive.ALL_ENTRIES); + URL[] expectedUrls = TestJar.expectedEntries().stream().map(this::toUrl).toArray(URL[]::new); + assertThat(urls).containsExactlyInAnyOrder(expectedUrls); + } + + @Test + void getClassPathUrlsWhenHasIncludeFilterReturnsUrls() throws Exception { + Set urls = this.archive.getClassPathUrls(this::entryNameIsNestedJar); + assertThat(urls).containsOnly(toUrl("nested.jar")); + } + + @Test + void getClassPathUrlsWhenHasIncludeFilterAndSpaceInRootNameReturnsUrls() throws Exception { + createArchive("spaces in the name"); + Set urls = this.archive.getClassPathUrls(this::entryNameIsNestedJar); + assertThat(urls).containsOnly(toUrl("nested.jar")); + } + + @Test + void getClassPathUrlsWhenHasSearchFilterReturnsUrls() throws Exception { + Set urls = this.archive.getClassPathUrls(Archive.ALL_ENTRIES, (entry) -> !entry.name().equals("d/")); + assertThat(urls).contains(toUrl("nested.jar")).doesNotContain(toUrl("d/9.dat")); + } + + private void createArchive() throws Exception { + createArchive(null); + } + + private void createArchive(String directoryName) throws Exception { + File file = new File(this.tempDir, "test.jar"); + TestJar.create(file); + this.rootDirectory = (StringUtils.hasText(directoryName) ? new File(this.tempDir, directoryName) + : new File(this.tempDir, UUID.randomUUID().toString())); + try (JarFile jarFile = new JarFile(file)) { + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + File destination = new File(this.rootDirectory.getAbsolutePath() + File.separator + entry.getName()); + destination.getParentFile().mkdirs(); + if (entry.isDirectory()) { + destination.mkdir(); + } + else { + try (InputStream in = jarFile.getInputStream(entry); + OutputStream out = new FileOutputStream(destination)) { + in.transferTo(out); + } + } + } + this.archive = new ExplodedArchive(this.rootDirectory); + } + } + + private URL toUrl(String name) { + return toUrl(new File(this.rootDirectory, name)); + } + + private URL toUrl(File file) { + try { + return file.toURI().toURL(); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + + private boolean entryNameIsNestedJar(Entry entry) { + return entry.name().equals("nested.jar"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/JarFileArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/JarFileArchiveTests.java new file mode 100755 index 000000000000..723890f16546 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/JarFileArchiveTests.java @@ -0,0 +1,172 @@ +/* + * Copyright 2012-2023 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.loader.launch; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.launch.Archive.Entry; +import org.springframework.boot.loader.net.protocol.jar.JarUrl; +import org.springframework.boot.loader.testsupport.TestJar; +import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JarFileArchive}. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @author Camille Vienot + */ +@AssertFileChannelDataBlocksClosed +class JarFileArchiveTests { + + @TempDir + File tempDir; + + private File file; + + private JarFileArchive archive; + + @BeforeEach + void setup() throws Exception { + createTestJarArchive(false); + } + + @AfterEach + void tearDown() throws Exception { + this.archive.close(); + } + + @Test + void isExplodedReturnsFalse() { + assertThat(this.archive.isExploded()).isFalse(); + } + + @Test + void getRootDirectoryReturnsNull() { + assertThat(this.archive.getRootDirectory()).isNull(); + } + + @Test + void getManifestReturnsManifest() throws Exception { + assertThat(this.archive.getManifest().getMainAttributes().getValue("Built-By")).isEqualTo("j1"); + } + + @Test + void getClassPathUrlsWhenNoPredicartesReturnsUrls() throws Exception { + Set urls = this.archive.getClassPathUrls(Archive.ALL_ENTRIES); + URL[] expected = TestJar.expectedEntries() + .stream() + .map((name) -> JarUrl.create(this.file, name)) + .toArray(URL[]::new); + assertThat(urls).containsExactly(expected); + } + + @Test + void getClassPathUrlsWhenHasIncludeFilterReturnsUrls() throws Exception { + Set urls = this.archive.getClassPathUrls(this::entryNameIsNestedJar); + assertThat(urls).containsOnly(JarUrl.create(this.file, "nested.jar")); + } + + @Test + void getClassPathUrlsWhenHasSearchFilterAllUrlsSinceSearchFilterIsNotUsed() throws Exception { + Set urls = this.archive.getClassPathUrls(Archive.ALL_ENTRIES, (entry) -> false); + URL[] expected = TestJar.expectedEntries() + .stream() + .map((name) -> JarUrl.create(this.file, name)) + .toArray(URL[]::new); + assertThat(urls).containsExactly(expected); + } + + @Test + void getClassPathUrlsWhenHasUnpackCommentUnpacksAndReturnsUrls() throws Exception { + createTestJarArchive(true); + Set urls = this.archive.getClassPathUrls(this::entryNameIsNestedJar); + assertThat(urls).hasSize(1); + URL url = urls.iterator().next(); + assertThat(url).isNotEqualTo(JarUrl.create(this.file, "nested.jar")); + assertThat(url.toString()).startsWith("jar:file:").endsWith("/nested.jar!/"); + } + + @Test + void getClassPathUrlsWhenHasUnpackCommentUnpacksToUniqueLocationsPerArchive() throws Exception { + createTestJarArchive(true); + URL firstNestedUrl = this.archive.getClassPathUrls(this::entryNameIsNestedJar).iterator().next(); + createTestJarArchive(true); + URL secondNestedUrl = this.archive.getClassPathUrls(this::entryNameIsNestedJar).iterator().next(); + assertThat(secondNestedUrl).isNotEqualTo(firstNestedUrl); + } + + @Test + void getClassPathUrlsWhenHasUnpackCommentUnpacksAndShareSameParent() throws Exception { + createTestJarArchive(true); + URL nestedUrl = this.archive.getClassPathUrls(this::entryNameIsNestedJar).iterator().next(); + URL anotherNestedUrl = this.archive.getClassPathUrls((entry) -> entry.name().equals("another-nested.jar")) + .iterator() + .next(); + assertThat(nestedUrl.toString()) + .isEqualTo(anotherNestedUrl.toString().replace("another-nested.jar", "nested.jar")); + } + + @Test + void getClassPathUrlsWhenZip64ListsAllEntries() throws Exception { + File file = new File(this.tempDir, "test.jar"); + FileCopyUtils.copy(writeZip64Jar(), file); + try (Archive jarArchive = new JarFileArchive(file)) { + Set urls = jarArchive.getClassPathUrls(Archive.ALL_ENTRIES); + assertThat(urls).hasSize(65537); + } + } + + private byte[] writeZip64Jar() throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (JarOutputStream jarOutput = new JarOutputStream(bytes)) { + for (int i = 0; i < 65537; i++) { + jarOutput.putNextEntry(new JarEntry(i + ".dat")); + jarOutput.closeEntry(); + } + } + return bytes.toByteArray(); + } + + private void createTestJarArchive(boolean unpackNested) throws Exception { + if (this.archive != null) { + this.archive.close(); + } + this.file = new File(this.tempDir, "root.jar"); + TestJar.create(this.file, unpackNested); + this.archive = new JarFileArchive(this.file); + } + + private boolean entryNameIsNestedJar(Entry entry) { + return entry.name().equals("nested.jar"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/JarLauncherTests.java similarity index 69% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/JarLauncherTests.java index afa32a7c4f18..7e231949bea4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/JarLauncherTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/JarLauncherTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.loader; +package org.springframework.boot.loader.launch; import java.io.File; import java.io.FileOutputStream; @@ -23,17 +23,16 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Iterator; import java.util.List; +import java.util.Set; import java.util.jar.Attributes; import java.util.jar.Attributes.Name; import java.util.jar.Manifest; import org.junit.jupiter.api.Test; -import org.springframework.boot.loader.archive.Archive; -import org.springframework.boot.loader.archive.ExplodedArchive; -import org.springframework.boot.loader.archive.JarFileArchive; +import org.springframework.boot.loader.net.protocol.jar.JarUrl; +import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed; import org.springframework.core.io.ClassPathResource; import org.springframework.core.test.tools.SourceFile; import org.springframework.core.test.tools.TestCompiler; @@ -47,19 +46,17 @@ * * @author Andy Wilkinson * @author Madhura Bhave + * @author Phillip Webb */ +@AssertFileChannelDataBlocksClosed class JarLauncherTests extends AbstractExecutableArchiveLauncherTests { @Test void explodedJarHasOnlyBootInfClassesAndContentsOfBootInfLibOnClasspath() throws Exception { File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF")); - JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true)); - List archives = new ArrayList<>(); - launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); - assertThat(getUrls(archives)).containsExactlyInAnyOrder(getExpectedFileUrls(explodedRoot)); - for (Archive archive : archives) { - archive.close(); - } + JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot)); + Set urls = launcher.getClassPathUrls(); + assertThat(urls).containsExactlyInAnyOrder(getExpectedFileUrls(explodedRoot)); } @Test @@ -67,41 +64,33 @@ void archivedJarHasOnlyBootInfClassesAndContentsOfBootInfLibOnClasspath() throws File jarRoot = createJarArchive("archive.jar", "BOOT-INF"); try (JarFileArchive archive = new JarFileArchive(jarRoot)) { JarLauncher launcher = new JarLauncher(archive); - List classPathArchives = new ArrayList<>(); - launcher.getClassPathArchivesIterator().forEachRemaining(classPathArchives::add); - assertThat(classPathArchives).hasSize(4); - assertThat(getUrls(classPathArchives)).containsOnly( - new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/classes!/"), - new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/foo.jar!/"), - new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/bar.jar!/"), - new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/baz.jar!/")); - for (Archive classPathArchive : classPathArchives) { - classPathArchive.close(); - } + Set urls = launcher.getClassPathUrls(); + List expectedUrls = new ArrayList<>(); + expectedUrls.add(JarUrl.create(jarRoot, "BOOT-INF/classes/")); + expectedUrls.add(JarUrl.create(jarRoot, "BOOT-INF/lib/foo.jar")); + expectedUrls.add(JarUrl.create(jarRoot, "BOOT-INF/lib/bar.jar")); + expectedUrls.add(JarUrl.create(jarRoot, "BOOT-INF/lib/baz.jar")); + assertThat(urls).containsOnlyOnceElementsOf(expectedUrls); } } @Test void explodedJarShouldPreserveClasspathOrderWhenIndexPresent() throws Exception { File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF", true, Collections.emptyList())); - JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true)); - Iterator archives = launcher.getClassPathArchivesIterator(); - URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives); - URL[] urls = classLoader.getURLs(); - assertThat(urls).containsExactly(getExpectedFileUrls(explodedRoot)); + JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot)); + URLClassLoader classLoader = createClassLoader(launcher); + assertThat(classLoader.getURLs()).containsExactly(getExpectedFileUrls(explodedRoot)); } @Test void jarFilesPresentInBootInfLibsAndNotInClasspathIndexShouldBeAddedAfterBootInfClasses() throws Exception { ArrayList extraLibs = new ArrayList<>(Arrays.asList("extra-1.jar", "extra-2.jar")); File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF", true, extraLibs)); - JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true)); - Iterator archives = launcher.getClassPathArchivesIterator(); - URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives); - URL[] urls = classLoader.getURLs(); + JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot)); + URLClassLoader classLoader = createClassLoader(launcher); List expectedFiles = getExpectedFilesWithExtraLibs(explodedRoot); URL[] expectedFileUrls = expectedFiles.stream().map(this::toUrl).toArray(URL[]::new); - assertThat(urls).containsExactly(expectedFileUrls); + assertThat(classLoader.getURLs()).containsExactly(expectedFileUrls); } @Test @@ -119,19 +108,22 @@ void explodedJarDefinedPackagesIncludeManifestAttributes() { target.getParentFile().mkdirs(); FileCopyUtils.copy(compiled.getClassLoader().getResourceAsStream("explodedsample/ExampleClass.class"), new FileOutputStream(target)); - JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true)); - Iterator archives = launcher.getClassPathArchivesIterator(); - URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives); + JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot)); + URLClassLoader classLoader = createClassLoader(launcher); Class loaded = classLoader.loadClass("explodedsample.ExampleClass"); assertThat(loaded.getPackage().getImplementationTitle()).isEqualTo("test"); })); } - protected final URL[] getExpectedFileUrls(File explodedRoot) { + private URLClassLoader createClassLoader(JarLauncher launcher) throws Exception { + return (URLClassLoader) launcher.createClassLoader(launcher.getClassPathUrls()); + } + + private URL[] getExpectedFileUrls(File explodedRoot) { return getExpectedFiles(explodedRoot).stream().map(this::toUrl).toArray(URL[]::new); } - protected final List getExpectedFiles(File parent) { + private List getExpectedFiles(File parent) { List expected = new ArrayList<>(); expected.add(new File(parent, "BOOT-INF/classes")); expected.add(new File(parent, "BOOT-INF/lib/foo.jar")); @@ -140,7 +132,7 @@ protected final List getExpectedFiles(File parent) { return expected; } - protected final List getExpectedFilesWithExtraLibs(File parent) { + private List getExpectedFilesWithExtraLibs(File parent) { List expected = new ArrayList<>(); expected.add(new File(parent, "BOOT-INF/classes")); expected.add(new File(parent, "BOOT-INF/lib/extra-1.jar")); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/LaunchedClassLoaderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/LaunchedClassLoaderTests.java new file mode 100644 index 000000000000..d3a92f50c689 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/LaunchedClassLoaderTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2023 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.loader.launch; + +import java.net.URL; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.loader.jarmode.JarMode; +import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LaunchedClassLoader}. + * + * @author Dave Syer + * @author Phillip Webb + * @author Andy Wilkinson + */ +@AssertFileChannelDataBlocksClosed +class LaunchedClassLoaderTests { + + @Test + void loadClassWhenJarModeClassLoadsInLaunchedClassLoader() throws Exception { + try (LaunchedClassLoader classLoader = new LaunchedClassLoader(false, new URL[] {}, + getClass().getClassLoader())) { + Class jarModeClass = classLoader.loadClass(JarMode.class.getName()); + Class jarModeRunnerClass = classLoader.loadClass(JarModeRunner.class.getName()); + assertThat(jarModeClass.getClassLoader()).isSameAs(classLoader); + assertThat(jarModeRunnerClass.getClassLoader()).isSameAs(classLoader); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/LauncherTests.java similarity index 52% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/LauncherTests.java index dec587e18bb2..f300970c6d64 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/LauncherTests.java @@ -14,54 +14,64 @@ * limitations under the License. */ -package org.springframework.boot.loader.jarmode; +package org.springframework.boot.loader.launch; +import java.net.URL; import java.util.Collections; -import java.util.Iterator; +import java.util.Set; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.loader.Launcher; -import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed; import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.testsupport.system.OutputCaptureExtension; import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link Launcher} with jar mode support. + * Tests for {@link Launcher}. * * @author Phillip Webb */ @ExtendWith(OutputCaptureExtension.class) -class LauncherJarModeTests { +@AssertFileChannelDataBlocksClosed +class LauncherTests { + + /** + * Jar Mode tests. + */ + @Nested + class JarMode { + + @BeforeEach + void setup() { + System.setProperty(JarModeRunner.DISABLE_SYSTEM_EXIT, "true"); + } - @BeforeEach - void setup() { - System.setProperty(JarModeLauncher.DISABLE_SYSTEM_EXIT, "true"); - } + @AfterEach + void cleanup() { + System.clearProperty("jarmode"); + System.clearProperty(JarModeRunner.DISABLE_SYSTEM_EXIT); + } - @AfterEach - void cleanup() { - System.clearProperty("jarmode"); - System.clearProperty(JarModeLauncher.DISABLE_SYSTEM_EXIT); - } + @Test + void launchWhenJarModePropertyIsSetLaunchesJarMode(CapturedOutput out) throws Exception { + System.setProperty("jarmode", "test"); + new TestLauncher().launch(new String[] { "boot" }); + assertThat(out).contains("running in test jar mode [boot]"); + } - @Test - void launchWhenJarModePropertyIsSetLaunchesJarMode(CapturedOutput out) throws Exception { - System.setProperty("jarmode", "test"); - new TestLauncher().launch(new String[] { "boot" }); - assertThat(out).contains("running in test jar mode [boot]"); - } + @Test + void launchWhenJarModePropertyIsNotAcceptedThrowsException(CapturedOutput out) throws Exception { + System.setProperty("jarmode", "idontexist"); + new TestLauncher().launch(new String[] { "boot" }); + assertThat(out).contains("Unsupported jarmode 'idontexist'"); + } - @Test - void launchWhenJarModePropertyIsNotAcceptedThrowsException(CapturedOutput out) throws Exception { - System.setProperty("jarmode", "idontexist"); - new TestLauncher().launch(new String[] { "boot" }); - assertThat(out).contains("Unsupported jarmode 'idontexist'"); } private static class TestLauncher extends Launcher { @@ -72,8 +82,13 @@ protected String getMainClass() throws Exception { } @Override - protected Iterator getClassPathArchivesIterator() throws Exception { - return Collections.emptyIterator(); + protected Archive getArchive() { + return null; + } + + @Override + protected Set getClassPathUrls() throws Exception { + return Collections.emptySet(); } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/PropertiesLauncherTests.java similarity index 75% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/PropertiesLauncherTests.java index ab7c296b38b9..6e41ed6f912f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/PropertiesLauncherTests.java @@ -14,20 +14,17 @@ * limitations under the License. */ -package org.springframework.boot.loader; +package org.springframework.boot.loader.launch; import java.io.File; import java.io.FileOutputStream; -import java.io.IOException; -import java.lang.ref.SoftReference; import java.net.URL; import java.net.URLClassLoader; import java.time.Duration; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; +import java.util.Set; import java.util.jar.Attributes; import java.util.jar.Manifest; @@ -39,11 +36,9 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; -import org.springframework.boot.loader.archive.Archive; -import org.springframework.boot.loader.archive.ExplodedArchive; -import org.springframework.boot.loader.archive.JarFileArchive; -import org.springframework.boot.loader.jar.Handler; -import org.springframework.boot.loader.jar.JarFile; +import org.springframework.boot.loader.net.protocol.jar.JarUrl; +import org.springframework.boot.loader.testsupport.TestJar; +import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed; import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.testsupport.system.OutputCaptureExtension; import org.springframework.core.io.FileSystemResource; @@ -51,7 +46,7 @@ import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.hamcrest.Matchers.containsString; /** @@ -61,6 +56,7 @@ * @author Andy Wilkinson */ @ExtendWith(OutputCaptureExtension.class) +@AssertFileChannelDataBlocksClosed class PropertiesLauncherTests { @TempDir @@ -73,9 +69,8 @@ class PropertiesLauncherTests { private CapturedOutput output; @BeforeEach - void setup(CapturedOutput capturedOutput) throws Exception { + void setup(CapturedOutput capturedOutput) { this.contextClassLoader = Thread.currentThread().getContextClassLoader(); - clearHandlerCache(); System.setProperty("loader.home", new File("src/test/resources").getAbsolutePath()); this.output = capturedOutput; } @@ -90,26 +85,13 @@ void close() throws Exception { System.clearProperty("loader.config.location"); System.clearProperty("loader.system"); System.clearProperty("loader.classLoader"); - clearHandlerCache(); if (this.launcher != null) { this.launcher.close(); } } - @SuppressWarnings("unchecked") - private void clearHandlerCache() throws Exception { - Map rootFileCache = ((SoftReference>) ReflectionTestUtils - .getField(Handler.class, "rootFileCache")).get(); - if (rootFileCache != null) { - for (JarFile rootJarFile : rootFileCache.values()) { - rootJarFile.close(); - } - rootFileCache.clear(); - } - } - @Test - void testDefaultHome() { + void testDefaultHome() throws Exception { System.clearProperty("loader.home"); this.launcher = new PropertiesLauncher(); assertThat(this.launcher.getHomeDirectory()).isEqualTo(new File(System.getProperty("user.dir"))); @@ -126,9 +108,8 @@ void testAlternateHome() throws Exception { @Test void testNonExistentHome() { System.setProperty("loader.home", "src/test/resources/nonexistent"); - assertThatIllegalStateException().isThrownBy(PropertiesLauncher::new) - .withMessageContaining("Invalid source directory") - .withCauseInstanceOf(IllegalArgumentException.class); + assertThatIllegalArgumentException().isThrownBy(PropertiesLauncher::new) + .withMessageContaining("Invalid source directory"); } @Test @@ -154,7 +135,7 @@ void testRootOfClasspathFirst() throws Exception { } @Test - void testUserSpecifiedDotPath() { + void testUserSpecifiedDotPath() throws Exception { System.setProperty("loader.path", "."); this.launcher = new PropertiesLauncher(); assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[.]"); @@ -165,9 +146,8 @@ void testUserSpecifiedSlashPath() throws Exception { System.setProperty("loader.path", "jars/"); this.launcher = new PropertiesLauncher(); assertThat(ReflectionTestUtils.getField(this.launcher, "paths")).hasToString("[jars/]"); - List archives = new ArrayList<>(); - this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); - assertThat(archives).areExactly(1, endingWith("app.jar")); + Set urls = this.launcher.getClassPathUrls(); + assertThat(urls).areExactly(1, endingWith("app.jar")); } @Test @@ -196,29 +176,26 @@ void testUserSpecifiedRootOfJarPath() throws Exception { this.launcher = new PropertiesLauncher(); assertThat(ReflectionTestUtils.getField(this.launcher, "paths")) .hasToString("[jar:file:./src/test/resources/nested-jars/app.jar!/]"); - List archives = new ArrayList<>(); - this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); - assertThat(archives).areExactly(1, endingWith("foo.jar!/")); - assertThat(archives).areExactly(1, endingWith("app.jar")); + Set urls = this.launcher.getClassPathUrls(); + assertThat(urls).areExactly(1, endingWith("foo.jar!/")); + assertThat(urls).areExactly(1, endingWith("app.jar!/")); } @Test void testUserSpecifiedRootOfJarPathWithDot() throws Exception { System.setProperty("loader.path", "nested-jars/app.jar!/./"); this.launcher = new PropertiesLauncher(); - List archives = new ArrayList<>(); - this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); - assertThat(archives).areExactly(1, endingWith("foo.jar!/")); - assertThat(archives).areExactly(1, endingWith("app.jar")); + Set urls = this.launcher.getClassPathUrls(); + assertThat(urls).areExactly(1, endingWith("foo.jar!/")); + assertThat(urls).areExactly(1, endingWith("app.jar!/")); } @Test void testUserSpecifiedRootOfJarPathWithDotAndJarPrefix() throws Exception { System.setProperty("loader.path", "jar:file:./src/test/resources/nested-jars/app.jar!/./"); this.launcher = new PropertiesLauncher(); - List archives = new ArrayList<>(); - this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); - assertThat(archives).areExactly(1, endingWith("foo.jar!/")); + Set urls = this.launcher.getClassPathUrls(); + assertThat(urls).areExactly(1, endingWith("foo.jar!/")); } @Test @@ -226,10 +203,9 @@ void testUserSpecifiedJarFileWithNestedArchives() throws Exception { System.setProperty("loader.path", "nested-jars/app.jar"); System.setProperty("loader.main", "demo.Application"); this.launcher = new PropertiesLauncher(); - List archives = new ArrayList<>(); - this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); - assertThat(archives).areExactly(1, endingWith("foo.jar!/")); - assertThat(archives).areExactly(1, endingWith("app.jar")); + Set urls = this.launcher.getClassPathUrls(); + assertThat(urls).areExactly(1, endingWith("foo.jar!/")); + assertThat(urls).areExactly(1, endingWith("app.jar")); } @Test @@ -287,32 +263,21 @@ void testUserSpecifiedClassPathOrder() throws Exception { void testCustomClassLoaderCreation() throws Exception { System.setProperty("loader.classLoader", TestLoader.class.getName()); this.launcher = new PropertiesLauncher(); - ClassLoader loader = this.launcher.createClassLoader(archives()); + ClassLoader loader = this.launcher.createClassLoader(classPathUrls()); assertThat(loader).isNotNull(); assertThat(loader.getClass().getName()).isEqualTo(TestLoader.class.getName()); } - private Iterator archives() throws Exception { - List archives = new ArrayList<>(); - String path = System.getProperty("java.class.path"); - for (String url : path.split(File.pathSeparator)) { - Archive archive = archive(url); - if (archive != null) { - archives.add(archive); + private Set classPathUrls() throws Exception { + Set urls = new LinkedHashSet<>(); + String classPath = System.getProperty("java.class.path"); + for (String path : classPath.split(File.pathSeparator)) { + File file = new FileSystemResource(path).getFile(); + if (file.exists()) { + urls.add(file.toURI().toURL()); } } - return archives.iterator(); - } - - private Archive archive(String url) throws IOException { - File file = new FileSystemResource(url).getFile(); - if (!file.exists()) { - return null; - } - if (url.endsWith(".jar")) { - return new JarFileArchive(file); - } - return new ExplodedArchive(file); + return urls; } @Test @@ -331,7 +296,7 @@ void testSystemPropertySpecifiedMain() throws Exception { } @Test - void testSystemPropertiesSet() { + void testSystemPropertiesSet() throws Exception { System.setProperty("loader.system", "true"); new PropertiesLauncher(); assertThat(System.getProperty("loader.main")).isEqualTo("demo.Application"); @@ -374,17 +339,15 @@ void encodedFileUrlLoaderPathIsHandledCorrectly() throws Exception { loaderPath.mkdir(); System.setProperty("loader.path", loaderPath.toURI().toURL().toString()); this.launcher = new PropertiesLauncher(); - List archives = new ArrayList<>(); - this.launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); - assertThat(archives).hasSize(1); - File archiveRoot = (File) ReflectionTestUtils.getField(archives.get(0), "root"); - assertThat(archiveRoot).isEqualTo(loaderPath); + Set urls = this.launcher.getClassPathUrls(); + assertThat(urls).hasSize(1); + assertThat(urls.iterator().next()).isEqualTo(loaderPath.toURI().toURL()); } @Test // gh-21575 void loadResourceFromJarFile() throws Exception { - File jarFile = new File(this.tempDir, "app.jar"); - TestJarCreator.createTestJar(jarFile); + File file = new File(this.tempDir, "app.jar"); + TestJar.create(file); System.setProperty("loader.home", this.tempDir.getAbsolutePath()); System.setProperty("loader.path", "app.jar"); this.launcher = new PropertiesLauncher(); @@ -393,11 +356,10 @@ void loadResourceFromJarFile() throws Exception { } catch (Exception ex) { // Expected ClassNotFoundException - LaunchedURLClassLoader classLoader = (LaunchedURLClassLoader) Thread.currentThread() - .getContextClassLoader(); + LaunchedClassLoader classLoader = (LaunchedClassLoader) Thread.currentThread().getContextClassLoader(); classLoader.close(); } - URL resource = new URL("jar:" + jarFile.toURI() + "!/nested.jar!/3.dat"); + URL resource = JarUrl.create(file, "nested.jar", "3.dat"); byte[] bytes = FileCopyUtils.copyToByteArray(resource.openStream()); assertThat(bytes).isNotEmpty(); } @@ -406,11 +368,11 @@ private void waitFor(String value) { Awaitility.waitAtMost(Duration.ofSeconds(5)).until(this.output::toString, containsString(value)); } - private Condition endingWith(String value) { + private Condition endingWith(String value) { return new Condition<>() { @Override - public boolean matches(Archive archive) { + public boolean matches(URL archive) { return archive.toString().endsWith(value); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/WarLauncherTests.java similarity index 64% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/WarLauncherTests.java index fbab8d36ed0a..cea89eabe7c7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/WarLauncherTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/WarLauncherTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.loader; +package org.springframework.boot.loader.launch; import java.io.File; import java.net.URL; @@ -22,14 +22,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Iterator; import java.util.List; +import java.util.Set; import org.junit.jupiter.api.Test; -import org.springframework.boot.loader.archive.Archive; -import org.springframework.boot.loader.archive.ExplodedArchive; -import org.springframework.boot.loader.archive.JarFileArchive; +import org.springframework.boot.loader.net.protocol.jar.JarUrl; +import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed; import static org.assertj.core.api.Assertions.assertThat; @@ -38,45 +37,39 @@ * * @author Andy Wilkinson * @author Scott Frederick + * @author Phillip Webb */ +@AssertFileChannelDataBlocksClosed class WarLauncherTests extends AbstractExecutableArchiveLauncherTests { @Test void explodedWarHasOnlyWebInfClassesAndContentsOfWebInfLibOnClasspath() throws Exception { File explodedRoot = explode(createJarArchive("archive.war", "WEB-INF")); - WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true)); - List archives = new ArrayList<>(); - launcher.getClassPathArchivesIterator().forEachRemaining(archives::add); - assertThat(getUrls(archives)).containsExactlyInAnyOrder(getExpectedFileUrls(explodedRoot)); - for (Archive archive : archives) { - archive.close(); - } + WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot)); + Set urls = launcher.getClassPathUrls(); + assertThat(urls).containsExactlyInAnyOrder(getExpectedFileUrls(explodedRoot)); } @Test void archivedWarHasOnlyWebInfClassesAndContentsOfWebInfLibOnClasspath() throws Exception { - File jarRoot = createJarArchive("archive.war", "WEB-INF"); - try (JarFileArchive archive = new JarFileArchive(jarRoot)) { + File file = createJarArchive("archive.war", "WEB-INF"); + try (JarFileArchive archive = new JarFileArchive(file)) { WarLauncher launcher = new WarLauncher(archive); - List classPathArchives = new ArrayList<>(); - launcher.getClassPathArchivesIterator().forEachRemaining(classPathArchives::add); - assertThat(getUrls(classPathArchives)).containsOnly( - new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/classes!/"), - new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/foo.jar!/"), - new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/bar.jar!/"), - new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/baz.jar!/")); - for (Archive classPathArchive : classPathArchives) { - classPathArchive.close(); - } + Set urls = launcher.getClassPathUrls(); + List expected = new ArrayList<>(); + expected.add(JarUrl.create(file, "WEB-INF/classes/")); + expected.add(JarUrl.create(file, "WEB-INF/lib/foo.jar")); + expected.add(JarUrl.create(file, "WEB-INF/lib/bar.jar")); + expected.add(JarUrl.create(file, "WEB-INF/lib/baz.jar")); + assertThat(urls).containsOnly(expected.toArray(URL[]::new)); } } @Test void explodedWarShouldPreserveClasspathOrderWhenIndexPresent() throws Exception { File explodedRoot = explode(createJarArchive("archive.war", "WEB-INF", true, Collections.emptyList())); - WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true)); - Iterator archives = launcher.getClassPathArchivesIterator(); - URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives); + WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot)); + URLClassLoader classLoader = createClassLoader(launcher); URL[] urls = classLoader.getURLs(); assertThat(urls).containsExactly(getExpectedFileUrls(explodedRoot)); } @@ -85,20 +78,23 @@ void explodedWarShouldPreserveClasspathOrderWhenIndexPresent() throws Exception void warFilesPresentInWebInfLibsAndNotInClasspathIndexShouldBeAddedAfterWebInfClasses() throws Exception { ArrayList extraLibs = new ArrayList<>(Arrays.asList("extra-1.jar", "extra-2.jar")); File explodedRoot = explode(createJarArchive("archive.war", "WEB-INF", true, extraLibs)); - WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true)); - Iterator archives = launcher.getClassPathArchivesIterator(); - URLClassLoader classLoader = (URLClassLoader) launcher.createClassLoader(archives); + WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot)); + URLClassLoader classLoader = createClassLoader(launcher); URL[] urls = classLoader.getURLs(); List expectedFiles = getExpectedFilesWithExtraLibs(explodedRoot); URL[] expectedFileUrls = expectedFiles.stream().map(this::toUrl).toArray(URL[]::new); assertThat(urls).containsExactly(expectedFileUrls); } - protected final URL[] getExpectedFileUrls(File explodedRoot) { + private URLClassLoader createClassLoader(Launcher launcher) throws Exception { + return (URLClassLoader) launcher.createClassLoader(launcher.getClassPathUrls()); + } + + private URL[] getExpectedFileUrls(File explodedRoot) { return getExpectedFiles(explodedRoot).stream().map(this::toUrl).toArray(URL[]::new); } - protected final List getExpectedFiles(File parent) { + private List getExpectedFiles(File parent) { List expected = new ArrayList<>(); expected.add(new File(parent, "WEB-INF/classes")); expected.add(new File(parent, "WEB-INF/lib/foo.jar")); @@ -107,7 +103,7 @@ protected final List getExpectedFiles(File parent) { return expected; } - protected final List getExpectedFilesWithExtraLibs(File parent) { + private List getExpectedFilesWithExtraLibs(File parent) { List expected = new ArrayList<>(); expected.add(new File(parent, "WEB-INF/classes")); expected.add(new File(parent, "WEB-INF/lib/extra-1.jar")); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/CanonicalizerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/CanonicalizerTests.java new file mode 100644 index 000000000000..1e59e50d347a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/CanonicalizerTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Canonicalizer}. + * + * @author Phillip Webb + */ +class CanonicalizerTests { + + @Test + void canonicalizeAfterOnlyChangesAfterPos() { + String prefix = "/foo/.././bar/.!/foo/.././bar/."; + String canonicalized = Canonicalizer.canonicalizeAfter(prefix, prefix.indexOf("!/")); + assertThat(canonicalized).isEqualTo("/foo/.././bar/.!/bar/"); + } + + @Test + void canonicalizeWhenHasEmbdeddSlashDotDotSlash() { + assertThat(Canonicalizer.canonicalize("/foo/../bar/bif/bam/../../baz")).isEqualTo("/bar/baz"); + } + + @Test + void canonicalizeWhenHasEmbdeddSlashDotSlash() { + assertThat(Canonicalizer.canonicalize("/foo/./bar/bif/bam/././baz")).isEqualTo("/foo/bar/bif/bam/baz"); + } + + @Test + void canonicalizeWhenHasTrailingSlashDotDot() { + assertThat(Canonicalizer.canonicalize("/foo/bar/baz/../..")).isEqualTo("/foo/"); + } + + @Test + void canonicalizeWhenHasTrailingSlashDot() { + assertThat(Canonicalizer.canonicalize("/foo/bar/baz/./.")).isEqualTo("/foo/bar/baz/"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/HandlerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/HandlerTests.java new file mode 100644 index 000000000000..8d695721158b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/HandlerTests.java @@ -0,0 +1,203 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.net.MalformedURLException; +import java.net.URL; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link Handler}. + * + * @author Andy Wilkinson + * @author Phillip Webb + */ +@AssertFileChannelDataBlocksClosed +class HandlerTests { + + private final Handler handler = new Handler(); + + @Test + void indexOfSeparator() { + String spec = "jar:nested:foo!bar!/some/entry#foo"; + assertThat(Handler.indexOfSeparator(spec, 0, spec.indexOf('#'))).isEqualTo(spec.lastIndexOf("!/")); + } + + @Test + void indexOfSeparatorWhenHasStartAndLimit() { + String spec = "a!/jar:nested:foo!bar!/some/entry#foo!/b"; + int beginIndex = 3; + int endIndex = spec.length() - 4; + String substring = spec.substring(beginIndex, endIndex); + assertThat(Handler.indexOfSeparator(spec, 0, spec.indexOf('#'))) + .isEqualTo(substring.lastIndexOf("!/") + beginIndex); + } + + @Test + void parseUrlWhenAbsoluteParses() throws MalformedURLException { + URL url = createJarUrl(""); + String spec = "jar:file:example.jar!/entry.txt"; + this.handler.parseURL(url, spec, 4, spec.length()); + assertThat(url.toExternalForm()).isEqualTo(spec); + } + + @Test + void parseUrlWhenAbsoluteWithAnchorParses() throws MalformedURLException { + URL url = createJarUrl(""); + String spec = "jar:file:example.jar!/entry.txt"; + this.handler.parseURL(url, spec + "#foo", 4, spec.length()); + assertThat(url.toExternalForm()).isEqualTo(spec + "#foo"); + } + + @Test + void parseUrlWhenAbsoluteWithNoSeparatorThrowsException() throws MalformedURLException { + URL url = createJarUrl(""); + String spec = "jar:file:example.jar!\\entry.txt"; + assertThatIllegalStateException().isThrownBy(() -> this.handler.parseURL(url, spec, 4, spec.length())) + .withMessage("no !/ in spec"); + } + + @Test + void parseUrlWhenAbsoluteWithMalformedInnerUrlThrowsException() throws MalformedURLException { + URL url = createJarUrl(""); + String spec = "jar:example.jar!/entry.txt"; + assertThatIllegalStateException().isThrownBy(() -> this.handler.parseURL(url, spec, 4, spec.length())) + .withMessage( + "invalid url: jar:example.jar!/entry.txt (java.net.MalformedURLException: no protocol: example.jar)"); + } + + @Test + void parseUrlWhenRelativeWithLeadingSlashParses() throws MalformedURLException { + URL url = createJarUrl("file:example.jar!/entry.txt"); + String spec = "/other.txt"; + this.handler.parseURL(url, spec, 0, spec.length()); + assertThat(url.toExternalForm()).isEqualTo("jar:file:example.jar!/other.txt"); + } + + @Test + void parseUrlWhenRelativeWithLeadingSlashAndAnchorParses() throws MalformedURLException { + URL url = createJarUrl("file:example.jar!/entry.txt"); + String spec = "/other.txt"; + this.handler.parseURL(url, spec + "#relative", 0, spec.length()); + assertThat(url.toExternalForm()).isEqualTo("jar:file:example.jar!/other.txt#relative"); + } + + @Test + void parseUrlWhenRelativeWithLeadingSlashAndNoSeparator() throws MalformedURLException { + URL url = createJarUrl("file:example.jar/entry.txt"); + String spec = "/other.txt"; + assertThatIllegalStateException().isThrownBy(() -> this.handler.parseURL(url, spec, 0, spec.length())) + .withMessage("malformed context url:jar:file:example.jar/entry.txt: no !/"); + } + + @Test + void parseUrlWhenRelativeWithoutLeadingSlashParses() throws MalformedURLException { + URL url = createJarUrl("file:example.jar!/foo/"); + String spec = "bar.txt"; + this.handler.parseURL(url, spec, 0, spec.length()); + assertThat(url.toExternalForm()).isEqualTo("jar:file:example.jar!/foo/bar.txt"); + } + + @Test + void parseUrlWhenRelativeWithoutLeadingSlashAndWithoutTrailingSlashParses() throws MalformedURLException { + URL url = createJarUrl("file:example.jar!/foo/baz"); + String spec = "bar.txt"; + this.handler.parseURL(url, spec, 0, spec.length()); + assertThat(url.toExternalForm()).isEqualTo("jar:file:example.jar!/foo/bar.txt"); + } + + @Test + void parseUrlWhenRelativeWithoutLeadingSlashAndWithoutContextSlashThrowsException() throws MalformedURLException { + URL url = createJarUrl("file:example.jar"); + String spec = "bar.txt"; + assertThatIllegalStateException().isThrownBy(() -> this.handler.parseURL(url, spec, 0, spec.length())) + .withMessage("malformed context url:jar:file:example.jar"); + } + + @Test + void parseUrlWhenAnchorOnly() throws MalformedURLException { + URL url = createJarUrl("file:example.jar!/entry.txt"); + String spec = "#runtime"; + this.handler.parseURL(url, spec, 0, 0); + assertThat(url.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt#runtime"); + } + + @Test + void hashCodeGeneratesHashCode() throws MalformedURLException { + URL url = createJarUrl("file:example.jar!/entry.txt"); + assertThat(this.handler.hashCode(url)).isEqualTo(1873709601); + } + + @Test + void hashCodeWhenMalformedInnerUrlGeneratesHashCode() throws MalformedURLException { + URL url = createJarUrl("example.jar!/entry.txt"); + assertThat(this.handler.hashCode(url)).isEqualTo(1870566566); + } + + @Test + void sameFileWhenSameReturnsTrue() throws MalformedURLException { + URL url1 = createJarUrl("file:example.jar!/entry.txt"); + URL url2 = createJarUrl("file:example.jar!/entry.txt"); + assertThat(this.handler.sameFile(url1, url2)).isTrue(); + } + + @Test + void sameFileWhenMissingSeparatorReturnsFalse() throws MalformedURLException { + URL url1 = createJarUrl("file:example.jar!/entry.txt"); + URL url2 = createJarUrl("file:example.jar/entry.txt"); + assertThat(this.handler.sameFile(url1, url2)).isFalse(); + } + + @Test + void sameFileWhenDifferentEntryReturnsFalse() throws MalformedURLException { + URL url1 = createJarUrl("file:example.jar!/entry1.txt"); + URL url2 = createJarUrl("file:example.jar!/entry2.txt"); + assertThat(this.handler.sameFile(url1, url2)).isFalse(); + } + + @Test + void sameFileWhenDifferentInnerUrlReturnsFalse() throws MalformedURLException { + URL url1 = createJarUrl("file:example1.jar!/entry.txt"); + URL url2 = createJarUrl("file:example2.jar!/entry.txt"); + assertThat(this.handler.sameFile(url1, url2)).isFalse(); + } + + @Test + void sameFileWhenSameMalformedInnerUrlReturnsTrue() throws MalformedURLException { + URL url1 = createJarUrl("example.jar!/entry.txt"); + URL url2 = createJarUrl("example.jar!/entry.txt"); + assertThat(this.handler.sameFile(url1, url2)).isTrue(); + } + + @Test + void sameFileWhenDifferentMalformedInnerUrlReturnsFalse() throws MalformedURLException { + URL url1 = createJarUrl("example1.jar!/entry.txt"); + URL url2 = createJarUrl("example2.jar!/entry.txt"); + assertThat(this.handler.sameFile(url1, url2)).isFalse(); + } + + private URL createJarUrl(String file) throws MalformedURLException { + return new URL("jar", null, -1, file, this.handler); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarFileUrlKeyTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarFileUrlKeyTests.java new file mode 100644 index 000000000000..b4131123d53e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarFileUrlKeyTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.net.URL; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.loader.net.protocol.Handlers; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JarFileUrlKey}. + * + * @author Phillip Webb + */ +class JarFileUrlKeyTests { + + @BeforeAll + static void setup() { + Handlers.register(); + } + + @Test + void getCreatesKey() throws Exception { + URL url = new URL("jar:nested:/my.jar/!mynested.jar!/my/path"); + assertThat(JarFileUrlKey.get(url)).isEqualTo("jar:nested:/my.jar/!mynested.jar!/my/path"); + } + + @Test + void getWhenUppercaseProtocolCreatesKey() throws Exception { + URL url = new URL("JAR:nested:/my.jar/!mynested.jar!/my/path"); + assertThat(JarFileUrlKey.get(url)).isEqualTo("jar:nested:/my.jar/!mynested.jar!/my/path"); + } + + @Test + void getWhenHasHostAndPortCreatesKey() throws Exception { + URL url = new URL("https://example.com:1234/test"); + assertThat(JarFileUrlKey.get(url)).isEqualTo("https:example.com:1234/test"); + } + + @Test + void getWhenHasUppercaseHostCreatesKey() throws Exception { + URL url = new URL("https://EXAMPLE.com:1234/test"); + assertThat(JarFileUrlKey.get(url)).isEqualTo("https:example.com:1234/test"); + } + + @Test + void getWhenHasNoPortCreatesKeyWithDefaultPort() throws Exception { + URL url = new URL("https://EXAMPLE.com/test"); + assertThat(JarFileUrlKey.get(url)).isEqualTo("https:example.com:443/test"); + } + + @Test + void getWhenHasNoFileCreatesKey() throws Exception { + URL url = new URL("https://EXAMPLE.com"); + assertThat(JarFileUrlKey.get(url)).isEqualTo("https:example.com:443"); + } + + @Test + void getWhenHasRuntimeRefCreatesKey() throws Exception { + URL url = new URL("jar:nested:/my.jar/!mynested.jar!/my/path#runtime"); + assertThat(JarFileUrlKey.get(url)).isEqualTo("jar:nested:/my.jar/!mynested.jar!/my/path#runtime"); + } + + @Test + void getWhenHasOtherRefCreatesKeyWithoutRef() throws Exception { + URL url = new URL("jar:nested:/my.jar/!mynested.jar!/my/path#example"); + assertThat(JarFileUrlKey.get(url)).isEqualTo("jar:nested:/my.jar/!mynested.jar!/my/path"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlClassLoaderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlClassLoaderTests.java new file mode 100644 index 000000000000..d4eeed8c29b0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlClassLoaderTests.java @@ -0,0 +1,147 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.net.protocol.Handlers; +import org.springframework.boot.loader.testsupport.TestJar; +import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JarUrlClassLoader}. + * + * @author Phillip Webb + */ +@AssertFileChannelDataBlocksClosed +class JarUrlClassLoaderTests { + + private static final URL APP_JAR; + static { + try { + APP_JAR = new URL("jar:file:src/test/resources/jars/app.jar!/"); + } + catch (MalformedURLException ex) { + throw new IllegalStateException(ex); + } + } + + @TempDir + File tempDir; + + @BeforeAll + static void setup() { + Handlers.register(); + } + + @Test + void resolveResourceFromArchive() throws Exception { + try (JarUrlClassLoader loader = new TestJarUrlClassLoader(APP_JAR)) { + assertThat(loader.getResource("demo/Application.java")).isNotNull(); + } + } + + @Test + void resolveResourcesFromArchive() throws Exception { + try (JarUrlClassLoader loader = new TestJarUrlClassLoader(APP_JAR)) { + assertThat(loader.getResources("demo/Application.java").hasMoreElements()).isTrue(); + } + } + + @Test + void resolveRootPathFromArchive() throws Exception { + try (JarUrlClassLoader loader = new TestJarUrlClassLoader(APP_JAR)) { + assertThat(loader.getResource("")).isNotNull(); + } + } + + @Test + void resolveRootResourcesFromArchive() throws Exception { + try (JarUrlClassLoader loader = new TestJarUrlClassLoader(APP_JAR)) { + assertThat(loader.getResources("").hasMoreElements()).isTrue(); + } + } + + @Test + void resolveFromNested() throws Exception { + File jarFile = new File(this.tempDir, "test.jar"); + TestJar.create(jarFile); + URL url = JarUrl.create(jarFile, "nested.jar"); + try (JarUrlClassLoader loader = new TestJarUrlClassLoader(url)) { + URL resource = loader.getResource("3.dat"); + assertThat(resource).hasToString(url + "3.dat"); + try (InputStream input = resource.openConnection().getInputStream()) { + assertThat(input.read()).isEqualTo(3); + } + } + } + + @Test + void loadClass() throws Exception { + try (JarUrlClassLoader loader = new TestJarUrlClassLoader(APP_JAR)) { + assertThat(loader.loadClass("demo.Application")).isNotNull().hasToString("class demo.Application"); + } + } + + @Test + void loadClassFromNested() throws Exception { + File appJar = new File("src/test/resources/jars/app.jar"); + File jarFile = new File(this.tempDir, "test.jar"); + FileOutputStream fileOutputStream = new FileOutputStream(jarFile); + try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) { + JarEntry nestedEntry = new JarEntry("app.jar"); + byte[] nestedJarData = Files.readAllBytes(appJar.toPath()); + nestedEntry.setSize(nestedJarData.length); + nestedEntry.setCompressedSize(nestedJarData.length); + CRC32 crc32 = new CRC32(); + crc32.update(nestedJarData); + nestedEntry.setCrc(crc32.getValue()); + nestedEntry.setMethod(ZipEntry.STORED); + jarOutputStream.putNextEntry(nestedEntry); + jarOutputStream.write(nestedJarData); + jarOutputStream.closeEntry(); + } + URL url = JarUrl.create(jarFile, "app.jar"); + try (JarUrlClassLoader loader = new TestJarUrlClassLoader(url)) { + assertThat(loader.loadClass("demo.Application")).isNotNull().hasToString("class demo.Application"); + } + } + + static class TestJarUrlClassLoader extends JarUrlClassLoader { + + TestJarUrlClassLoader(URL... urls) { + super(urls, JarUrlClassLoaderTests.class.getClassLoader()); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java new file mode 100644 index 000000000000..5d7ccf616b5a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java @@ -0,0 +1,480 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.security.Permission; +import java.util.List; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipEntry; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.net.protocol.Handlers; +import org.springframework.boot.loader.testsupport.TestJar; +import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed; +import org.springframework.boot.loader.zip.ZipContent; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIOException; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +/** + * Tests for {@link JarUrlConnection}. + * + * @author Phillip Webb + */ +@AssertFileChannelDataBlocksClosed +class JarUrlConnectionTests { + + @TempDir + File temp; + + private File file; + + private URL url; + + @BeforeAll + static void registerHandlers() { + Handlers.register(); + } + + @BeforeEach + @AfterEach + void reset() { + JarUrlConnection.clearCache(); + Optimizations.disable(); + } + + @BeforeEach + void setup() throws Exception { + this.file = new File(this.temp, "test.jar"); + TestJar.create(this.file); + this.url = JarUrl.create(this.file, "nested.jar"); + } + + @Test + void getJarFileReturnsJarFile() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(this.url); + JarFile jarFile = connection.getJarFile(); + assertThat(jarFile).isNotNull(); + assertThat(jarFile.getEntry("3.dat")).isNotNull(); + } + + @Test + void getJarEntryReturnsJarEntry() throws Exception { + URL url = JarUrl.create(this.file, "nested.jar", "3.dat"); + JarUrlConnection connection = JarUrlConnection.open(url); + JarEntry entry = connection.getJarEntry(); + assertThat(entry).isNotNull(); + assertThat(entry.getName()).isEqualTo("3.dat"); + } + + @Test + void getJarEntryWhenHasNoEntryNameReturnsNull() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(this.url); + JarEntry entry = connection.getJarEntry(); + assertThat(entry).isNull(); + } + + @Test + void getContentLengthReturnsContentLength() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(this.url); + try (ZipContent content = ZipContent.open(this.file.toPath())) { + int expected = content.getEntry("nested.jar").getUncompressedSize(); + assertThat(connection.getContentLength()).isEqualTo(expected); + } + } + + @Test + void getContentLengthWhenLengthIsLargerThanMaxIntReturnsMinusOne() { + JarUrlConnection connection = mock(JarUrlConnection.class); + given(connection.getContentLength()).willCallRealMethod(); + given(connection.getContentLengthLong()).willReturn((long) Integer.MAX_VALUE + 1); + assertThat(connection.getContentLength()).isEqualTo(-1); + } + + @Test + void getContentLengthLongWhenHasNoEntryReturnsSizeOfJar() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(this.url); + try (ZipContent content = ZipContent.open(this.file.toPath())) { + int expected = content.getEntry("nested.jar").getUncompressedSize(); + assertThat(connection.getContentLengthLong()).isEqualTo(expected); + } + } + + @Test + void getContentLengthLongWhenHasEntryReturnsEntrySize() throws Exception { + URL url = JarUrl.create(this.file, "nested.jar", "3.dat"); + JarUrlConnection connection = JarUrlConnection.open(url); + assertThat(connection.getContentLengthLong()).isEqualTo(1); + } + + @Test + void getContentLengthLongWhenCannotConnectReturnsMinusOne() throws IOException { + JarUrlConnection connection = mock(JarUrlConnection.class); + willThrow(IOException.class).given(connection).connect(); + given(connection.getContentLengthLong()).willCallRealMethod(); + assertThat(connection.getContentLengthLong()).isEqualTo(-1); + } + + @Test + void getContentTypeWhenHasNoEntryReturnsJavaJar() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(this.url); + assertThat(connection.getContentType()).isEqualTo("x-java/jar"); + } + + @Test + void getContentTypeWhenHasKnownStreamReturnsDeducedType() throws Exception { + String content = ""; + try (JarOutputStream out = new JarOutputStream(new FileOutputStream(this.file))) { + out.putNextEntry(new ZipEntry("test.dat")); + out.write(content.getBytes(StandardCharsets.UTF_8)); + out.closeEntry(); + } + JarUrlConnection connection = JarUrlConnection + .open(new URL("jar:file:" + this.file.getAbsolutePath() + "!/test.dat")); + assertThat(connection.getContentType()).isEqualTo("application/xml"); + } + + @Test + void getContentTypeWhenNotKnownInStreamButKnownNameReturnsDeducedType() throws Exception { + String content = "nothinguseful"; + try (JarOutputStream out = new JarOutputStream(new FileOutputStream(this.file))) { + out.putNextEntry(new ZipEntry("test.xml")); + out.write(content.getBytes(StandardCharsets.UTF_8)); + out.closeEntry(); + } + JarUrlConnection connection = JarUrlConnection + .open(new URL("jar:file:" + this.file.getAbsolutePath() + "!/test.xml")); + assertThat(connection.getContentType()).isEqualTo("application/xml"); + } + + @Test + void getContentTypeWhenCannotBeDeducedReturnsContentUnknown() throws Exception { + String content = "nothinguseful"; + try (JarOutputStream out = new JarOutputStream(new FileOutputStream(this.file))) { + out.putNextEntry(new ZipEntry("test.dat")); + out.write(content.getBytes(StandardCharsets.UTF_8)); + out.closeEntry(); + } + JarUrlConnection connection = JarUrlConnection + .open(new URL("jar:file:" + this.file.getAbsolutePath() + "!/test.dat")); + assertThat(connection.getContentType()).isEqualTo("content/unknown"); + } + + @Test + void getHeaderFieldDelegatesToJarFileConnection() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(this.url); + URLConnection jarFileConnection = mock(URLConnection.class); + given(jarFileConnection.getHeaderField("test")).willReturn("test"); + ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection); + assertThat(connection.getHeaderField("test")).isEqualTo("test"); + } + + @Test + void getContentWhenHasEntryReturnsContentFromEntry() throws Exception { + String content = "hello"; + try (JarOutputStream out = new JarOutputStream(new FileOutputStream(this.file))) { + out.putNextEntry(new ZipEntry("test.txt")); + out.write(content.getBytes(StandardCharsets.UTF_8)); + out.closeEntry(); + } + JarUrlConnection connection = JarUrlConnection + .open(new URL("jar:file:" + this.file.getAbsolutePath() + "!/test.txt")); + assertThat(connection.getContent()).isInstanceOf(FilterInputStream.class); + } + + @Test + void getContentWhenHasNoEntryReturnsJarFile() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(this.url); + assertThat(connection.getContent()).isInstanceOf(JarFile.class); + } + + @Test + void getPermissionReturnJarConnectionPermission() throws IOException { + JarUrlConnection connection = JarUrlConnection.open(this.url); + URLConnection jarFileConnection = mock(URLConnection.class); + Permission permission = mock(Permission.class); + given(jarFileConnection.getPermission()).willReturn(permission); + ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection); + assertThat(connection.getPermission()).isSameAs(permission); + } + + @Test + void getInputStreamWhenHasNoEntryThrowsException() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(this.url); + assertThatIOException().isThrownBy(() -> connection.getInputStream()).withMessage("no entry name specified"); + } + + @Test + void getInputStreamWhenOptimizedWithoutReadAndHasCachedJarWithEntryReturnsEmptyInputStream() throws Exception { + JarUrlConnection setupConnection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar")); + setupConnection.connect(); + assertThat(JarUrlConnection.jarFiles.getCached(setupConnection.getJarFileURL())).isNotNull(); + JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "3.dat")); + connection.setUseCaches(false); + Optimizations.enable(false); + assertThat(connection.getInputStream()).isSameAs(JarUrlConnection.emptyInputStream); + } + + @Test + void getInputStreamWhenNoEntryAndOptimzedThrowsException() throws Exception { + JarUrlConnection setupConnection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar")); + setupConnection.connect(); + assertThat(JarUrlConnection.jarFiles.getCached(setupConnection.getJarFileURL())).isNotNull(); + JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "missing.dat")); + Optimizations.enable(false); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(connection::getInputStream) + .isSameAs(JarUrlConnection.FILE_NOT_FOUND_EXCEPTION); + } + + @Test + void getInputStreamWhenNoEntryAndNotOptimzedThrowsException() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "missing.dat")); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(connection::getInputStream) + .withMessageContaining("JAR entry missing.dat not found in"); + } + + @Test + void getInputStreamReturnsInputStream() throws IOException { + JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "3.dat")); + try (InputStream in = connection.getInputStream()) { + assertThat(in).hasBinaryContent(new byte[] { 3 }); + } + } + + @Test + void getInputStreamWhenNoCachedClosesJarFileOnClose() throws IOException { + JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "3.dat")); + connection.setUseCaches(false); + InputStream in = connection.getInputStream(); + JarFile jarFile = (JarFile) ReflectionTestUtils.getField(connection, "jarFile"); + jarFile = spy(jarFile); + ReflectionTestUtils.setField(connection, "jarFile", jarFile); + in.close(); + then(jarFile).should().close(); + } + + @Test + void getAllowUserInteractionDelegatesToJarFileConnection() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(this.url); + URLConnection jarFileConnection = mock(URLConnection.class); + ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection); + given(jarFileConnection.getAllowUserInteraction()).willReturn(true); + assertThat(connection.getAllowUserInteraction()).isTrue(); + then(jarFileConnection).should().getAllowUserInteraction(); + } + + @Test + void setAllowUserInteractionDelegatesToJarFileConnection() throws IOException { + JarUrlConnection connection = JarUrlConnection.open(this.url); + URLConnection jarFileConnection = mock(URLConnection.class); + ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection); + connection.setAllowUserInteraction(true); + then(jarFileConnection).should().setAllowUserInteraction(true); + } + + @Test + void getUseCachesDelegatesToJarFileConnection() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(this.url); + URLConnection jarFileConnection = mock(URLConnection.class); + ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection); + given(jarFileConnection.getUseCaches()).willReturn(true); + assertThat(connection.getUseCaches()).isTrue(); + then(jarFileConnection).should().getUseCaches(); + } + + @Test + void setUseCachesDelegatesToJarFileConnection() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(this.url); + URLConnection jarFileConnection = mock(URLConnection.class); + ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection); + connection.setUseCaches(true); + then(jarFileConnection).should().setUseCaches(true); + } + + @Test + void getDefaultUseCachesDelegatesToJarFileConnection() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(this.url); + URLConnection jarFileConnection = mock(URLConnection.class); + ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection); + given(jarFileConnection.getDefaultUseCaches()).willReturn(true); + assertThat(connection.getDefaultUseCaches()).isTrue(); + then(jarFileConnection).should().getDefaultUseCaches(); + } + + @Test + void setDefaultUseCachesDelegatesToJarFileConnection() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(this.url); + URLConnection jarFileConnection = mock(URLConnection.class); + ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection); + connection.setDefaultUseCaches(true); + then(jarFileConnection).should().setDefaultUseCaches(true); + } + + @Test + void setIfModifiedSinceDelegatesToJarFileConnection() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(this.url); + URLConnection jarFileConnection = mock(URLConnection.class); + ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection); + connection.setIfModifiedSince(123L); + then(jarFileConnection).should().setIfModifiedSince(123L); + } + + @Test + void getRequestPropertyDelegatesToJarFileConnection() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(this.url); + URLConnection jarFileConnection = mock(URLConnection.class); + ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection); + given(jarFileConnection.getRequestProperty("test")).willReturn("test"); + assertThat(connection.getRequestProperty("test")).isEqualTo("test"); + then(jarFileConnection).should().getRequestProperty("test"); + } + + @Test + void setRequestPropertyDelegatesToJarFileConnection() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(this.url); + URLConnection jarFileConnection = mock(URLConnection.class); + ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection); + connection.setRequestProperty("test", "testvalue"); + then(jarFileConnection).should().setRequestProperty("test", "testvalue"); + } + + @Test + void addRequestPropertyDelegatesToJarFileConnection() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(this.url); + URLConnection jarFileConnection = mock(URLConnection.class); + ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection); + connection.addRequestProperty("test", "testvalue"); + then(jarFileConnection).should().addRequestProperty("test", "testvalue"); + } + + @Test + void getRequestPropertiesDelegatesToJarFileConnection() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(this.url); + URLConnection jarFileConnection = mock(URLConnection.class); + ReflectionTestUtils.setField(connection, "jarFileConnection", jarFileConnection); + Map> properties = Map.of("test", List.of("testvalue")); + given(jarFileConnection.getRequestProperties()).willReturn(properties); + assertThat(connection.getRequestProperties()).isEqualTo(properties); + then(jarFileConnection).should().getRequestProperties(); + } + + @Test + void connectWhenConnectedDoesNotReconnect() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(this.url); + connection.connect(); + ReflectionTestUtils.setField(connection, "jarFile", null); + connection.connect(); + assertThat(ReflectionTestUtils.getField(connection, "jarFile")).isNull(); + } + + @Test + void connectWhenHasNotFoundSupplierThrowsException() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "missing.dat")); + assertThat(connection).extracting("notFound").isNotNull(); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(connection::connect) + .withMessageContaining("JAR entry missing.dat not found in"); + } + + @Test + void connectWhenOptimizationsEnabledAndHasCachedJarWithoutEntryThrowsException() throws Exception { + JarUrlConnection setupConnection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar")); + setupConnection.connect(); + assertThat(JarUrlConnection.jarFiles.getCached(setupConnection.getJarFileURL())).isNotNull(); + JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "missing.dat")); + Optimizations.enable(true); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(connection::connect) + .isSameAs(JarUrlConnection.FILE_NOT_FOUND_EXCEPTION); + } + + @Test + void connectWhenHasNoEntryConnects() throws Exception { + JarUrlConnection setupConnection = JarUrlConnection.open(this.url); + setupConnection.connect(); + assertThat(setupConnection.getJarFile()).isNotNull(); + } + + @Test + void connectWhenEntryDoesNotExistAndOptimizationsEnabledThrowsException() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "missing.dat")); + Optimizations.enable(true); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(connection::connect) + .isSameAs(JarUrlConnection.FILE_NOT_FOUND_EXCEPTION); + } + + @Test + void connectWhenEntryDoesNotExistAndNoOptimizationsEnabledThrowsException() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "missing.dat")); + assertThatExceptionOfType(FileNotFoundException.class).isThrownBy(connection::connect) + .withMessageContaining("JAR entry missing.dat not found in"); + } + + @Test + void connectWhenEntryExists() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "3.dat")); + connection.connect(); + assertThat(connection.getJarEntry()).isNotNull(); + } + + @Test + void connectWhenAddedToCacheReconnects() throws IOException { + JarUrlConnection connection = JarUrlConnection.open(this.url); + Object originalConnection = ReflectionTestUtils.getField(connection, "jarFileConnection"); + connection.connect(); + assertThat(connection).extracting("jarFileConnection").isNotSameAs(originalConnection); + } + + @Test + void openWhenNestedAndInCachedWithoutEntryAndOptimzationsEnabledReturnsNoFoundConnection() throws Exception { + JarUrlConnection setupConnection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar")); + setupConnection.connect(); + assertThat(JarUrlConnection.jarFiles.getCached(setupConnection.getJarFileURL())).isNotNull(); + Optimizations.enable(true); + JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "missing.dat")); + assertThat(connection).isSameAs(JarUrlConnection.NOT_FOUND_CONNECTION); + } + + @Test + void openReturnsConnection() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(this.url); + assertThat(connection).isNotNull(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlTests.java new file mode 100644 index 000000000000..082550058e60 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.jar.JarEntry; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JarUrl}. + * + * @author Phillip Webb + */ +class JarUrlTests { + + @TempDir + File temp; + + File jarFile; + + String jarFileUrlPath; + + @BeforeEach + void setup() throws MalformedURLException { + this.jarFile = new File(this.temp, "my.jar"); + this.jarFileUrlPath = this.temp.toURI().toURL().toString().substring("file:".length()); + } + + @Test + void createWithFileReturnsUrl() { + URL url = JarUrl.create(this.temp); + assertThat(url).hasToString("jar:file:%s!/".formatted(this.jarFileUrlPath)); + } + + @Test + void createWithFileAndEntryReturnsUrl() { + JarEntry entry = new JarEntry("lib.jar"); + URL url = JarUrl.create(this.temp, entry); + assertThat(url).hasToString("jar:nested:%s/!lib.jar!/".formatted(this.jarFileUrlPath)); + } + + @Test + void createWithFileAndNullEntryReturnsUrl() { + URL url = JarUrl.create(this.temp, (JarEntry) null); + assertThat(url).hasToString("jar:file:%s!/".formatted(this.jarFileUrlPath)); + } + + @Test + void createWithFileAndNameReturnsUrl() { + URL url = JarUrl.create(this.temp, "lib.jar"); + assertThat(url).hasToString("jar:nested:%s/!lib.jar!/".formatted(this.jarFileUrlPath)); + } + + @Test + void createWithFileAndNullNameReturnsUrl() { + URL url = JarUrl.create(this.temp, (String) null); + assertThat(url).hasToString("jar:file:%s!/".formatted(this.jarFileUrlPath)); + } + + @Test + void createWithFileNameAndPathReturnsUrl() { + URL url = JarUrl.create(this.temp, "lib.jar", "com/example/My.class"); + assertThat(url).hasToString("jar:nested:%s/!lib.jar!/com/example/My.class".formatted(this.jarFileUrlPath)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/LazyDelegatingInputStreamTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/LazyDelegatingInputStreamTests.java new file mode 100644 index 000000000000..f272a6d8aa21 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/LazyDelegatingInputStreamTests.java @@ -0,0 +1,127 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.io.IOException; +import java.io.InputStream; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link LazyDelegatingInputStream}. + * + * @author Phillip Webb + */ +class LazyDelegatingInputStreamTests { + + private InputStream delegate = mock(InputStream.class); + + private TestLazyDelegatingInputStream inputStream = new TestLazyDelegatingInputStream(); + + @Test + void noOperationsDoesNotGetDelegateInputStream() { + then(this.delegate).shouldHaveNoInteractions(); + } + + @Test + void readDelegatesToInputStream() throws Exception { + this.inputStream.read(); + then(this.delegate).should().read(); + } + + @Test + void readWithByteArrayDelegatesToInputStream() throws Exception { + byte[] bytes = new byte[1]; + this.inputStream.read(bytes); + then(this.delegate).should().read(bytes); + } + + @Test + void readWithByteArrayAndOffsetAndLenDelegatesToInputStream() throws Exception { + byte[] bytes = new byte[1]; + this.inputStream.read(bytes, 0, 1); + then(this.delegate).should().read(bytes, 0, 1); + } + + @Test + void skipDelegatesToInputStream() throws Exception { + this.inputStream.skip(10); + then(this.delegate).should().skip(10); + } + + @Test + void availableDelegatesToInputStream() throws Exception { + this.inputStream.available(); + then(this.delegate).should().available(); + } + + @Test + void markSupportedDelegatesToInputStream() { + this.inputStream.markSupported(); + then(this.delegate).should().markSupported(); + } + + @Test + void markDelegatesToInputStream() { + this.inputStream.mark(10); + then(this.delegate).should().mark(10); + } + + @Test + void resetDelegatesToInputStream() throws Exception { + this.inputStream.reset(); + then(this.delegate).should().reset(); + } + + @Test + void closeWhenDelegateNotCreatedDoesNothing() throws Exception { + this.inputStream.close(); + then(this.delegate).shouldHaveNoInteractions(); + } + + @Test + void closeDelegatesToInputStream() throws Exception { + this.inputStream.available(); + this.inputStream.close(); + then(this.delegate).should().close(); + } + + @Test + void getDelegateInputStreamIsOnlyCalledOnce() throws Exception { + this.inputStream.available(); + this.inputStream.mark(10); + this.inputStream.read(); + assertThat(this.inputStream.count).isOne(); + } + + private class TestLazyDelegatingInputStream extends LazyDelegatingInputStream { + + private int count; + + @Override + protected InputStream getDelegateInputStream() throws IOException { + this.count++; + return LazyDelegatingInputStreamTests.this.delegate; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/OptimizationsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/OptimizationsTests.java new file mode 100644 index 000000000000..40afdb813abe --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/OptimizationsTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Optimizations}. + * + * @author Phillip Webb + */ +class OptimizationsTests { + + @AfterEach + void reset() { + Optimizations.disable(); + } + + @Test + void defaultIsNotEnabled() { + assertThat(Optimizations.isEnabled()).isFalse(); + assertThat(Optimizations.isEnabled(true)).isFalse(); + assertThat(Optimizations.isEnabled(false)).isFalse(); + } + + @Test + void enableWithReadContentsEnables() { + Optimizations.enable(true); + assertThat(Optimizations.isEnabled()).isTrue(); + assertThat(Optimizations.isEnabled(true)).isTrue(); + assertThat(Optimizations.isEnabled(false)).isFalse(); + } + + @Test + void enableWithoutReadContentsEnables() { + Optimizations.enable(false); + assertThat(Optimizations.isEnabled()).isTrue(); + assertThat(Optimizations.isEnabled(true)).isFalse(); + assertThat(Optimizations.isEnabled(false)).isTrue(); + } + + @Test + void enableIsByThread() throws InterruptedException { + Optimizations.enable(true); + boolean[] enabled = new boolean[1]; + Thread thread = new Thread(() -> enabled[0] = Optimizations.isEnabled()); + thread.start(); + thread.join(); + assertThat(enabled[0]).isFalse(); + } + + @Test + void disableDisables() { + Optimizations.enable(true); + Optimizations.disable(); + assertThat(Optimizations.isEnabled()).isFalse(); + assertThat(Optimizations.isEnabled(true)).isFalse(); + assertThat(Optimizations.isEnabled(false)).isFalse(); + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarEntryTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarEntryTests.java new file mode 100644 index 000000000000..44d71008f3fa --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarEntryTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.util.jar.Attributes; +import java.util.jar.JarEntry; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link UrlJarEntry}. + * + * @author Phillip Webb + */ +class UrlJarEntryTests { + + @Test + void ofWhenEntryIsNullReturnsNull() { + assertThat(UrlJarEntry.of(null, null)).isNull(); + } + + @Test + void ofReturnsUrlJarEntry() { + JarEntry entry = new JarEntry("test"); + assertThat(UrlJarEntry.of(entry, null)).isNotNull(); + + } + + @Test + void getAttributesDelegatesToUrlJarManifest() throws Exception { + JarEntry entry = new JarEntry("test"); + UrlJarManifest manifest = mock(UrlJarManifest.class); + Attributes attributes = mock(Attributes.class); + given(manifest.getEntryAttributes(any())).willReturn(attributes); + assertThat(UrlJarEntry.of(entry, manifest).getAttributes()).isSameAs(attributes); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactoryTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactoryTests.java new file mode 100644 index 000000000000..5f16d8c6cb78 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactoryTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.URL; +import java.util.function.Consumer; +import java.util.jar.JarFile; + +import com.sun.net.httpserver.HttpServer; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.boot.loader.net.protocol.Handlers; +import org.springframework.boot.loader.testsupport.TestJar; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link UrlJarFileFactory}. + * + * @author Phillip Webb + */ +class UrlJarFileFactoryTests { + + @TempDir + File temp; + + private final UrlJarFileFactory factory = new UrlJarFileFactory(); + + @Mock + private Consumer closeAction; + + @BeforeAll + static void registerHandlers() { + Handlers.register(); + } + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + } + + @Test + void createJarFileWhenLocalFile() throws Throwable { + File file = new File(this.temp, "test.jar"); + TestJar.create(file); + URL url = file.toURI().toURL(); + JarFile jarFile = this.factory.createJarFile(url, this.closeAction); + assertThat(jarFile).isInstanceOf(UrlJarFile.class); + assertThat(jarFile).hasFieldOrPropertyWithValue("closeAction", this.closeAction); + } + + @Test + void createJarFileWhenNested() throws Throwable { + File file = new File(this.temp, "test.jar"); + TestJar.create(file); + URL url = new URL("nested:" + file.getPath() + "/!nested.jar"); + JarFile jarFile = this.factory.createJarFile(url, this.closeAction); + assertThat(jarFile).isInstanceOf(UrlNestedJarFile.class); + assertThat(jarFile).hasFieldOrPropertyWithValue("closeAction", this.closeAction); + } + + @Test + void createJarFileWhenStream() throws Exception { + File file = new File(this.temp, "test.jar"); + TestJar.create(file); + HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); + server.createContext("/test", (exchange) -> { + exchange.sendResponseHeaders(200, file.length()); + try (InputStream in = new FileInputStream(file)) { + in.transferTo(exchange.getResponseBody()); + } + exchange.close(); + }); + server.start(); + try { + URL url = new URL("http://localhost:" + server.getAddress().getPort() + "/test"); + JarFile jarFile = this.factory.createJarFile(url, this.closeAction); + assertThat(jarFile).isInstanceOf(UrlJarFile.class); + assertThat(jarFile).hasFieldOrPropertyWithValue("closeAction", this.closeAction); + } + finally { + server.stop(0); + } + } + + @Test + void createWhenHasRuntimeRef() { + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileTests.java new file mode 100644 index 000000000000..0640483c8c0c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.io.File; +import java.util.function.Consumer; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.boot.loader.testsupport.TestJar; +import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; + +/** + * Tests for {@link UrlJarFile}. + * + * @author Phillip Webb + */ +@AssertFileChannelDataBlocksClosed +class UrlJarFileTests { + + @TempDir + File temp; + + private UrlJarFile jarFile; + + @Mock + private Consumer closeAction; + + @BeforeEach + void setup() throws Exception { + MockitoAnnotations.openMocks(this); + File file = new File(this.temp, "test.jar"); + TestJar.create(file); + this.jarFile = new UrlJarFile(file, Runtime.version(), this.closeAction); + } + + @AfterEach + void cleanup() throws Exception { + this.jarFile.close(); + } + + @Test + void getEntryWhenNotfoundReturnsNull() { + assertThat(this.jarFile.getEntry("missing")).isNull(); + } + + @Test + void getEntryWhenFoundReturnsUrlJarEntry() { + assertThat(this.jarFile.getEntry("1.dat")).isInstanceOf(UrlJarEntry.class); + } + + @Test + void getManifestReturnsNewCopy() throws Exception { + Manifest manifest1 = this.jarFile.getManifest(); + Manifest manifest2 = this.jarFile.getManifest(); + assertThat(manifest1).isNotSameAs(manifest2); + } + + @Test + void closeCallsCloseAction() throws Exception { + this.jarFile.close(); + then(this.closeAction).should().accept(this.jarFile); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFilesTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFilesTests.java new file mode 100644 index 000000000000..f7a6ed089fc5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFilesTests.java @@ -0,0 +1,164 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.io.File; +import java.net.URL; +import java.net.URLConnection; +import java.util.jar.JarFile; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.net.protocol.Handlers; +import org.springframework.boot.loader.testsupport.TestJar; +import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +/** + * Tests for {@link UrlJarFiles}. + * + * @author Phillip Webb + */ +@AssertFileChannelDataBlocksClosed +class UrlJarFilesTests { + + @TempDir + File temp; + + private UrlJarFileFactory factory = mock(UrlJarFileFactory.class); + + private final UrlJarFiles jarFiles = new UrlJarFiles(this.factory); + + private File file; + + private URL url; + + @BeforeAll + static void registerHandlers() { + Handlers.register(); + } + + @BeforeEach + void setup() throws Exception { + this.file = new File(this.temp, "test.jar"); + this.url = new URL("nested:" + this.file.getAbsolutePath() + "/!nested.jar"); + TestJar.create(this.file); + } + + @Test + void getOrCreateWhenNotUsingCachesAlwaysCreatesNewJarFile() throws Exception { + given(this.factory.createJarFile(any(), any())).willCallRealMethod(); + JarFile jarFile1 = this.jarFiles.getOrCreate(false, this.url); + JarFile jarFile2 = this.jarFiles.getOrCreate(false, this.url); + JarFile jarFile3 = this.jarFiles.getOrCreate(false, this.url); + assertThat(jarFile1).isNotSameAs(jarFile2).isNotSameAs(jarFile3); + } + + @Test + void getOrCreateWhenUsingCachingReturnsCachedWhenAvailable() throws Exception { + given(this.factory.createJarFile(any(), any())).willCallRealMethod(); + JarFile jarFile1 = this.jarFiles.getOrCreate(true, this.url); + this.jarFiles.cacheIfAbsent(true, this.url, jarFile1); + JarFile jarFile2 = this.jarFiles.getOrCreate(true, this.url); + JarFile jarFile3 = this.jarFiles.getOrCreate(true, this.url); + assertThat(jarFile1).isSameAs(jarFile2).isSameAs(jarFile3); + } + + @Test + void getCachedWhenNotCachedReturnsNull() { + assertThat(this.jarFiles.getCached(this.url)).isNull(); + } + + @Test + void getCachedWhenCachedReturnsCachedJar() throws Exception { + given(this.factory.createJarFile(any(), any())).willCallRealMethod(); + JarFile jarFile = this.factory.createJarFile(this.url, null); + this.jarFiles.cacheIfAbsent(true, this.url, jarFile); + assertThat(this.jarFiles.getCached(this.url)).isSameAs(jarFile); + } + + @Test + void cacheIfAbsentWhenNotUsingCachesDoesNotCacheAndReturnsFalse() throws Exception { + given(this.factory.createJarFile(any(), any())).willCallRealMethod(); + JarFile jarFile = this.factory.createJarFile(this.url, null); + this.jarFiles.cacheIfAbsent(false, this.url, jarFile); + assertThat(this.jarFiles.getCached(this.url)).isNull(); + } + + @Test + void cacheIfAbsentWhenUsingCachingAndNotAlreadyCachedCachesAndReturnsTrue() throws Exception { + given(this.factory.createJarFile(any(), any())).willCallRealMethod(); + JarFile jarFile = this.factory.createJarFile(this.url, null); + assertThat(this.jarFiles.cacheIfAbsent(true, this.url, jarFile)).isTrue(); + assertThat(this.jarFiles.getCached(this.url)).isSameAs(jarFile); + } + + @Test + void cacheIfAbsentWhenUsingCachingAndAlreadyCachedLeavesCacheAndReturnsFalse() throws Exception { + given(this.factory.createJarFile(any(), any())).willCallRealMethod(); + JarFile jarFile1 = this.factory.createJarFile(this.url, null); + JarFile jarFile2 = this.factory.createJarFile(this.url, null); + assertThat(this.jarFiles.cacheIfAbsent(true, this.url, jarFile1)).isTrue(); + assertThat(this.jarFiles.cacheIfAbsent(true, this.url, jarFile2)).isFalse(); + assertThat(this.jarFiles.getCached(this.url)).isSameAs(jarFile1); + } + + @Test + void closeIfNotCachedWhenNotCachedClosesJarFile() throws Exception { + JarFile jarFile = mock(JarFile.class); + this.jarFiles.closeIfNotCached(this.url, jarFile); + then(jarFile).should().close(); + } + + @Test + void closeIfNotCachedWhenCachedDoesNotCloseJarFile() throws Exception { + JarFile jarFile = mock(JarFile.class); + this.jarFiles.cacheIfAbsent(true, this.url, jarFile); + this.jarFiles.closeIfNotCached(this.url, jarFile); + then(jarFile).should(never()).close(); + } + + @Test + void reconnectReconnectsAndAppliesUseCaches() throws Exception { + JarFile jarFile = mock(JarFile.class); + this.jarFiles.cacheIfAbsent(true, this.url, jarFile); + URLConnection existingConnection = mock(URLConnection.class); + given(existingConnection.getUseCaches()).willReturn(true); + URLConnection connection = this.jarFiles.reconnect(jarFile, existingConnection); + assertThat(connection).isNotSameAs(existingConnection); + assertThat(connection.getUseCaches()).isTrue(); + } + + @Test + void reconnectWhenExistingConnectionIsNullReconnects() throws Exception { + JarFile jarFile = mock(JarFile.class); + this.jarFiles.cacheIfAbsent(true, this.url, jarFile); + URLConnection connection = this.jarFiles.reconnect(jarFile, null); + assertThat(connection).isNotNull(); + assertThat(connection.getUseCaches()).isTrue(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarManifestTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarManifestTests.java new file mode 100644 index 000000000000..be13846ab91e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarManifestTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.io.IOException; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.Manifest; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.loader.net.protocol.jar.UrlJarManifest.ManifestSupplier; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; + +/** + * Tests for {@link UrlJarManifest}. + * + * @author Phillip Webb + */ +class UrlJarManifestTests { + + @Test + void getWhenSuppliedManifestIsNullReturnsNull() throws Exception { + UrlJarManifest urlJarManifest = new UrlJarManifest(() -> null); + assertThat(urlJarManifest.get()).isNull(); + } + + @Test + void getAlwaysReturnsDeepCopy() throws Exception { + Manifest manifest = new Manifest(); + UrlJarManifest urlJarManifest = new UrlJarManifest(() -> manifest); + manifest.getMainAttributes().putValue("test", "one"); + manifest.getEntries().put("spring", new Attributes()); + Manifest copy = urlJarManifest.get(); + assertThat(copy).isNotSameAs(manifest); + manifest.getMainAttributes().clear(); + manifest.getEntries().clear(); + assertThat(copy.getMainAttributes()).isNotEmpty(); + assertThat(copy.getAttributes("spring")).isNotNull(); + } + + @Test + void getEntrtyAttributesWhenSuppliedManifestIsNullReturnsNull() throws Exception { + UrlJarManifest urlJarManifest = new UrlJarManifest(() -> null); + assertThat(urlJarManifest.getEntryAttributes(new JarEntry("test"))).isNull(); + } + + @Test + void getEntryAttributesReturnsDeepCopy() throws Exception { + Manifest manifest = new Manifest(); + UrlJarManifest urlJarManifest = new UrlJarManifest(() -> manifest); + Attributes attributes = new Attributes(); + attributes.putValue("test", "test"); + manifest.getEntries().put("spring", attributes); + Attributes copy = urlJarManifest.getEntryAttributes(new JarEntry("spring")); + assertThat(copy).isNotSameAs(attributes); + attributes.clear(); + assertThat(copy.getValue("test")).isNotNull(); + + } + + @Test + void supplierIsOnlyCalledOnce() throws IOException { + ManifestSupplier supplier = mock(ManifestSupplier.class); + UrlJarManifest urlJarManifest = new UrlJarManifest(supplier); + urlJarManifest.get(); + urlJarManifest.get(); + then(supplier).should(times(1)).getManifest(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlNestedJarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlNestedJarFileTests.java new file mode 100644 index 000000000000..137caca278ee --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlNestedJarFileTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.jar; + +import java.io.File; +import java.util.function.Consumer; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.boot.loader.testsupport.TestJar; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; + +/** + * Tests for {@link UrlNestedJarFile}. + * + * @author Phillip Webb + */ +class UrlNestedJarFileTests { + + @TempDir + File temp; + + private UrlNestedJarFile jarFile; + + @Mock + private Consumer closeAction; + + @BeforeEach + void setup() throws Exception { + MockitoAnnotations.openMocks(this); + File file = new File(this.temp, "test.jar"); + TestJar.create(file); + this.jarFile = new UrlNestedJarFile(file, "multi-release.jar", Runtime.version(), this.closeAction); + } + + @AfterEach + void cleanup() throws Exception { + this.jarFile.close(); + } + + @Test + void getEntryWhenNotfoundReturnsNull() { + assertThat(this.jarFile.getEntry("missing")).isNull(); + } + + @Test + void getEntryWhenFoundReturnsUrlJarEntry() { + assertThat(this.jarFile.getEntry("multi-release.dat")).isInstanceOf(UrlJarEntry.class); + } + + @Test + void getManifestReturnsNewCopy() throws Exception { + Manifest manifest1 = this.jarFile.getManifest(); + Manifest manifest2 = this.jarFile.getManifest(); + assertThat(manifest1).isNotSameAs(manifest2); + } + + @Test + void closeCallsCloseAction() throws Exception { + this.jarFile.close(); + then(this.closeAction).should().accept(this.jarFile); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/HandlerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/HandlerTests.java new file mode 100644 index 000000000000..b6d73394473b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/HandlerTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.nested; + +import java.io.File; +import java.net.URL; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.net.protocol.Handlers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNoException; + +/** + * Tests for {@link Handler}. + * + * @author Phillip Webb + */ +class HandlerTests { + + @TempDir + File temp; + + @BeforeAll + static void registerHandlers() { + Handlers.register(); + } + + @Test + void openConnectionReturnsNestedUrlConnection() throws Exception { + URL url = new URL("nested:" + this.temp.getAbsolutePath() + "/!nested.jar"); + assertThat(url.openConnection()).isInstanceOf(NestedUrlConnection.class); + } + + @Test + void assertUrlIsNotMalformedWhenUrlIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> Handler.assertUrlIsNotMalformed(null)) + .withMessageContaining("'url' must not be null"); + } + + @Test + void assertUrlIsNotMalformedWhenUrlIsNotNestedThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> Handler.assertUrlIsNotMalformed("file:")) + .withMessageContaining("must use 'nested'"); + } + + @Test + void assertUrlIsNotMalformedWhenUrlIsMalformedThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> Handler.assertUrlIsNotMalformed("nested:bad")) + .withMessageContaining("'path' must contain '/!'"); + } + + @Test + void assertUrlIsNotMalformedWhenUrlIsValidDoesNotThrowException() { + String url = "nested:" + this.temp.getAbsolutePath() + "/!nested.jar"; + assertThatNoException().isThrownBy(() -> Handler.assertUrlIsNotMalformed(url)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java new file mode 100644 index 000000000000..0ec9c1f66ed6 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.nested; + +import java.io.File; +import java.net.URL; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.net.protocol.Handlers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link NestedLocation}. + * + * @author Phillip Webb + */ +class NestedLocationTests { + + @TempDir + File temp; + + @BeforeAll + static void registerHandlers() { + Handlers.register(); + } + + @Test + void createWhenFileIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new NestedLocation(null, "nested.jar")) + .withMessageContaining("'file' must not be null"); + } + + @Test + void createWhenNestedEntryNameIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new NestedLocation(new File("test.jar"), null)) + .withMessageContaining("'nestedEntryName' must not be empty"); + } + + @Test + void createWhenNestedEntryNameIsEmptyThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new NestedLocation(new File("test.jar"), null)) + .withMessageContaining("'nestedEntryName' must not be empty"); + } + + @Test + void fromUrlWhenUrlIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> NestedLocation.fromUrl(null)) + .withMessageContaining("'url' must not be null"); + } + + @Test + void fromUrlWhenNotNestedProtocolThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> NestedLocation.fromUrl(new URL("file://test.jar"))) + .withMessageContaining("must use 'nested' protocol"); + } + + @Test + void fromUrlWhenNoPathThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> NestedLocation.fromUrl(new URL("nested:"))) + .withMessageContaining("'path' must not be empty"); + } + + @Test + void fromUrlWhenNoSeparatorThrowsExceptiuon() { + assertThatIllegalArgumentException() + .isThrownBy(() -> NestedLocation.fromUrl(new URL("nested:test.jar!nested.jar"))) + .withMessageContaining("'path' must contain '/!'"); + } + + @Test + void fromUrlReturnsNestedLocation() throws Exception { + File file = new File(this.temp, "test.jar"); + NestedLocation location = NestedLocation + .fromUrl(new URL("nested:" + file.getAbsolutePath() + "/!lib/nested.jar")); + assertThat(location.file()).isEqualTo(file); + assertThat(location.nestedEntryName()).isEqualTo("lib/nested.jar"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionTests.java new file mode 100644 index 000000000000..7efcf2b25726 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionTests.java @@ -0,0 +1,151 @@ +/* + * Copyright 2012-2023 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.loader.net.protocol.nested; + +import java.io.File; +import java.io.FilePermission; +import java.io.InputStream; +import java.lang.ref.Cleaner.Cleanable; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.Permission; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.ArgumentCaptor; + +import org.springframework.boot.loader.net.protocol.Handlers; +import org.springframework.boot.loader.ref.Cleaner; +import org.springframework.boot.loader.testsupport.TestJar; +import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed; +import org.springframework.boot.loader.zip.ZipContent; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link NestedUrlConnection}. + * + * @author Phillip Webb + */ +@AssertFileChannelDataBlocksClosed +class NestedUrlConnectionTests { + + @TempDir + File temp; + + private File jarFile; + + private URL url; + + @BeforeAll + static void registerHandlers() { + Handlers.register(); + } + + @BeforeEach + void setup() throws Exception { + this.jarFile = new File(this.temp, "test.jar"); + TestJar.create(this.jarFile); + this.url = new URL("nested:" + this.jarFile.getAbsolutePath() + "/!nested.jar"); + } + + @Test + void createWhenMalformedUrlThrowsException() throws Exception { + URL url = new URL("nested:bad.jar"); + assertThatExceptionOfType(MalformedURLException.class).isThrownBy(() -> new NestedUrlConnection(url)) + .withMessage("'path' must contain '/!'"); + } + + @Test + void getContentLengthWhenContentLengthMoreThanMaxIntReturnsMinusOne() { + NestedUrlConnection connection = mock(NestedUrlConnection.class); + given(connection.getContentLength()).willCallRealMethod(); + given(connection.getContentLengthLong()).willReturn((long) Integer.MAX_VALUE + 1); + assertThat(connection.getContentLength()).isEqualTo(-1); + } + + @Test + void getContentLengthGetsContentLength() throws Exception { + NestedUrlConnection connection = new NestedUrlConnection(this.url); + try (ZipContent zipContent = ZipContent.open(this.jarFile.toPath())) { + int expectedSize = zipContent.getEntry("nested.jar").getUncompressedSize(); + assertThat(connection.getContentLength()).isEqualTo(expectedSize); + } + } + + @Test + void getContentLengthLongReturnsContentLength() throws Exception { + NestedUrlConnection connection = new NestedUrlConnection(this.url); + try (ZipContent zipContent = ZipContent.open(this.jarFile.toPath())) { + int expectedSize = zipContent.getEntry("nested.jar").getUncompressedSize(); + assertThat(connection.getContentLengthLong()).isEqualTo(expectedSize); + } + } + + @Test + void getContentTypeReturnsJavaJar() throws Exception { + NestedUrlConnection connection = new NestedUrlConnection(this.url); + assertThat(connection.getContentType()).isEqualTo("x-java/jar"); + } + + @Test + void getLastModifiedReturnsFileLastModified() throws Exception { + NestedUrlConnection connection = new NestedUrlConnection(this.url); + assertThat(connection.getLastModified()).isEqualTo(this.jarFile.lastModified()); + } + + @Test + void getPermissionReturnsFilePermission() throws Exception { + NestedUrlConnection connection = new NestedUrlConnection(this.url); + Permission permission = connection.getPermission(); + assertThat(permission).isInstanceOf(FilePermission.class); + assertThat(permission.getName()).isEqualTo(this.jarFile.getCanonicalPath()); + } + + @Test + void getInputStreamReturnsContentOfNestedJar() throws Exception { + NestedUrlConnection connection = new NestedUrlConnection(this.url); + try (InputStream actual = connection.getInputStream()) { + try (ZipContent zipContent = ZipContent.open(this.jarFile.toPath())) { + try (InputStream expected = zipContent.getEntry("nested.jar").openContent().asInputStream()) { + assertThat(actual).hasSameContentAs(expected); + } + } + } + } + + @Test + void inputStreamCloseCleansResource() throws Exception { + Cleaner cleaner = mock(Cleaner.class); + Cleanable cleanable = mock(Cleanable.class); + given(cleaner.register(any(), any())).willReturn(cleanable); + NestedUrlConnection connection = new NestedUrlConnection(this.url, cleaner); + connection.getInputStream().close(); + then(cleanable).should().clean(); + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(Runnable.class); + then(cleaner).should().register(any(), actionCaptor.capture()); + actionCaptor.getValue().run(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/util/UrlDecoderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/util/UrlDecoderTests.java new file mode 100644 index 000000000000..84708a0ba507 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/util/UrlDecoderTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2023 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.loader.net.util; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link UrlDecoder}. + * + * @author Phillip Webb + */ +class UrlDecoderTests { + + @Test + void decodeWhenBasicString() { + assertThat(UrlDecoder.decode("a/b/C.class")).isEqualTo("a/b/C.class"); + } + + @Test + void decodeWhenHasSingleByteEncodedCharacters() { + assertThat(UrlDecoder.decode("%61/%62/%43.class")).isEqualTo("a/b/C.class"); + } + + @Test + void decodeWhenHasDoubleByteEncodedCharacters() { + assertThat(UrlDecoder.decode("%c3%a1/b/C.class")).isEqualTo("\u00e1/b/C.class"); + } + + @Test + void decodeWhenHasMixtureOfEncodedAndUnencodedDoubleByteCharacters() { + assertThat(UrlDecoder.decode("%c3%a1/b/\u00c7.class")).isEqualTo("\u00e1/b/\u00c7.class"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ref/DefaultCleanerTracking.java similarity index 62% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ref/DefaultCleanerTracking.java index 22e04b329c30..8049acd7d037 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/CentralDirectoryVisitor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ref/DefaultCleanerTracking.java @@ -14,21 +14,23 @@ * limitations under the License. */ -package org.springframework.boot.loader.jar; +package org.springframework.boot.loader.ref; -import org.springframework.boot.loader.data.RandomAccessData; +import java.lang.ref.Cleaner.Cleanable; +import java.util.function.Consumer; /** - * Callback visitor triggered by {@link CentralDirectoryParser}. + * Utility that allows tests to set a tracker on {@link DefaultCleaner}. * * @author Phillip Webb */ -interface CentralDirectoryVisitor { +public final class DefaultCleanerTracking { - void visitStart(CentralDirectoryEndRecord endRecord, RandomAccessData centralDirectoryData); + private DefaultCleanerTracking() { + } - void visitFileHeader(CentralDirectoryFileHeader fileHeader, long dataOffset); - - void visitEnd(); + public static void set(Consumer tracker) { + DefaultCleaner.tracker = tracker; + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/testsupport/TestJar.java similarity index 83% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java rename to spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/testsupport/TestJar.java index c5c5fd3b95c9..292fd44b66e1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/TestJarCreator.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/testsupport/TestJar.java @@ -14,12 +14,13 @@ * limitations under the License. */ -package org.springframework.boot.loader; +package org.springframework.boot.loader.testsupport; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.util.List; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; @@ -28,11 +29,11 @@ import java.util.zip.ZipEntry; /** - * Creates a simple test jar. + * Support class to create or get test jars. * * @author Phillip Webb */ -public abstract class TestJarCreator { +public abstract class TestJar { private static final int BASE_VERSION = 8; @@ -50,15 +51,22 @@ public abstract class TestJarCreator { RUNTIME_VERSION = version; } - public static void createTestJar(File file) throws Exception { - createTestJar(file, false); + public static void create(File file) throws Exception { + create(file, false); } - public static void createTestJar(File file, boolean unpackNested) throws Exception { + public static void create(File file, boolean unpackNested) throws Exception { + create(file, unpackNested, false); + } + + public static void create(File file, boolean unpackNested, boolean addSignatureFile) throws Exception { FileOutputStream fileOutputStream = new FileOutputStream(file); try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) { jarOutputStream.setComment("outer"); writeManifest(jarOutputStream, "j1"); + if (addSignatureFile) { + writeEntry(jarOutputStream, "META-INF/some.DSA", 0); + } writeEntry(jarOutputStream, "1.dat", 1); writeEntry(jarOutputStream, "2.dat", 2); writeDirEntry(jarOutputStream, "d/"); @@ -72,6 +80,11 @@ public static void createTestJar(File file, boolean unpackNested) throws Excepti } } + public static List expectedEntries() { + return List.of("META-INF/", "META-INF/MANIFEST.MF", "1.dat", "2.dat", "d/", "d/9.dat", "special/", + "special/\u00EB.dat", "nested.jar", "another-nested.jar", "space nested.jar", "multi-release.jar"); + } + private static void writeNestedEntry(String name, boolean unpackNested, JarOutputStream jarOutputStream) throws Exception { writeNestedEntry(name, unpackNested, jarOutputStream, false); @@ -148,4 +161,14 @@ private static void writeEntry(JarOutputStream jarOutputStream, String name, int jarOutputStream.closeEntry(); } + public static File getSigned() { + String[] entries = System.getProperty("java.class.path").split(System.getProperty("path.separator")); + for (String entry : entries) { + if (entry.contains("bcprov")) { + return new File(entry); + } + } + return null; + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java deleted file mode 100644 index 802a762e79dd..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/util/SystemPropertyUtilsTests.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2012-2023 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.loader.util; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link SystemPropertyUtils}. - * - * @author Dave Syer - */ -class SystemPropertyUtilsTests { - - @BeforeEach - void init() { - System.setProperty("foo", "bar"); - } - - @AfterEach - void close() { - System.clearProperty("foo"); - } - - @Test - void testVanillaPlaceholder() { - assertThat(SystemPropertyUtils.resolvePlaceholders("${foo}")).isEqualTo("bar"); - } - - @Test - void testDefaultValue() { - assertThat(SystemPropertyUtils.resolvePlaceholders("${bar:foo}")).isEqualTo("foo"); - } - - @Test - void testNestedPlaceholder() { - assertThat(SystemPropertyUtils.resolvePlaceholders("${bar:${spam:foo}}")).isEqualTo("foo"); - } - - @Test - void testEnvVar() { - assertThat(SystemPropertyUtils.getProperty("lang")).isEqualTo(System.getenv("LANG")); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/AssertFileChannelDataBlocksClosed.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/AssertFileChannelDataBlocksClosed.java new file mode 100644 index 000000000000..75c208e5853f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/AssertFileChannelDataBlocksClosed.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Annotation that can be added to tests to assert that {@link FileChannelDataBlock} files + * are not left open. + * + * @author Phillip Webb + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ExtendWith(AssertFileChannelDataBlocksClosedExtension.class) +public @interface AssertFileChannelDataBlocksClosed { + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/AssertFileChannelDataBlocksClosedExtension.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/AssertFileChannelDataBlocksClosedExtension.java new file mode 100644 index 000000000000..caa8d23e2162 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/AssertFileChannelDataBlocksClosedExtension.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.lang.ref.Cleaner.Cleanable; +import java.nio.channels.FileChannel; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +import org.springframework.boot.loader.ref.DefaultCleanerTracking; +import org.springframework.boot.loader.zip.FileChannelDataBlock.Tracker; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Extension for {@link AssertFileChannelDataBlocksClosed @TrackFileChannelDataBlock}. + */ +class AssertFileChannelDataBlocksClosedExtension implements BeforeEachCallback, AfterEachCallback { + + private static OpenFilesTracker tracker = new OpenFilesTracker(); + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + tracker.clear(); + FileChannelDataBlock.tracker = tracker; + DefaultCleanerTracking.set(tracker::addedCleanable); + } + + @Override + public void afterEach(ExtensionContext context) throws Exception { + tracker.assertAllClosed(); + FileChannelDataBlock.tracker = null; + } + + private static class OpenFilesTracker implements Tracker { + + private final Set paths = new LinkedHashSet<>(); + + private final List cleanup = new ArrayList<>(); + + @Override + public void openedFileChannel(Path path, FileChannel fileChannel) { + this.paths.add(path); + } + + @Override + public void closedFileChannel(Path path, FileChannel fileChannel) { + this.paths.remove(path); + } + + void clear() { + this.paths.clear(); + this.cleanup.clear(); + } + + void assertAllClosed() { + this.cleanup.forEach(Cleanable::clean); + assertThat(this.paths).as("open paths").isEmpty(); + } + + private void addedCleanable(Cleanable cleanable) { + this.cleanup.add(cleanable); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ByteArrayDataBlockTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ByteArrayDataBlockTests.java new file mode 100644 index 000000000000..7c78ec4276fb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ByteArrayDataBlockTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.nio.ByteBuffer; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ByteArrayDataBlock}. + * + * @author Phillip Webb + */ +class ByteArrayDataBlockTests { + + private final byte[] BYTES = { 0, 1, 2, 3, 4, 5, 6, 7 }; + + @Test + void sizeReturnsByteArrayLength() throws Exception { + ByteArrayDataBlock dataBlock = new ByteArrayDataBlock(this.BYTES); + assertThat(dataBlock.size()).isEqualTo(this.BYTES.length); + } + + @Test + void readPutsBytes() throws Exception { + ByteArrayDataBlock dataBlock = new ByteArrayDataBlock(this.BYTES); + ByteBuffer dst = ByteBuffer.allocate(8); + int result = dataBlock.read(dst, 0); + assertThat(result).isEqualTo(8); + assertThat(dst.array()).containsExactly(this.BYTES); + } + + @Test + void readWhenLessBytesThanRemainingInBufferPutsBytes() throws Exception { + ByteArrayDataBlock dataBlock = new ByteArrayDataBlock(this.BYTES); + ByteBuffer dst = ByteBuffer.allocate(9); + int result = dataBlock.read(dst, 0); + assertThat(result).isEqualTo(8); + assertThat(dst.array()).containsExactly(0, 1, 2, 3, 4, 5, 6, 7, 0); + } + + @Test + void readWhenLessRemainingInBufferThanLengthPutsBytes() throws Exception { + ByteArrayDataBlock dataBlock = new ByteArrayDataBlock(this.BYTES); + ByteBuffer dst = ByteBuffer.allocate(7); + int result = dataBlock.read(dst, 0); + assertThat(result).isEqualTo(7); + assertThat(dst.array()).containsExactly(0, 1, 2, 3, 4, 5, 6); + } + + @Test + void readWhenHasPosOffsetReadsBytes() throws Exception { + ByteArrayDataBlock dataBlock = new ByteArrayDataBlock(this.BYTES); + ByteBuffer dst = ByteBuffer.allocate(3); + int result = dataBlock.read(dst, 4); + assertThat(result).isEqualTo(3); + assertThat(dst.array()).containsExactly(4, 5, 6); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/DataBlockTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/DataBlockTests.java new file mode 100644 index 000000000000..f238059a3400 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/DataBlockTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; +import org.mockito.stubbing.Answer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.withSettings; + +/** + * Tests for {@link DataBlock}. + * + * @author Phillip Webb + */ +class DataBlockTests { + + @Test + void readFullyReadsAllBytesByCallingReadMultipleTimes() throws IOException { + DataBlock dataBlock = mock(DataBlock.class, withSettings().defaultAnswer(CALLS_REAL_METHODS)); + given(dataBlock.read(any(), anyLong())) + .will(putBytes(new byte[] { 0, 1 }, new byte[] { 2 }, new byte[] { 3, 4, 5 })); + ByteBuffer dst = ByteBuffer.allocate(6); + dataBlock.readFully(dst, 0); + assertThat(dst.array()).containsExactly(0, 1, 2, 3, 4, 5); + } + + private Answer putBytes(byte[]... bytes) { + AtomicInteger count = new AtomicInteger(); + return (invocation) -> { + int index = count.getAndIncrement(); + invocation.getArgument(0, ByteBuffer.class).put(bytes[index]); + return bytes.length; + }; + } + + @Test + void readFullyWhenReadReturnsNegativeResultThrowsException() throws Exception { + DataBlock dataBlock = mock(DataBlock.class, withSettings().defaultAnswer(CALLS_REAL_METHODS)); + given(dataBlock.read(any(), anyLong())).willReturn(-1); + ByteBuffer dst = ByteBuffer.allocate(8); + assertThatExceptionOfType(EOFException.class).isThrownBy(() -> dataBlock.readFully(dst, 0)); + } + + @Test + void asInputStreamReturnsDataBlockInputStream() { + DataBlock dataBlock = mock(DataBlock.class, withSettings().defaultAnswer(CALLS_REAL_METHODS)); + assertThat(dataBlock.asInputStream()).isInstanceOf(DataBlockInputStream.class); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockTests.java new file mode 100644 index 000000000000..9beb4aa314d0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockTests.java @@ -0,0 +1,238 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.zip.FileChannelDataBlock.Tracker; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link FileChannelDataBlock}. + * + * @author Phillip Webb + */ +class FileChannelDataBlockTests { + + private static final byte[] CONTENT = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 }; + + @TempDir + File tempDir; + + File tempFile; + + @BeforeEach + void writeTempFile() throws IOException { + this.tempFile = new File(this.tempDir, "content"); + Files.write(this.tempFile.toPath(), CONTENT); + } + + @AfterEach + void resetTracker() { + FileChannelDataBlock.tracker = null; + } + + @Test + void sizeReturnsFileSize() throws IOException { + try (FileChannelDataBlock block = createAndOpenBlock()) { + assertThat(block.size()).isEqualTo(CONTENT.length); + } + } + + @Test + void readReadsFile() throws IOException { + try (FileChannelDataBlock block = createAndOpenBlock()) { + ByteBuffer buffer = ByteBuffer.allocate(CONTENT.length); + assertThat(block.read(buffer, 0)).isEqualTo(6); + assertThat(buffer.array()).containsExactly(CONTENT); + } + } + + @Test + void readDoesNotReadPastEndOfFile() throws IOException { + try (FileChannelDataBlock block = createAndOpenBlock()) { + ByteBuffer buffer = ByteBuffer.allocate(CONTENT.length); + assertThat(block.read(buffer, 2)).isEqualTo(4); + assertThat(buffer.array()).containsExactly(0x02, 0x03, 0x04, 0x05, 0x0, 0x0); + } + } + + @Test + void readWhenPosAtSizeReturnsMinusOne() throws IOException { + try (FileChannelDataBlock block = createAndOpenBlock()) { + ByteBuffer buffer = ByteBuffer.allocate(CONTENT.length); + assertThat(block.read(buffer, 6)).isEqualTo(-1); + } + } + + @Test + void readWhenPosOverSizeReturnsMinusOne() throws IOException { + try (FileChannelDataBlock block = createAndOpenBlock()) { + ByteBuffer buffer = ByteBuffer.allocate(CONTENT.length); + assertThat(block.read(buffer, 7)).isEqualTo(-1); + } + } + + @Test + void readWhenPosIsNegativeThrowsException() throws IOException { + try (FileChannelDataBlock block = createAndOpenBlock()) { + ByteBuffer buffer = ByteBuffer.allocate(CONTENT.length); + assertThatIllegalArgumentException().isThrownBy(() -> block.read(buffer, -1)); + } + } + + @Test + void sliceWhenOffsetIsNegativeThrowsException() throws IOException { + try (FileChannelDataBlock block = createAndOpenBlock()) { + assertThatIllegalArgumentException().isThrownBy(() -> block.slice(-1, 0)) + .withMessage("Offset must not be negative"); + } + } + + @Test + void sliceWhenSizeIsNegativeThrowsException() throws IOException { + try (FileChannelDataBlock block = createAndOpenBlock()) { + assertThatIllegalArgumentException().isThrownBy(() -> block.slice(0, -1)) + .withMessage("Size must not be negative and must be within bounds"); + } + } + + @Test + void sliceWhenSizeIsOutOfBoundsThrowsException() throws IOException { + try (FileChannelDataBlock block = createAndOpenBlock()) { + assertThatIllegalArgumentException().isThrownBy(() -> block.slice(2, 5)) + .withMessage("Size must not be negative and must be within bounds"); + } + } + + @Test + void sliceReturnsSlice() throws IOException { + try (FileChannelDataBlock slice = createAndOpenBlock().slice(1, 4)) { + assertThat(slice.size()).isEqualTo(4); + ByteBuffer buffer = ByteBuffer.allocate(4); + assertThat(slice.read(buffer, 0)).isEqualTo(4); + assertThat(buffer.array()).containsExactly(0x01, 0x02, 0x03, 0x04); + } + } + + @Test + void openAndCloseHandleReferenceCounting() throws IOException { + TestTracker tracker = new TestTracker(); + FileChannelDataBlock.tracker = tracker; + FileChannelDataBlock block = createBlock(); + assertThat(block).extracting("channel.referenceCount").isEqualTo(0); + tracker.assertOpenCloseCounts(0, 0); + block.open(); + assertThat(block).extracting("channel.referenceCount").isEqualTo(1); + tracker.assertOpenCloseCounts(1, 0); + block.open(); + assertThat(block).extracting("channel.referenceCount").isEqualTo(2); + tracker.assertOpenCloseCounts(1, 0); + block.close(); + assertThat(block).extracting("channel.referenceCount").isEqualTo(1); + tracker.assertOpenCloseCounts(1, 0); + block.close(); + assertThat(block).extracting("channel.referenceCount").isEqualTo(0); + tracker.assertOpenCloseCounts(1, 1); + block.open(); + assertThat(block).extracting("channel.referenceCount").isEqualTo(1); + tracker.assertOpenCloseCounts(2, 1); + block.close(); + assertThat(block).extracting("channel.referenceCount").isEqualTo(0); + tracker.assertOpenCloseCounts(2, 2); + } + + @Test + void openAndCloseSliceHandleReferenceCounting() throws IOException { + TestTracker tracker = new TestTracker(); + FileChannelDataBlock.tracker = tracker; + FileChannelDataBlock block = createBlock(); + FileChannelDataBlock slice = block.slice(1, 4); + assertThat(block).extracting("channel.referenceCount").isEqualTo(0); + tracker.assertOpenCloseCounts(0, 0); + block.open(); + assertThat(block).extracting("channel.referenceCount").isEqualTo(1); + tracker.assertOpenCloseCounts(1, 0); + slice.open(); + assertThat(slice).extracting("channel.referenceCount").isEqualTo(2); + tracker.assertOpenCloseCounts(1, 0); + slice.open(); + assertThat(slice).extracting("channel.referenceCount").isEqualTo(3); + tracker.assertOpenCloseCounts(1, 0); + slice.close(); + assertThat(slice).extracting("channel.referenceCount").isEqualTo(2); + tracker.assertOpenCloseCounts(1, 0); + slice.close(); + assertThat(slice).extracting("channel.referenceCount").isEqualTo(1); + tracker.assertOpenCloseCounts(1, 0); + block.close(); + assertThat(block).extracting("channel.referenceCount").isEqualTo(0); + tracker.assertOpenCloseCounts(1, 1); + slice.open(); + assertThat(slice).extracting("channel.referenceCount").isEqualTo(1); + tracker.assertOpenCloseCounts(2, 1); + slice.close(); + assertThat(slice).extracting("channel.referenceCount").isEqualTo(0); + tracker.assertOpenCloseCounts(2, 2); + } + + private FileChannelDataBlock createAndOpenBlock() throws IOException { + FileChannelDataBlock block = createBlock(); + block.open(); + return block; + } + + private FileChannelDataBlock createBlock() throws IOException { + return new FileChannelDataBlock(this.tempFile.toPath()); + } + + static class TestTracker implements Tracker { + + private int openCount; + + private int closeCount; + + @Override + public void openedFileChannel(Path path, FileChannel fileChannel) { + this.openCount++; + } + + @Override + public void closedFileChannel(Path path, FileChannel fileChannel) { + this.closeCount++; + } + + void assertOpenCloseCounts(int expectedOpenCount, int expectedCloseCount) { + assertThat(this.openCount).as("openCount").isEqualTo(expectedOpenCount); + assertThat(this.closeCount).as("closeCount").isEqualTo(expectedCloseCount); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/VirtualDataBlockTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/VirtualDataBlockTests.java new file mode 100644 index 000000000000..c2b8c8338392 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/VirtualDataBlockTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link VirtualDataBlock}. + * + * @author Phillip Webb + */ +class VirtualDataBlockTests { + + private VirtualDataBlock virtualDataBlock; + + @BeforeEach + void setup() throws IOException { + List subsections = new ArrayList<>(); + subsections.add(new ByteArrayDataBlock("abc".getBytes(StandardCharsets.UTF_8))); + subsections.add(new ByteArrayDataBlock("defg".getBytes(StandardCharsets.UTF_8))); + subsections.add(new ByteArrayDataBlock("h".getBytes(StandardCharsets.UTF_8))); + this.virtualDataBlock = new VirtualDataBlock(subsections); + } + + @Test + void sizeReturnsSize() throws IOException { + assertThat(this.virtualDataBlock.size()).isEqualTo(8); + } + + @Test + void readFullyReadsAllBlocks() throws IOException { + ByteBuffer dst = ByteBuffer.allocate((int) this.virtualDataBlock.size()); + this.virtualDataBlock.readFully(dst, 0); + assertThat(dst.array()).containsExactly("abcdefgh".getBytes(StandardCharsets.UTF_8)); + } + + @Test + void readWithShortBlock() throws IOException { + ByteBuffer dst = ByteBuffer.allocate(2); + assertThat(this.virtualDataBlock.read(dst, 1)).isEqualTo(2); + assertThat(dst.array()).containsExactly("bc".getBytes(StandardCharsets.UTF_8)); + } + + @Test + void readWithShortBlockAcrossSubsections() throws IOException { + ByteBuffer dst = ByteBuffer.allocate(3); + assertThat(this.virtualDataBlock.read(dst, 2)).isEqualTo(3); + assertThat(dst.array()).containsExactly("cde".getBytes(StandardCharsets.UTF_8)); + } + + @Test + void readWithBigBlock() throws IOException { + ByteBuffer dst = ByteBuffer.allocate(16); + assertThat(this.virtualDataBlock.read(dst, 1)).isEqualTo(7); + assertThat(dst.array()).startsWith("bcdefgh".getBytes(StandardCharsets.UTF_8)); + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/VirtualZipDataBlockTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/VirtualZipDataBlockTests.java new file mode 100644 index 000000000000..42ea979673ca --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/VirtualZipDataBlockTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.testsupport.TestJar; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link VirtualZipDataBlock}. + * + * @author Phillip Webb + */ +@AssertFileChannelDataBlocksClosed +class VirtualZipDataBlockTests { + + @TempDir + File tempDir; + + private File file; + + @BeforeEach + void setup() throws Exception { + this.file = new File(this.tempDir, "test.jar"); + TestJar.create(this.file); + } + + @Test + void createContainsValidZipContent() throws IOException { + FileChannelDataBlock data = new FileChannelDataBlock(this.file.toPath()); + data.open(); + List centralRecords = new ArrayList<>(); + List centralRecordPositions = new ArrayList<>(); + ZipEndOfCentralDirectoryRecord eocd = ZipEndOfCentralDirectoryRecord.load(data).endOfCentralDirectoryRecord(); + long pos = eocd.offsetToStartOfCentralDirectory(); + for (int i = 0; i < eocd.totalNumberOfCentralDirectoryEntries(); i++) { + ZipCentralDirectoryFileHeaderRecord centralRecord = ZipCentralDirectoryFileHeaderRecord.load(data, pos); + String name = ZipString.readString(data, pos + ZipCentralDirectoryFileHeaderRecord.FILE_NAME_OFFSET, + centralRecord.fileNameLength()); + if (name.endsWith(".jar")) { + centralRecords.add(centralRecord); + centralRecordPositions.add(pos); + } + pos += centralRecord.size(); + } + NameOffsetLookups nameOffsetLookups = new NameOffsetLookups(2, centralRecords.size()); + for (int i = 0; i < centralRecords.size(); i++) { + nameOffsetLookups.enable(i, true); + } + nameOffsetLookups.enable(0, true); + File outputFile = new File(this.tempDir, "out.jar"); + try (VirtualZipDataBlock block = new VirtualZipDataBlock(data, nameOffsetLookups, + centralRecords.toArray(ZipCentralDirectoryFileHeaderRecord[]::new), + centralRecordPositions.stream().mapToLong(Long::longValue).toArray())) { + try (FileOutputStream out = new FileOutputStream(outputFile)) { + block.asInputStream().transferTo(out); + } + } + try (FileSystem fileSystem = FileSystems.newFileSystem(outputFile.toPath())) { + assertThatExceptionOfType(NoSuchFileException.class) + .isThrownBy(() -> Files.size(fileSystem.getPath("nessted.jar"))); + assertThat(Files.size(fileSystem.getPath("sted.jar"))).isGreaterThan(0); + assertThat(Files.size(fileSystem.getPath("other-nested.jar"))).isGreaterThan(0); + assertThat(Files.size(fileSystem.getPath("ace nested.jar"))).isGreaterThan(0); + assertThat(Files.size(fileSystem.getPath("lti-release.jar"))).isGreaterThan(0); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/Zip64EndOfCentralDirectoryLocatorTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/Zip64EndOfCentralDirectoryLocatorTests.java new file mode 100644 index 000000000000..78b5a00498ce --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/Zip64EndOfCentralDirectoryLocatorTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Zip64EndOfCentralDirectoryLocator}. + * + * @author Phillip Webb + */ +class Zip64EndOfCentralDirectoryLocatorTests { + + @Test + void findReturnsRecord() throws Exception { + DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { // + 0x50, 0x4b, 0x06, 0x07, // + 0x01, 0x00, 0x00, 0x00, // + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x03, 0x00, 0x00, 0x00 }); // + Zip64EndOfCentralDirectoryLocator eocd = Zip64EndOfCentralDirectoryLocator.find(dataBlock, 20); + assertThat(eocd.pos()).isEqualTo(0); + assertThat(eocd.numberOfThisDisk()).isEqualTo(1); + assertThat(eocd.offsetToZip64EndOfCentralDirectoryRecord()).isEqualTo(2); + assertThat(eocd.totalNumberOfDisks()).isEqualTo(3); + } + + @Test + void findWhenSignatureDoesNotMatchReturnsNull() throws IOException { + DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { // + 0x51, 0x4b, 0x06, 0x07, // + 0x01, 0x00, 0x00, 0x00, // + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x03, 0x00, 0x00, 0x00 }); // + Zip64EndOfCentralDirectoryLocator eocd = Zip64EndOfCentralDirectoryLocator.find(dataBlock, 20); + assertThat(eocd).isNull(); + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/Zip64EndOfCentralDirectoryRecordTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/Zip64EndOfCentralDirectoryRecordTests.java new file mode 100644 index 000000000000..486d34970ddd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/Zip64EndOfCentralDirectoryRecordTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIOException; + +/** + * Tests for {@link Zip64EndOfCentralDirectoryRecord}. + * + * @author Phillip Webb + */ +class Zip64EndOfCentralDirectoryRecordTests { + + @Test + void loadLoadsData() throws Exception { + DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { // + 0x50, 0x4b, 0x06, 0x06, // + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x02, 0x00, // + 0x03, 0x00, // + 0x04, 0x00, 0x00, 0x00, // + 0x05, 0x00, 0x00, 0x00, // + 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); // + Zip64EndOfCentralDirectoryLocator locator = new Zip64EndOfCentralDirectoryLocator(56, 0, 0, 0); + Zip64EndOfCentralDirectoryRecord eocd = Zip64EndOfCentralDirectoryRecord.load(dataBlock, locator); + assertThat(eocd.size()).isEqualTo(56); + assertThat(eocd.sizeOfZip64EndOfCentralDirectoryRecord()).isEqualTo(1); + assertThat(eocd.versionMadeBy()).isEqualTo((short) 2); + assertThat(eocd.versionNeededToExtract()).isEqualTo((short) 3); + assertThat(eocd.numberOfThisDisk()).isEqualTo(4); + assertThat(eocd.diskWhereCentralDirectoryStarts()).isEqualTo(5); + assertThat(eocd.numberOfCentralDirectoryEntriesOnThisDisk()).isEqualTo(6); + assertThat(eocd.totalNumberOfCentralDirectoryEntries()).isEqualTo(7); + assertThat(eocd.sizeOfCentralDirectory()).isEqualTo(8); + assertThat(eocd.offsetToStartOfCentralDirectory()); + } + + @Test + void loadWhenSignatureDoesNotMatchThrowsException() { + DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { // + 0x51, 0x4b, 0x06, 0x06, // + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x02, 0x00, // + 0x03, 0x00, // + 0x04, 0x00, 0x00, 0x00, // + 0x05, 0x00, 0x00, 0x00, // + 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); // + Zip64EndOfCentralDirectoryLocator locator = new Zip64EndOfCentralDirectoryLocator(56, 0, 0, 0); + assertThatIOException().isThrownBy(() -> Zip64EndOfCentralDirectoryRecord.load(dataBlock, locator)) + .withMessageContaining("Zip64 'End Of Central Directory Record' not found at position"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipCentralDirectoryFileHeaderRecordTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipCentralDirectoryFileHeaderRecordTests.java new file mode 100644 index 000000000000..a0a8645e9c10 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipCentralDirectoryFileHeaderRecordTests.java @@ -0,0 +1,213 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.util.zip.ZipEntry; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIOException; + +/** + * Tests for {@link ZipCentralDirectoryFileHeaderRecord}. + * + * @author Phillip Webb + */ +class ZipCentralDirectoryFileHeaderRecordTests { + + @Test + void loadLoadsData() throws Exception { + DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { // + 0x50, 0x4b, 0x01, 0x02, // + 0x01, 0x00, // + 0x02, 0x00, // + 0x03, 0x00, // + 0x04, 0x00, // + 0x05, 0x00, // + 0x06, 0x00, // + 0x07, 0x00, 0x00, 0x00, // + 0x08, 0x00, 0x00, 0x00, // + 0x09, 0x00, 0x00, 0x00, // + 0x0A, 0x00, // + 0x0B, 0x00, // + 0x0C, 0x00, // + 0x0D, 0x00, // + 0x0E, 0x00, // + 0x0F, 0x00, 0x00, 0x00, // + 0x10, 0x00, 0x00, 0x00 }); // + ZipCentralDirectoryFileHeaderRecord record = ZipCentralDirectoryFileHeaderRecord.load(dataBlock, 0); + assertThat(record.versionMadeBy()).isEqualTo((short) 1); + assertThat(record.versionNeededToExtract()).isEqualTo((short) 2); + assertThat(record.generalPurposeBitFlag()).isEqualTo((short) 3); + assertThat(record.compressionMethod()).isEqualTo((short) 4); + assertThat(record.lastModFileTime()).isEqualTo((short) 5); + assertThat(record.lastModFileDate()).isEqualTo((short) 6); + assertThat(record.crc32()).isEqualTo(7); + assertThat(record.compressedSize()).isEqualTo(8); + assertThat(record.uncompressedSize()).isEqualTo(9); + assertThat(record.fileNameLength()).isEqualTo((short) 10); + assertThat(record.extraFieldLength()).isEqualTo((short) 11); + assertThat(record.fileCommentLength()).isEqualTo((short) 12); + assertThat(record.diskNumberStart()).isEqualTo((short) 13); + assertThat(record.internalFileAttributes()).isEqualTo((short) 14); + assertThat(record.externalFileAttributes()).isEqualTo(15); + assertThat(record.offsetToLocalHeader()).isEqualTo(16); + } + + @Test + void loadWhenSignatureDoesNotMatchThrowsException() { + DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { // + 0x51, 0x4b, 0x01, 0x02, // + 0x01, 0x00, // + 0x02, 0x00, // + 0x03, 0x00, // + 0x04, 0x00, // + 0x05, 0x00, // + 0x06, 0x00, // + 0x07, 0x00, 0x00, 0x00, // + 0x08, 0x00, 0x00, 0x00, // + 0x09, 0x00, 0x00, 0x00, // + 0x0A, 0x00, // + 0x0B, 0x00, // + 0x0C, 0x00, // + 0x0D, 0x00, // + 0x0E, 0x00, // + 0x0F, 0x00, 0x00, 0x00, // + 0x10, 0x00, 0x00, 0x00 }); // + assertThatIOException().isThrownBy(() -> ZipCentralDirectoryFileHeaderRecord.load(dataBlock, 0)) + .withMessageContaining("'Central Directory File Header Record' not found"); + } + + @Test + void sizeReturnsSize() { + ZipCentralDirectoryFileHeaderRecord record = new ZipCentralDirectoryFileHeaderRecord((short) 1, (short) 2, + (short) 3, (short) 4, (short) 5, (short) 6, 7, 8, 9, (short) 10, (short) 11, (short) 12, (short) 13, + (short) 14, 15, 16); + assertThat(record.size()).isEqualTo(79L); + } + + @Test + void copyToCopiesDataToZipEntry() throws Exception { + DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { // + 0x50, 0x4b, 0x01, 0x02, // + 0x00, 0x00, // + 0x00, 0x00, // + 0x00, 0x00, // + 0x08, 0x00, // + 0x23, 0x74, // + 0x58, 0x36, // + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, // + 0x01, 0x00, 0x00, 0x00, // + 0x02, 0x00, 0x00, 0x00, // + 0x01, 0x00, // + 0x01, 0x00, // + 0x01, 0x00, // + 0x00, 0x00, // + 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, // + 0x61, // + 0x62, // + 0x63 }); // + ZipCentralDirectoryFileHeaderRecord record = ZipCentralDirectoryFileHeaderRecord.load(dataBlock, 0); + ZipEntry entry = new ZipEntry(""); + record.copyTo(dataBlock, 0, entry); + assertThat(entry.getMethod()).isEqualTo(ZipEntry.DEFLATED); + assertThat(entry.getTimeLocal()).hasYear(2007); + assertThat(entry.getTime()).isEqualTo(1172356386000L); + assertThat(entry.getCrc()).isEqualTo(0xFFFFFFFFL); + assertThat(entry.getCompressedSize()).isEqualTo(1); + assertThat(entry.getSize()).isEqualTo(2); + assertThat(entry.getExtra()).containsExactly(0x62); + assertThat(entry.getComment()).isEqualTo("c"); + } + + @Test + void withFileNameLengthReturnsUpdatedInstance() { + ZipCentralDirectoryFileHeaderRecord record = new ZipCentralDirectoryFileHeaderRecord((short) 1, (short) 2, + (short) 3, (short) 4, (short) 5, (short) 6, 7, 8, 9, (short) 10, (short) 11, (short) 12, (short) 13, + (short) 14, 15, 16) + .withFileNameLength((short) 100); + assertThat(record.versionMadeBy()).isEqualTo((short) 1); + assertThat(record.versionNeededToExtract()).isEqualTo((short) 2); + assertThat(record.generalPurposeBitFlag()).isEqualTo((short) 3); + assertThat(record.compressionMethod()).isEqualTo((short) 4); + assertThat(record.lastModFileTime()).isEqualTo((short) 5); + assertThat(record.lastModFileDate()).isEqualTo((short) 6); + assertThat(record.crc32()).isEqualTo(7); + assertThat(record.compressedSize()).isEqualTo(8); + assertThat(record.uncompressedSize()).isEqualTo(9); + assertThat(record.fileNameLength()).isEqualTo((short) 100); + assertThat(record.extraFieldLength()).isEqualTo((short) 11); + assertThat(record.fileCommentLength()).isEqualTo((short) 12); + assertThat(record.diskNumberStart()).isEqualTo((short) 13); + assertThat(record.internalFileAttributes()).isEqualTo((short) 14); + assertThat(record.externalFileAttributes()).isEqualTo(15); + assertThat(record.offsetToLocalHeader()).isEqualTo(16); + } + + @Test + void withOffsetToLocalHeaderReturnsUpdatedInstance() { + ZipCentralDirectoryFileHeaderRecord record = new ZipCentralDirectoryFileHeaderRecord((short) 1, (short) 2, + (short) 3, (short) 4, (short) 5, (short) 6, 7, 8, 9, (short) 10, (short) 11, (short) 12, (short) 13, + (short) 14, 15, 16) + .withOffsetToLocalHeader(100); + assertThat(record.versionMadeBy()).isEqualTo((short) 1); + assertThat(record.versionNeededToExtract()).isEqualTo((short) 2); + assertThat(record.generalPurposeBitFlag()).isEqualTo((short) 3); + assertThat(record.compressionMethod()).isEqualTo((short) 4); + assertThat(record.lastModFileTime()).isEqualTo((short) 5); + assertThat(record.lastModFileDate()).isEqualTo((short) 6); + assertThat(record.crc32()).isEqualTo(7); + assertThat(record.compressedSize()).isEqualTo(8); + assertThat(record.uncompressedSize()).isEqualTo(9); + assertThat(record.fileNameLength()).isEqualTo((short) 10); + assertThat(record.extraFieldLength()).isEqualTo((short) 11); + assertThat(record.fileCommentLength()).isEqualTo((short) 12); + assertThat(record.diskNumberStart()).isEqualTo((short) 13); + assertThat(record.internalFileAttributes()).isEqualTo((short) 14); + assertThat(record.externalFileAttributes()).isEqualTo(15); + assertThat(record.offsetToLocalHeader()).isEqualTo(100); + } + + @Test + void asByteArrayReturnsByteArray() throws Exception { + byte[] bytes = new byte[] { // + 0x50, 0x4b, 0x01, 0x02, // + 0x01, 0x00, // + 0x02, 0x00, // + 0x03, 0x00, // + 0x04, 0x00, // + 0x05, 0x00, // + 0x06, 0x00, // + 0x07, 0x00, 0x00, 0x00, // + 0x08, 0x00, 0x00, 0x00, // + 0x09, 0x00, 0x00, 0x00, // + 0x0A, 0x00, // + 0x0B, 0x00, // + 0x0C, 0x00, // + 0x0D, 0x00, // + 0x0E, 0x00, // + 0x0F, 0x00, 0x00, 0x00, // + 0x10, 0x00, 0x00, 0x00 }; + DataBlock dataBlock = new ByteArrayDataBlock(bytes); + ZipCentralDirectoryFileHeaderRecord record = ZipCentralDirectoryFileHeaderRecord.load(dataBlock, 0); + assertThat(record.asByteArray()).containsExactly(bytes); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipContentTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipContentTests.java new file mode 100644 index 000000000000..fe0e8bd54263 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipContentTests.java @@ -0,0 +1,437 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.Iterator; +import java.util.Random; +import java.util.jar.Manifest; +import java.util.zip.CRC32; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.testsupport.TestJar; +import org.springframework.boot.loader.zip.ZipContent.Entry; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StreamUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIOException; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link ZipContent}. + * + * @author Phillip Webb + * @author Martin Lau + * @author Andy Wilkinson + * @author Madhura Bhave + */ +class ZipContentTests { + + @TempDir + File tempDir; + + private File file; + + private ZipContent zipContent; + + @BeforeEach + void setup() throws Exception { + this.file = new File(this.tempDir, "test.jar"); + TestJar.create(this.file); + this.zipContent = ZipContent.open(this.file.toPath()); + } + + @AfterEach + void tearDown() throws Exception { + if (this.zipContent != null) { + try { + this.zipContent.close(); + } + catch (IllegalStateException ex) { + } + } + } + + @Test + void getCommentReturnsComment() { + assertThat(this.zipContent.getComment()).isEqualTo("outer"); + } + + @Test + void getCommentWhenClosedThrowsException() throws IOException { + this.zipContent.close(); + assertThatIllegalStateException().isThrownBy(() -> this.zipContent.getComment()) + .withMessage("Zip content closed"); + } + + @Test + void getEntryWhenPresentReturnsEntry() { + Entry entry = this.zipContent.getEntry("1.dat"); + assertThat(entry).isNotNull(); + assertThat(entry.getName()).isEqualTo("1.dat"); + } + + @Test + void getEntryWhenMissingReturnsNull() { + assertThat(this.zipContent.getEntry("missing.dat")).isNull(); + } + + @Test + void getEntryWithPrefixWhenPresentReturnsEntry() { + Entry entry = this.zipContent.getEntry("1", ".dat"); + assertThat(entry).isNotNull(); + assertThat(entry.getName()).isEqualTo("1.dat"); + } + + @Test + void getEntryWithLongPrefixWhenNameIsShorterReturnsNull() { + Entry entry = this.zipContent.getEntry("iamaverylongprefixandiwontfindanything", "1.dat"); + assertThat(entry).isNull(); + } + + @Test + void getEntryWithPrefixWhenMissingReturnsNull() { + assertThat(this.zipContent.getEntry("miss", "ing.dat")).isNull(); + } + + @Test + void getEntryWhenUsingSlashesIsCompatibleWithZipFile() throws IOException { + try (ZipFile zipFile = new ZipFile(this.file)) { + assertThat(zipFile.getEntry("META-INF").getName()).isEqualTo("META-INF/"); + assertThat(this.zipContent.getEntry("META-INF").getName()).isEqualTo("META-INF/"); + assertThat(zipFile.getEntry("META-INF/").getName()).isEqualTo("META-INF/"); + assertThat(this.zipContent.getEntry("META-INF/").getName()).isEqualTo("META-INF/"); + assertThat(zipFile.getEntry("d/9.dat").getName()).isEqualTo("d/9.dat"); + assertThat(this.zipContent.getEntry("d/9.dat").getName()).isEqualTo("d/9.dat"); + assertThat(zipFile.getEntry("d/9.dat/")).isNull(); + assertThat(this.zipContent.getEntry("d/9.dat/")).isNull(); + } + } + + @Test + void getManifestEntry() throws Exception { + Entry entry = this.zipContent.getEntry("META-INF/MANIFEST.MF"); + try (CloseableDataBlock dataBlock = entry.openContent()) { + Manifest manifest = new Manifest(asInflaterInputStream(dataBlock)); + assertThat(manifest.getMainAttributes().getValue("Built-By")).isEqualTo("j1"); + } + } + + @Test + void getEntryAsCreatesCompatibleEntries() throws IOException { + try (ZipFile zipFile = new ZipFile(this.file)) { + Iterator expected = zipFile.entries().asIterator(); + int i = 0; + while (expected.hasNext()) { + Entry actual = this.zipContent.getEntry(i++); + assertThatFieldsAreEqual(actual.as(ZipEntry::new), expected.next()); + } + } + } + + private void assertThatFieldsAreEqual(ZipEntry actual, ZipEntry expected) { + assertThat(actual.getName()).isEqualTo(expected.getName()); + assertThat(actual.getTime()).isEqualTo(expected.getTime()); + assertThat(actual.getLastModifiedTime()).isEqualTo(expected.getLastModifiedTime()); + assertThat(actual.getLastAccessTime()).isEqualTo(expected.getLastAccessTime()); + assertThat(actual.getCreationTime()).isEqualTo(expected.getCreationTime()); + assertThat(actual.getSize()).isEqualTo(expected.getSize()); + assertThat(actual.getCompressedSize()).isEqualTo(expected.getCompressedSize()); + assertThat(actual.getCrc()).isEqualTo(expected.getCrc()); + assertThat(actual.getMethod()).isEqualTo(expected.getMethod()); + assertThat(actual.getExtra()).isEqualTo(expected.getExtra()); + assertThat(actual.getComment()).isEqualTo(expected.getComment()); + } + + @Test + void sizeReturnsNumberOfEntries() { + assertThat(this.zipContent.size()).isEqualTo(12); + } + + @Test + void nestedJarFileReturnsNestedJar() throws IOException { + try (ZipContent nested = ZipContent.open(this.file.toPath(), "nested.jar")) { + assertThat(nested.size()).isEqualTo(5); + assertThat(nested.getComment()).isEqualTo("nested"); + assertThat(nested.size()).isEqualTo(5); + assertThat(nested.getEntry(0).getName()).isEqualTo("META-INF/"); + assertThat(nested.getEntry(1).getName()).isEqualTo("META-INF/MANIFEST.MF"); + assertThat(nested.getEntry(2).getName()).isEqualTo("3.dat"); + assertThat(nested.getEntry(3).getName()).isEqualTo("4.dat"); + assertThat(nested.getEntry(4).getName()).isEqualTo("\u00E4.dat"); + } + } + + @Test + void nestedJarFileWhenNameEndsInSlashThrowsException() { + assertThatIOException().isThrownBy(() -> ZipContent.open(this.file.toPath(), "nested.jar/")) + .withMessageStartingWith("Nested entry 'nested.jar/' not found in container zip"); + } + + @Test + void nestedDirectoryReturnsNestedJar() throws IOException { + try (ZipContent nested = ZipContent.open(this.file.toPath(), "d/")) { + assertThat(nested.size()).isEqualTo(3); + assertThat(nested.getEntry("9.dat")).isNotNull(); + assertThat(nested.getEntry(0).getName()).isEqualTo("META-INF/"); + assertThat(nested.getEntry(1).getName()).isEqualTo("META-INF/MANIFEST.MF"); + assertThat(nested.getEntry(2).getName()).isEqualTo("9.dat"); + } + } + + @Test + void nestedDirectoryWhenNotEndingInSlashThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> ZipContent.open(this.file.toPath(), "d")) + .withMessage("Nested entry name must end with '/'"); + } + + @Test + void getDataWhenNestedDirectoryReturnsVirtualZipDataBlock() throws IOException { + try (ZipContent nested = ZipContent.open(this.file.toPath(), "d/")) { + File file = new File(this.tempDir, "included.zip"); + write(file, nested.openRawZipData()); + try (ZipFile loadedZipFile = new ZipFile(file)) { + assertThat(loadedZipFile.size()).isEqualTo(3); + assertThat(loadedZipFile.stream().map(ZipEntry::getName)).containsExactly("META-INF/", + "META-INF/MANIFEST.MF", "9.dat"); + assertThat(loadedZipFile.getEntry("9.dat")).isNotNull(); + try (InputStream in = loadedZipFile.getInputStream(loadedZipFile.getEntry("9.dat"))) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + in.transferTo(out); + assertThat(out.toByteArray()).containsExactly(0x09); + } + } + } + } + + @Test + void loadWhenHasFrontMatterOpensZip() throws IOException { + File fileWithFrontMatter = new File(this.tempDir, "withfrontmatter.jar"); + FileOutputStream outputStream = new FileOutputStream(fileWithFrontMatter); + StreamUtils.copy("#/bin/bash", Charset.defaultCharset(), outputStream); + FileCopyUtils.copy(new FileInputStream(this.file), outputStream); + try (ZipContent zip = ZipContent.open(fileWithFrontMatter.toPath())) { + assertThat(zip.size()).isEqualTo(12); + assertThat(zip.getEntry(0).getName()).isEqualTo("META-INF/"); + assertThat(zip.getEntry(1).getName()).isEqualTo("META-INF/MANIFEST.MF"); + assertThat(zip.getEntry(2).getName()).isEqualTo("1.dat"); + assertThat(zip.getEntry(3).getName()).isEqualTo("2.dat"); + assertThat(zip.getEntry(4).getName()).isEqualTo("d/"); + assertThat(zip.getEntry(5).getName()).isEqualTo("d/9.dat"); + assertThat(zip.getEntry(6).getName()).isEqualTo("special/"); + assertThat(zip.getEntry(7).getName()).isEqualTo("special/\u00EB.dat"); + assertThat(zip.getEntry(8).getName()).isEqualTo("nested.jar"); + assertThat(zip.getEntry(9).getName()).isEqualTo("another-nested.jar"); + assertThat(zip.getEntry(10).getName()).isEqualTo("space nested.jar"); + assertThat(zip.getEntry(11).getName()).isEqualTo("multi-release.jar"); + } + } + + @Test + void openWhenZip64ThatExceedsZipEntryLimitOpensZip() throws Exception { + File zip64File = new File(this.tempDir, "zip64.zip"); + FileCopyUtils.copy(zip64Bytes(), zip64File); + try (ZipContent zip64Content = ZipContent.open(zip64File.toPath())) { + assertThat(zip64Content.size()).isEqualTo(65537); + for (int i = 0; i < zip64Content.size(); i++) { + Entry entry = zip64Content.getEntry(i); + try (CloseableDataBlock dataBlock = entry.openContent()) { + assertThat(asInflaterInputStream(dataBlock)).hasContent("Entry " + (i + 1)); + } + } + } + } + + @Test + void openWhenZip64ThatExceedsZipSizeLimitOpensZip() throws Exception { + Assumptions.assumeTrue(this.tempDir.getFreeSpace() > 6 * 1024 * 1024 * 1024, "Insufficient disk space"); + File zip64File = new File(this.tempDir, "zip64.zip"); + File entryFile = new File(this.tempDir, "entry.dat"); + CRC32 crc32 = new CRC32(); + try (FileOutputStream entryOut = new FileOutputStream(entryFile)) { + byte[] data = new byte[1024 * 1024]; + new Random().nextBytes(data); + for (int i = 0; i < 1024; i++) { + entryOut.write(data); + crc32.update(data); + } + } + try (ZipOutputStream zipOutput = new ZipOutputStream(new FileOutputStream(zip64File))) { + for (int i = 0; i < 6; i++) { + ZipEntry storedEntry = new ZipEntry("huge-" + i); + storedEntry.setSize(entryFile.length()); + storedEntry.setCompressedSize(entryFile.length()); + storedEntry.setCrc(crc32.getValue()); + storedEntry.setMethod(ZipEntry.STORED); + zipOutput.putNextEntry(storedEntry); + try (FileInputStream entryIn = new FileInputStream(entryFile)) { + StreamUtils.copy(entryIn, zipOutput); + } + zipOutput.closeEntry(); + } + } + try (ZipContent zip64Content = ZipContent.open(zip64File.toPath())) { + assertThat(zip64Content.size()).isEqualTo(6); + } + } + + @Test + void nestedZip64CanBeRead() throws Exception { + File containerFile = new File(this.tempDir, "outer.zip"); + try (ZipOutputStream jarOutput = new ZipOutputStream(new FileOutputStream(containerFile))) { + ZipEntry nestedEntry = new ZipEntry("nested-zip64.zip"); + byte[] contents = zip64Bytes(); + nestedEntry.setSize(contents.length); + nestedEntry.setCompressedSize(contents.length); + CRC32 crc32 = new CRC32(); + crc32.update(contents); + nestedEntry.setCrc(crc32.getValue()); + nestedEntry.setMethod(ZipEntry.STORED); + jarOutput.putNextEntry(nestedEntry); + jarOutput.write(contents); + jarOutput.closeEntry(); + } + try (ZipContent nestedZip = ZipContent.open(containerFile.toPath(), "nested-zip64.zip")) { + assertThat(nestedZip.size()).isEqualTo(65537); + for (int i = 0; i < nestedZip.size(); i++) { + Entry entry = nestedZip.getEntry(i); + try (CloseableDataBlock content = entry.openContent()) { + assertThat(asInflaterInputStream(content)).hasContent("Entry " + (i + 1)); + } + } + } + } + + private byte[] zip64Bytes() throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ZipOutputStream zipOutput = new ZipOutputStream(bytes); + for (int i = 0; i < 65537; i++) { + zipOutput.putNextEntry(new ZipEntry(i + ".dat")); + zipOutput.write(("Entry " + (i + 1)).getBytes(StandardCharsets.UTF_8)); + zipOutput.closeEntry(); + } + zipOutput.close(); + return bytes.toByteArray(); + } + + @Test + void entryWithEpochTimeOfZeroShouldNotFail() throws Exception { + File file = createZipFileWithEpochTimeOfZero(); + try (ZipContent zip = ZipContent.open(file.toPath())) { + ZipEntry entry = zip.getEntry(0).as(ZipEntry::new); + assertThat(entry.getLastModifiedTime().toInstant()).isEqualTo(Instant.EPOCH); + assertThat(entry.getName()).isEqualTo("1.dat"); + } + } + + private File createZipFileWithEpochTimeOfZero() throws Exception { + File file = new File(this.tempDir, "temp.zip"); + String comment = "outer"; + try (ZipOutputStream zipOutput = new ZipOutputStream(new FileOutputStream(file))) { + zipOutput.setComment(comment); + ZipEntry entry = new ZipEntry("1.dat"); + entry.setLastModifiedTime(FileTime.from(Instant.EPOCH)); + zipOutput.putNextEntry(entry); + zipOutput.write(new byte[] { (byte) 1 }); + zipOutput.closeEntry(); + } + ByteBuffer data = ByteBuffer.wrap(Files.readAllBytes(file.toPath())); + data.order(ByteOrder.LITTLE_ENDIAN); + int endOfCentralDirectoryRecordPos = data.remaining() - ZipFile.ENDHDR - comment.getBytes().length; + data.position(endOfCentralDirectoryRecordPos + ZipFile.ENDOFF); + int startOfCentralDirectoryOffset = data.getInt(); + data.position(startOfCentralDirectoryOffset + ZipFile.CENOFF); + int localHeaderPosition = data.getInt(); + writeTimeBlock(data.array(), startOfCentralDirectoryOffset + ZipFile.CENTIM, 0); + writeTimeBlock(data.array(), localHeaderPosition + ZipFile.LOCTIM, 0); + File zerotimedFile = new File(this.tempDir, "zerotimed.zip"); + Files.write(zerotimedFile.toPath(), data.array()); + return zerotimedFile; + } + + @Test + void getInfoReturnsComputedInfo() { + ZipInfo info = this.zipContent.getInfo(ZipInfo.class, ZipInfo::get); + assertThat(info.size()).isEqualTo(12); + } + + private static void writeTimeBlock(byte[] data, int pos, int value) { + data[pos] = (byte) (value & 0xff); + data[pos + 1] = (byte) ((value >> 8) & 0xff); + data[pos + 2] = (byte) ((value >> 16) & 0xff); + data[pos + 3] = (byte) ((value >> 24) & 0xff); + } + + private InputStream asInflaterInputStream(DataBlock dataBlock) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate((int) dataBlock.size() + 1); + buffer.limit(buffer.limit() - 1); + dataBlock.readFully(buffer, 0); + ByteArrayInputStream in = new ByteArrayInputStream(buffer.array()); + return new InflaterInputStream(in, new Inflater(true)); + } + + private void write(File file, CloseableDataBlock dataBlock) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate((int) dataBlock.size()); + dataBlock.readFully(buffer, 0); + Files.write(file.toPath(), buffer.array()); + dataBlock.close(); + } + + private static class ZipInfo { + + private int size; + + ZipInfo(int size) { + this.size = size; + } + + int size() { + return this.size; + } + + static ZipInfo get(ZipContent content) { + return new ZipInfo(content.size()); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipEndOfCentralDirectoryRecordTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipEndOfCentralDirectoryRecordTests.java new file mode 100644 index 000000000000..4a52c0be9b5b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipEndOfCentralDirectoryRecordTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIOException; + +/** + * Tests for {@link ZipEndOfCentralDirectoryRecord}. + * + * @author Phillip Webb + */ +class ZipEndOfCentralDirectoryRecordTests { + + @Test + void loadLocatesAndLoadsData() throws Exception { + DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { // + 0x50, 0x4b, 0x05, 0x06, // + 0x01, 0x00, // + 0x02, 0x00, // + 0x03, 0x00, // + 0x04, 0x00, // + 0x05, 0x00, 0x00, 0x00, // + 0x06, 0x00, 0x00, 0x00, // + 0x07, 0x00 }); // + ZipEndOfCentralDirectoryRecord.Located located = ZipEndOfCentralDirectoryRecord.load(dataBlock); + assertThat(located.pos()).isEqualTo(0L); + ZipEndOfCentralDirectoryRecord record = located.endOfCentralDirectoryRecord(); + assertThat(record.numberOfThisDisk()).isEqualTo((short) 1); + assertThat(record.diskWhereCentralDirectoryStarts()).isEqualTo((short) 2); + assertThat(record.numberOfCentralDirectoryEntriesOnThisDisk()).isEqualTo((short) 3); + assertThat(record.totalNumberOfCentralDirectoryEntries()).isEqualTo((short) 4); + assertThat(record.sizeOfCentralDirectory()).isEqualTo(5); + assertThat(record.offsetToStartOfCentralDirectory()).isEqualTo(6); + assertThat(record.commentLength()).isEqualTo((short) 7); + } + + @Test + void loadWhenMultipleBuffersBackLoadsData() throws Exception { + byte[] bytes = new byte[ZipEndOfCentralDirectoryRecord.BUFFER_SIZE * 4]; + byte[] data = new byte[] { // + 0x50, 0x4b, 0x05, 0x06, // + 0x01, 0x00, // + 0x02, 0x00, // + 0x03, 0x00, // + 0x04, 0x00, // + 0x05, 0x00, 0x00, 0x00, // + 0x06, 0x00, 0x00, 0x00, // + 0x07, 0x00 }; // + System.arraycopy(data, 0, bytes, 4, data.length); + ZipEndOfCentralDirectoryRecord.Located located = ZipEndOfCentralDirectoryRecord + .load(new ByteArrayDataBlock(bytes)); + assertThat(located.pos()).isEqualTo(4L); + } + + @Test + void loadWhenSignatureDoesNotMatchThrowsException() { + DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { // + 0x51, 0x4b, 0x05, 0x06, // + 0x01, 0x00, // + 0x02, 0x00, // + 0x03, 0x00, // + 0x04, 0x00, // + 0x05, 0x00, 0x00, 0x00, // + 0x06, 0x00, 0x00, 0x00, // + 0x07, 0x00 }); // + assertThatIOException().isThrownBy(() -> ZipEndOfCentralDirectoryRecord.load(dataBlock)) + .withMessageContaining("'End Of Central Directory Record' not found"); + } + + @Test + void asByteArrayReturnsByteArray() throws Exception { + byte[] bytes = new byte[] { // + 0x50, 0x4b, 0x05, 0x06, // + 0x01, 0x00, // + 0x02, 0x00, // + 0x03, 0x00, // + 0x04, 0x00, // + 0x05, 0x00, 0x00, 0x00, // + 0x06, 0x00, 0x00, 0x00, // + 0x07, 0x00 }; // + ZipEndOfCentralDirectoryRecord.Located located = ZipEndOfCentralDirectoryRecord + .load(new ByteArrayDataBlock(bytes)); + assertThat(located.endOfCentralDirectoryRecord().asByteArray()).isEqualTo(bytes); + } + + @Test + void sizeReturnsSize() { + ZipEndOfCentralDirectoryRecord record = new ZipEndOfCentralDirectoryRecord((short) 1, (short) 2, (short) 3, + (short) 4, 5, 6, (short) 7); + assertThat(record.size()).isEqualTo(29L); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipLocalFileHeaderRecordTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipLocalFileHeaderRecordTests.java new file mode 100644 index 000000000000..02cc96fca27b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipLocalFileHeaderRecordTests.java @@ -0,0 +1,117 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIOException; + +/** + * Tests for {@link ZipLocalFileHeaderRecord}. + * + * @author Phillip Webb + */ +class ZipLocalFileHeaderRecordTests { + + @Test + void loadLoadsData() throws Exception { + DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { // + 0x50, 0x4b, 0x03, 0x04, // + 0x01, 0x00, // + 0x02, 0x00, // + 0x03, 0x00, // + 0x04, 0x00, // + 0x05, 0x00, // + 0x06, 0x00, 0x00, 0x00, // + 0x07, 0x00, 0x00, 0x00, // + 0x08, 0x00, 0x00, 0x00, // + 0x09, 0x00, // + 0x0A, 0x00 }); // + ZipLocalFileHeaderRecord record = ZipLocalFileHeaderRecord.load(dataBlock, 0); + assertThat(record.versionNeededToExtract()).isEqualTo((short) 1); + assertThat(record.generalPurposeBitFlag()).isEqualTo((short) 2); + assertThat(record.compressionMethod()).isEqualTo((short) 3); + assertThat(record.lastModFileTime()).isEqualTo((short) 4); + assertThat(record.lastModFileDate()).isEqualTo((short) 5); + assertThat(record.crc32()).isEqualTo(6); + assertThat(record.compressedSize()).isEqualTo(7); + assertThat(record.uncompressedSize()).isEqualTo(8); + assertThat(record.fileNameLength()).isEqualTo((short) 9); + assertThat(record.extraFieldLength()).isEqualTo((short) 10); + } + + @Test + void loadWhenSignatureDoesNotMatchThrowsException() { + DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { // + 0x51, 0x4b, 0x03, 0x04, // + 0x01, 0x00, // + 0x02, 0x00, // + 0x03, 0x00, // + 0x04, 0x00, // + 0x05, 0x00, // + 0x06, 0x00, 0x00, 0x00, // + 0x07, 0x00, 0x00, 0x00, // + 0x08, 0x00, 0x00, 0x00, // + 0x09, 0x00, // + 0x0A, 0x00 }); // + assertThatIOException().isThrownBy(() -> ZipLocalFileHeaderRecord.load(dataBlock, 0)) + .withMessageContaining("'Local File Header Record' not found"); + } + + @Test + void sizeReturnsSize() { + ZipLocalFileHeaderRecord record = new ZipLocalFileHeaderRecord((short) 1, (short) 2, (short) 3, (short) 4, + (short) 5, 6, 7, 8, (short) 9, (short) 10); + assertThat(record.size()).isEqualTo(49L); + } + + @Test + void withExtraFieldLengthReturnsUpdatedInstance() { + ZipLocalFileHeaderRecord record = new ZipLocalFileHeaderRecord((short) 1, (short) 2, (short) 3, (short) 4, + (short) 5, 6, 7, 8, (short) 9, (short) 10) + .withExtraFieldLength((short) 100); + assertThat(record.extraFieldLength()).isEqualTo((short) 100); + } + + @Test + void withFileNameLengthReturnsUpdatedInstance() { + ZipLocalFileHeaderRecord record = new ZipLocalFileHeaderRecord((short) 1, (short) 2, (short) 3, (short) 4, + (short) 5, 6, 7, 8, (short) 9, (short) 10) + .withFileNameLength((short) 100); + assertThat(record.fileNameLength()).isEqualTo((short) 100); + } + + @Test + void asByteArrayReturnsByteArray() throws Exception { + byte[] bytes = new byte[] { // + 0x50, 0x4b, 0x03, 0x04, // + 0x01, 0x00, // + 0x02, 0x00, // + 0x03, 0x00, // + 0x04, 0x00, // + 0x05, 0x00, // + 0x06, 0x00, 0x00, 0x00, // + 0x07, 0x00, 0x00, 0x00, // + 0x08, 0x00, 0x00, 0x00, // + 0x09, 0x00, // + 0x0A, 0x00 }; // + ZipLocalFileHeaderRecord record = ZipLocalFileHeaderRecord.load(new ByteArrayDataBlock(bytes), 0); + assertThat(record.asByteArray()).isEqualTo(bytes); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipStringTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipStringTests.java new file mode 100644 index 000000000000..d421c2514520 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipStringTests.java @@ -0,0 +1,194 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.assertj.core.api.AbstractBooleanAssert; +import org.assertj.core.api.AbstractIntegerAssert; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ZipString}. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +class ZipStringTests { + + @ParameterizedTest + @EnumSource + void hashGeneratesCorrectHashCode(HashSourceType sourceType) throws Exception { + testHash(sourceType, true, "abcABC123xyz!"); + testHash(sourceType, false, "abcABC123xyz!"); + } + + @ParameterizedTest + @EnumSource + void hashWhenHasSpecialCharsGeneratesCorrectHashCode(HashSourceType sourceType) throws Exception { + testHash(sourceType, true, "special/\u00EB.dat"); + } + + @ParameterizedTest + @EnumSource + void hashWhenHasCyrillicCharsGeneratesCorrectHashCode(HashSourceType sourceType) throws Exception { + testHash(sourceType, true, "\u0432\u0435\u0441\u043D\u0430"); + } + + @ParameterizedTest + @EnumSource + void hashWhenHasEmojiGeneratesCorrectHashCode(HashSourceType sourceType) throws Exception { + testHash(sourceType, true, "\ud83d\udca9"); + } + + @ParameterizedTest + @EnumSource + void hashWhenOnlyDifferenceIsEndSlashGeneratesSameHashCode(HashSourceType sourceType) throws Exception { + testHash(sourceType, "", true, "/".hashCode()); + testHash(sourceType, "/", true, "/".hashCode()); + testHash(sourceType, "a/b", true, "a/b/".hashCode()); + testHash(sourceType, "a/b/", true, "a/b/".hashCode()); + } + + void testHash(HashSourceType sourceType, boolean addSlash, String source) throws Exception { + String expected = (addSlash && !source.endsWith("/")) ? source + "/" : source; + testHash(sourceType, source, addSlash, expected.hashCode()); + } + + void testHash(HashSourceType sourceType, String source, boolean addEndSlash, int expected) throws Exception { + switch (sourceType) { + case STRING -> { + assertThat(ZipString.hash(source, addEndSlash)).isEqualTo(expected); + } + case CHAR_SEQUENCE -> { + CharSequence charSequence = new StringBuilder(source); + assertThat(ZipString.hash(charSequence, addEndSlash)).isEqualTo(expected); + } + case DATA_BLOCK -> { + ByteArrayDataBlock dataBlock = new ByteArrayDataBlock(source.getBytes(StandardCharsets.UTF_8)); + assertThat(ZipString.hash(null, dataBlock, 0, (int) dataBlock.size(), addEndSlash)).isEqualTo(expected); + + } + } + } + + @Test + void matchesWhenExactMatchReturnsTrue() throws Exception { + assertMatches("one/two/three", "one/two/three", false).isTrue(); + } + + @Test + void matchesWhenNotMatchWithSameLengthReturnsFalse() throws Exception { + assertMatches("one/two/three", "one/too/three", false).isFalse(); + } + + @Test + void matchesWhenExactMatchWithSpecialCharsReturnsTrue() throws Exception { + assertMatches("special/\u00EB.dat", "special/\u00EB.dat", false).isTrue(); + } + + @Test + void matchesWhenExactMatchWithCyrillicCharsReturnsTrue() throws Exception { + assertMatches("\u0432\u0435\u0441\u043D\u0430", "\u0432\u0435\u0441\u043D\u0430", false).isTrue(); + } + + @Test + void matchesWhenNoMatchWithCyrillicCharsReturnsFalse() throws Exception { + assertMatches("\u0432\u0435\u0441\u043D\u0430", "\u0432\u0435\u0441\u043D\u043D", false).isFalse(); + } + + @Test + void matchesWhenExactMatchWithEmojiCharsReturnsTrue() throws Exception { + assertMatches("\ud83d\udca9", "\ud83d\udca9", false).isTrue(); + } + + @Test + void matchesWithAddSlash() throws Exception { + assertMatches("META-INF/MANFIFEST.MF", "META-INF/MANFIFEST.MF", true).isTrue(); + assertMatches("one/two/three/", "one/two/three", true).isTrue(); + assertMatches("one/two/three", "one/two/three/", true).isFalse(); + assertMatches("one/two/three/", "one/too/three", true).isFalse(); + assertMatches("one/two/three", "one/too/three/", true).isFalse(); + assertMatches("one/two/three//", "one/two/three", true).isFalse(); + assertMatches("one/two/three", "one/two/three//", true).isFalse(); + } + + @Test + void matchesWhenDataBlockShorterThenCharSequenceReturnsFalse() throws Exception { + assertMatches("one/two/thre", "one/two/three", false).isFalse(); + } + + @Test + void matchesWhenCharSequenceShorterThanDataBlockReturnsFalse() throws Exception { + assertMatches("one/two/three", "one/two/thre", false).isFalse(); + } + + @Test + void startsWithWhenStartsWith() throws Exception { + assertStartsWith("one/two", "one/").isEqualTo(4); + } + + @Test + void startsWithWhenExact() throws Exception { + assertStartsWith("one/", "one/").isEqualTo(4); + } + + @Test + void startsWithWhenTooShort() throws Exception { + assertStartsWith("one/two", "one/two/three/").isEqualTo(-1); + } + + @Test + void startsWithWhenDoesNotStartWith() throws Exception { + assertStartsWith("one/three/", "one/two/").isEqualTo(-1); + } + + @Test + void zipStringWhenMultiCodePointAtBufferBoundary() throws Exception { + StringBuilder source = new StringBuilder(); + for (int i = 0; i < ZipString.BUFFER_SIZE - 1; i++) { + source.append("A"); + } + source.append("\u1EFF"); + String charSequence = source.toString(); + source.append("suffix"); + assertStartsWith(source.toString(), charSequence); + } + + private AbstractBooleanAssert assertMatches(String source, CharSequence charSequence, boolean addSlash) + throws Exception { + ByteArrayDataBlock dataBlock = new ByteArrayDataBlock(source.getBytes(StandardCharsets.UTF_8)); + return assertThat(ZipString.matches(null, dataBlock, 0, (int) dataBlock.size(), charSequence, addSlash)); + } + + private AbstractIntegerAssert assertStartsWith(String source, CharSequence charSequence) throws IOException { + ByteArrayDataBlock dataBlock = new ByteArrayDataBlock(source.getBytes(StandardCharsets.UTF_8)); + return assertThat(ZipString.startsWith(null, dataBlock, 0, (int) dataBlock.size(), charSequence)); + } + + enum HashSourceType { + + STRING, CHAR_SEQUENCE, DATA_BLOCK + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/launch/classpath-index-file.idx similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/classpath-index-file.idx rename to spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/org/springframework/boot/loader/launch/classpath-index-file.idx diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/MANIFEST.MF b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/MANIFEST.MF deleted file mode 100644 index 8b137891791f..000000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/root/META-INF/MANIFEST.MF +++ /dev/null @@ -1 +0,0 @@ - From fd9b2b114edcc1f18e2967c47868a7ffcafa5798 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 28 Sep 2023 23:13:49 -0700 Subject: [PATCH 0526/1215] Improve Tomcat performance when using nested jars Add `NestedJarResourceSet` which can be used for nested jar URLs and unlike the standard Tomcat implementation does not assume that the JAR is backed by a single file. Closes gh-37452 --- .../embedded/tomcat/NestedJarResourceSet.java | 161 ++++++++++++++++++ .../tomcat/TomcatServletWebServerFactory.java | 40 ++++- 2 files changed, 192 insertions(+), 9 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/NestedJarResourceSet.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/NestedJarResourceSet.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/NestedJarResourceSet.java new file mode 100644 index 000000000000..0f1be9560a92 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/NestedJarResourceSet.java @@ -0,0 +1,161 @@ +/* + * Copyright 2012-2023 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.web.embedded.tomcat; + +import java.io.IOException; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.jar.Attributes; +import java.util.jar.Attributes.Name; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.WebResourceSet; +import org.apache.catalina.webresources.AbstractSingleArchiveResourceSet; +import org.apache.catalina.webresources.JarResource; + +import org.springframework.util.Assert; +import org.springframework.util.ResourceUtils; + +/** + * A {@link WebResourceSet} for a resource in a nested JAR. + * + * @author Phillip Webb + */ +class NestedJarResourceSet extends AbstractSingleArchiveResourceSet { + + private static final Name MULTI_RELEASE = new Name("Multi-Release"); + + private final URL url; + + private JarFile archive = null; + + private long archiveUseCount = 0; + + private boolean useCaches; + + private volatile Boolean multiRelease; + + NestedJarResourceSet(URL url, WebResourceRoot root, String webAppMount, String internalPath) + throws IllegalArgumentException { + this.url = url; + setRoot(root); + setWebAppMount(webAppMount); + setInternalPath(internalPath); + setStaticOnly(true); + if (getRoot().getState().isAvailable()) { + try { + start(); + } + catch (LifecycleException ex) { + throw new IllegalStateException(ex); + } + } + } + + @Override + protected WebResource createArchiveResource(JarEntry jarEntry, String webAppPath, Manifest manifest) { + return new JarResource(this, webAppPath, getBaseUrlString(), jarEntry); + } + + @Override + protected void initInternal() throws LifecycleException { + try { + JarURLConnection connection = connect(); + try { + setManifest(connection.getManifest()); + setBaseUrl(connection.getJarFileURL()); + } + finally { + if (!connection.getUseCaches()) { + connection.getJarFile().close(); + } + } + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + @Override + protected JarFile openJarFile() throws IOException { + synchronized (this.archiveLock) { + if (this.archive == null) { + JarURLConnection connection = connect(); + this.useCaches = connection.getUseCaches(); + this.archive = connection.getJarFile(); + } + this.archiveUseCount++; + return this.archive; + } + } + + @Override + protected void closeJarFile() { + synchronized (this.archiveLock) { + this.archiveUseCount--; + } + } + + @Override + protected boolean isMultiRelease() { + if (this.multiRelease == null) { + synchronized (this.archiveLock) { + if (this.multiRelease == null) { + // JarFile.isMultiRelease() is final so we must go to the manifest + Manifest manifest = getManifest(); + Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null; + this.multiRelease = (attributes != null) ? attributes.containsKey(MULTI_RELEASE) : false; + } + } + } + return this.multiRelease.booleanValue(); + } + + @Override + public void gc() { + synchronized (this.archiveLock) { + if (this.archive != null && this.archiveUseCount == 0) { + try { + if (!this.useCaches) { + this.archive.close(); + } + } + catch (IOException ex) { + // Ignore + } + this.archive = null; + this.archiveEntries = null; + } + } + } + + private JarURLConnection connect() throws IOException { + URLConnection connection = this.url.openConnection(); + ResourceUtils.useCachesIfNecessary(connection); + Assert.state(connection instanceof JarURLConnection, + () -> "URL '%s' did not return a JAR connection".formatted(this.url)); + connection.connect(); + return (JarURLConnection) connection; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java index ca5e6fa1d413..7f05d87a1159 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java @@ -19,6 +19,7 @@ import java.io.File; import java.io.InputStream; import java.lang.reflect.Method; +import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -46,6 +47,7 @@ import org.apache.catalina.Manager; import org.apache.catalina.Valve; import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; import org.apache.catalina.WebResourceRoot.ResourceSetType; import org.apache.catalina.WebResourceSet; import org.apache.catalina.Wrapper; @@ -772,6 +774,10 @@ public void lifecycleEvent(LifecycleEvent event) { private final class StaticResourceConfigurer implements LifecycleListener { + private static final String WEB_APP_MOUNT = "/"; + + private static final String INTERNAL_PATH = "/META-INF/resources"; + private final Context context; private StaticResourceConfigurer(Context context) { @@ -804,23 +810,39 @@ private void addResourceJars(List resourceJarUrls) { private void addResourceSet(String resource) { try { - if (isInsideNestedJar(resource)) { - // It's a nested jar but we now don't want the suffix because Tomcat - // is going to try and locate it as a root URL (not the resource - // inside it) - resource = resource.substring(0, resource.length() - 2); + if (isInsideClassicNestedJar(resource)) { + addClassicNestedResourceSet(resource); + return; } + WebResourceRoot root = this.context.getResources(); URL url = new URL(resource); - String path = "/META-INF/resources"; - this.context.getResources().createWebResourceSet(ResourceSetType.RESOURCE_JAR, "/", url, path); + if (isInsideNestedJar(resource)) { + root.addJarResources(new NestedJarResourceSet(url, root, WEB_APP_MOUNT, INTERNAL_PATH)); + } + else { + root.createWebResourceSet(ResourceSetType.RESOURCE_JAR, WEB_APP_MOUNT, url, INTERNAL_PATH); + } } catch (Exception ex) { // Ignore (probably not a directory) } } - private boolean isInsideNestedJar(String dir) { - return dir.indexOf("!/") < dir.lastIndexOf("!/"); + private void addClassicNestedResourceSet(String resource) throws MalformedURLException { + // It's a nested jar but we now don't want the suffix because Tomcat + // is going to try and locate it as a root URL (not the resource + // inside it) + URL url = new URL(resource.substring(0, resource.length() - 2)); + this.context.getResources() + .createWebResourceSet(ResourceSetType.RESOURCE_JAR, WEB_APP_MOUNT, url, INTERNAL_PATH); + } + + private boolean isInsideClassicNestedJar(String resource) { + return !isInsideNestedJar(resource) && resource.indexOf("!/") < resource.lastIndexOf("!/"); + } + + private boolean isInsideNestedJar(String resource) { + return resource.startsWith("jar:nested:"); } } From 560527945bdb158014d05daea57c09565cc7b016 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 3 Oct 2023 17:28:44 -0700 Subject: [PATCH 0527/1215] Add background preinitializers for Tomcat and JDK ZoneId Closes gh-37670 --- .../BackgroundPreinitializer.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/BackgroundPreinitializer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/BackgroundPreinitializer.java index 06a0f557df5c..d64b0763fae6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/BackgroundPreinitializer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/BackgroundPreinitializer.java @@ -17,11 +17,14 @@ package org.springframework.boot.autoconfigure; import java.nio.charset.StandardCharsets; +import java.time.ZoneId; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; import jakarta.validation.Configuration; import jakarta.validation.Validation; +import org.apache.catalina.authenticator.NonLoginAuthenticator; +import org.apache.tomcat.util.http.Rfc6265CookieProcessor; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.boot.context.event.ApplicationFailedEvent; @@ -107,6 +110,8 @@ public void run() { runSafely(new JacksonInitializer()); } runSafely(new CharsetInitializer()); + runSafely(new TomcatInitializer()); + runSafely(new JdkInitializer()); preinitializationComplete.countDown(); } @@ -189,4 +194,23 @@ public void run() { } + private static class TomcatInitializer implements Runnable { + + @Override + public void run() { + new Rfc6265CookieProcessor(); + new NonLoginAuthenticator(); + } + + } + + private static class JdkInitializer implements Runnable { + + @Override + public void run() { + ZoneId.systemDefault(); + } + + } + } From 42f50fa2925ff4809e09d65a88730c7db5508186 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 3 Oct 2023 20:29:33 -0700 Subject: [PATCH 0528/1215] Attempt to fix CI failures Attempt to fix CI failures caused by timezone differences and different JDK versions. See gh-37668 --- .../boot/loader/jar/NestedJarFileTests.java | 6 ++++-- .../loader/launch/ExplodedArchiveTests.java | 4 ++-- .../boot/loader/testsupport/TestJar.java | 21 +++++-------------- ...CentralDirectoryFileHeaderRecordTests.java | 5 ++++- 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java index 1944f30b9f4d..59b3e7cc8fd2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java @@ -352,7 +352,8 @@ void streamStreamsEnties() throws IOException { assertThat(jar.stream().map((entry) -> entry.getName() + ":" + entry.getRealName())).containsExactly( "META-INF/:META-INF/", "META-INF/MANIFEST.MF:META-INF/MANIFEST.MF", "multi-release.dat:multi-release.dat", - "META-INF/versions/17/multi-release.dat:META-INF/versions/17/multi-release.dat"); + "META-INF/versions/%1$d/multi-release.dat:META-INF/versions/%1$d/multi-release.dat" + .formatted(TestJar.MULTI_JAR_VERSION)); } } @@ -361,7 +362,8 @@ void versionedStreamStreamsEntries() throws IOException { try (NestedJarFile jar = new NestedJarFile(this.file, "multi-release.jar", Runtime.version())) { assertThat(jar.versionedStream().map((entry) -> entry.getName() + ":" + entry.getRealName())) .containsExactly("META-INF/:META-INF/", "META-INF/MANIFEST.MF:META-INF/MANIFEST.MF", - "multi-release.dat:META-INF/versions/17/multi-release.dat"); + "multi-release.dat:META-INF/versions/%1$d/multi-release.dat" + .formatted(TestJar.MULTI_JAR_VERSION)); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/ExplodedArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/ExplodedArchiveTests.java index 96107512a33f..b18164f7513b 100755 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/ExplodedArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/ExplodedArchiveTests.java @@ -85,7 +85,7 @@ void getManifestReturnsManifest() throws Exception { } @Test - void getClassPathUrlsWhenNoPredicartesReturnsUrls() throws Exception { + void getClassPathUrlsWhenNoPredicatesReturnsUrls() throws Exception { Set urls = this.archive.getClassPathUrls(Archive.ALL_ENTRIES); URL[] expectedUrls = TestJar.expectedEntries().stream().map(this::toUrl).toArray(URL[]::new); assertThat(urls).containsExactlyInAnyOrder(expectedUrls); @@ -123,7 +123,7 @@ private void createArchive(String directoryName) throws Exception { Enumeration entries = jarFile.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); - File destination = new File(this.rootDirectory.getAbsolutePath() + File.separator + entry.getName()); + File destination = new File(this.rootDirectory, entry.getName()); destination.getParentFile().mkdirs(); if (entry.isDirectory()) { destination.mkdir(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/testsupport/TestJar.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/testsupport/TestJar.java index 292fd44b66e1..efa625e59ce3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/testsupport/TestJar.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/testsupport/TestJar.java @@ -35,21 +35,9 @@ */ public abstract class TestJar { - private static final int BASE_VERSION = 8; - - private static final int RUNTIME_VERSION; + public static final int MULTI_JAR_VERSION = Runtime.version().feature(); - static { - int version; - try { - Object runtimeVersion = Runtime.class.getMethod("version").invoke(null); - version = (int) runtimeVersion.getClass().getMethod("major").invoke(runtimeVersion); - } - catch (Throwable ex) { - version = BASE_VERSION; - } - RUNTIME_VERSION = version; - } + private static final int BASE_VERSION = 8; public static void create(File file) throws Exception { create(file, false); @@ -120,8 +108,9 @@ private static byte[] getNestedJarData(boolean multiRelease) throws Exception { writeManifest(jarOutputStream, "j2", multiRelease); if (multiRelease) { writeEntry(jarOutputStream, "multi-release.dat", BASE_VERSION); - writeEntry(jarOutputStream, String.format("META-INF/versions/%d/multi-release.dat", RUNTIME_VERSION), - RUNTIME_VERSION); + writeEntry(jarOutputStream, + String.format("META-INF/versions/%d/multi-release.dat", MULTI_JAR_VERSION), + MULTI_JAR_VERSION); } else { writeEntry(jarOutputStream, "3.dat", 3); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipCentralDirectoryFileHeaderRecordTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipCentralDirectoryFileHeaderRecordTests.java index a0a8645e9c10..5fe7e9ee8897 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipCentralDirectoryFileHeaderRecordTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipCentralDirectoryFileHeaderRecordTests.java @@ -16,6 +16,8 @@ package org.springframework.boot.loader.zip; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.zip.ZipEntry; import org.junit.jupiter.api.Test; @@ -129,7 +131,8 @@ void copyToCopiesDataToZipEntry() throws Exception { record.copyTo(dataBlock, 0, entry); assertThat(entry.getMethod()).isEqualTo(ZipEntry.DEFLATED); assertThat(entry.getTimeLocal()).hasYear(2007); - assertThat(entry.getTime()).isEqualTo(1172356386000L); + ZonedDateTime expectedTime = ZonedDateTime.of(2007, 02, 24, 14, 33, 06, 0, ZoneId.systemDefault()); + assertThat(entry.getTime()).isEqualTo(expectedTime.toEpochSecond() * 1000); assertThat(entry.getCrc()).isEqualTo(0xFFFFFFFFL); assertThat(entry.getCompressedSize()).isEqualTo(1); assertThat(entry.getSize()).isEqualTo(2); From 1f5472387d539f8aa0b58d293f1dae087297cb25 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 3 Oct 2023 20:41:48 -0700 Subject: [PATCH 0529/1215] Fix formatting --- .../org/springframework/boot/loader/testsupport/TestJar.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/testsupport/TestJar.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/testsupport/TestJar.java index efa625e59ce3..137f684c0796 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/testsupport/TestJar.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/testsupport/TestJar.java @@ -108,8 +108,7 @@ private static byte[] getNestedJarData(boolean multiRelease) throws Exception { writeManifest(jarOutputStream, "j2", multiRelease); if (multiRelease) { writeEntry(jarOutputStream, "multi-release.dat", BASE_VERSION); - writeEntry(jarOutputStream, - String.format("META-INF/versions/%d/multi-release.dat", MULTI_JAR_VERSION), + writeEntry(jarOutputStream, String.format("META-INF/versions/%d/multi-release.dat", MULTI_JAR_VERSION), MULTI_JAR_VERSION); } else { From c6096e21a120b5de47582abdd74b513484f83889 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 3 Oct 2023 21:39:15 -0700 Subject: [PATCH 0530/1215] Set CI image locale to en_US.UTF-8 Closes gh-37673 --- ci/images/ci-image-jdk21/Dockerfile | 3 +++ ci/images/ci-image/Dockerfile | 3 +++ ci/images/setup.sh | 5 +++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/ci/images/ci-image-jdk21/Dockerfile b/ci/images/ci-image-jdk21/Dockerfile index 4a7571121088..d8ed447fccbc 100644 --- a/ci/images/ci-image-jdk21/Dockerfile +++ b/ci/images/ci-image-jdk21/Dockerfile @@ -6,6 +6,9 @@ ADD get-docker-url.sh /get-docker-url.sh ADD get-docker-compose-url.sh /get-docker-compose-url.sh RUN ./setup.sh java17 java21 +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en +ENV LC_ALL en_US.UTF-8 ENV JAVA_HOME /opt/openjdk ENV PATH $JAVA_HOME/bin:$PATH ADD docker-lib.sh /docker-lib.sh diff --git a/ci/images/ci-image/Dockerfile b/ci/images/ci-image/Dockerfile index d4d2ce1b08a4..37a572f7e6e4 100644 --- a/ci/images/ci-image/Dockerfile +++ b/ci/images/ci-image/Dockerfile @@ -6,6 +6,9 @@ ADD get-docker-url.sh /get-docker-url.sh ADD get-docker-compose-url.sh /get-docker-compose-url.sh RUN ./setup.sh java17 +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en +ENV LC_ALL en_US.UTF-8 ENV JAVA_HOME /opt/openjdk ENV PATH $JAVA_HOME/bin:$PATH ADD docker-lib.sh /docker-lib.sh diff --git a/ci/images/setup.sh b/ci/images/setup.sh index 48df23b70732..dfde3d20dd9c 100755 --- a/ci/images/setup.sh +++ b/ci/images/setup.sh @@ -2,12 +2,13 @@ set -ex ########################################################### -# UTILS +# OS and UTILS ########################################################### export DEBIAN_FRONTEND=noninteractive apt-get update -apt-get install --no-install-recommends -y tzdata ca-certificates net-tools libxml2-utils git curl libudev1 libxml2-utils iptables iproute2 jq +apt-get install --no-install-recommends -y locales tzdata ca-certificates net-tools libxml2-utils git curl libudev1 libxml2-utils iptables iproute2 jq +locale-gen en_US.utf8 ln -fs /usr/share/zoneinfo/UTC /etc/localtime dpkg-reconfigure --frontend noninteractive tzdata rm -rf /var/lib/apt/lists/* From 30f29dead81325ba196372f403cbfac1f83897bc Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 4 Oct 2023 08:49:10 +0200 Subject: [PATCH 0531/1215] Revert "Temporarily remove auto-config for Reactor context propagation" This reverts commit 88de3cc0890e687217e5b673dc911965098b2bda. See gh-34201 --- .../spring-boot-autoconfigure/build.gradle | 1 + .../reactor/ReactorAutoConfiguration.java | 43 +++++++++++++ .../reactor/ReactorProperties.java | 57 +++++++++++++++++ .../autoconfigure/reactor/package-info.java | 20 ++++++ ...itional-spring-configuration-metadata.json | 4 ++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../ReactorAutoConfigurationTests.java | 64 +++++++++++++++++++ .../docs/asciidoc/actuator/observability.adoc | 3 + .../spring-boot-parent/build.gradle | 7 ++ 9 files changed, 200 insertions(+) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/ReactorAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/ReactorProperties.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/package-info.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/reactor/ReactorAutoConfigurationTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index 002b4788aadb..25d111149932 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -220,6 +220,7 @@ dependencies { testImplementation("com.mysql:mysql-connector-j") testImplementation("com.squareup.okhttp3:mockwebserver") testImplementation("com.sun.xml.messaging.saaj:saaj-impl") + testImplementation("io.micrometer:context-propagation") testImplementation("io.projectreactor:reactor-test") testImplementation("io.r2dbc:r2dbc-h2") testImplementation("jakarta.json:jakarta.json-api") diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/ReactorAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/ReactorAutoConfiguration.java new file mode 100644 index 000000000000..c247b1a9c0f1 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/ReactorAutoConfiguration.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2023 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.reactor; + +import reactor.core.publisher.Hooks; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Reactor. + * + * @author Brian Clozel + * @since 3.0.2 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(Hooks.class) +@EnableConfigurationProperties(ReactorProperties.class) +public class ReactorAutoConfiguration { + + public ReactorAutoConfiguration(ReactorProperties properties) { + if (properties.getContextPropagation() == ReactorProperties.ContextPropagationMode.AUTO) { + Hooks.enableAutomaticContextPropagation(); + } + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/ReactorProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/ReactorProperties.java new file mode 100644 index 000000000000..6725ee1ba4f3 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/ReactorProperties.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2023 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.reactor; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for Reactor. + * + * @author Brian Clozel + * @since 3.0.3 + */ +@ConfigurationProperties(prefix = "spring.reactor") +public class ReactorProperties { + + /** + * Context Propagation support mode for Reactor operators. + */ + private ContextPropagationMode contextPropagation = ContextPropagationMode.AUTO; + + public ContextPropagationMode getContextPropagation() { + return this.contextPropagation; + } + + public void setContextPropagation(ContextPropagationMode contextPropagation) { + this.contextPropagation = contextPropagation; + } + + public enum ContextPropagationMode { + + /** + * Context Propagation is applied to all Reactor operators. + */ + AUTO, + + /** + * Context Propagation is only applied to "tap" and "handle" Reactor operators. + */ + LIMITED + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/package-info.java new file mode 100644 index 000000000000..4b55cfe4d534 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Auto-configuration for Reactor. + */ +package org.springframework.boot.autoconfigure.reactor; 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 d1d75b8e1924..f12c04ea45c0 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 @@ -2090,6 +2090,10 @@ "level": "error" } }, + { + "name": "spring.reactor.context-propagation", + "defaultValue": "auto" + }, { "name": "spring.reactor.stacktrace-mode.enabled", "description": "Whether Reactor should collect stacktrace information at runtime.", 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 c8fbc3b8d57b..3a2a6ab1e79c 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 @@ -97,6 +97,7 @@ org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration org.springframework.boot.autoconfigure.pulsar.PulsarAutoConfiguration org.springframework.boot.autoconfigure.pulsar.PulsarReactiveAutoConfiguration org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration +org.springframework.boot.autoconfigure.reactor.ReactorAutoConfiguration org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration org.springframework.boot.autoconfigure.r2dbc.R2dbcTransactionManagerAutoConfiguration org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/reactor/ReactorAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/reactor/ReactorAutoConfigurationTests.java new file mode 100644 index 000000000000..73642c5883e9 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/reactor/ReactorAutoConfigurationTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2023 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.reactor; + +import java.util.concurrent.atomic.AtomicReference; + +import io.micrometer.context.ContextRegistry; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.util.context.Context; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ReactorAutoConfiguration}. + * + * @author Brian Clozel + */ +class ReactorAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ReactorAutoConfiguration.class)); + + private static final String THREADLOCAL_KEY = "ReactorAutoConfigurationTests"; + + private static final ThreadLocal THREADLOCAL_VALUE = ThreadLocal.withInitial(() -> "failure"); + + @BeforeAll + static void initializeThreadLocalAccessors() { + ContextRegistry globalRegistry = ContextRegistry.getInstance(); + globalRegistry.registerThreadLocalAccessor(THREADLOCAL_KEY, THREADLOCAL_VALUE); + } + + @Test + void shouldConfigureAutomaticContextPropagation() { + AtomicReference threadLocalValue = new AtomicReference<>(); + this.contextRunner.run((applicationContext) -> { + Mono.just("test") + .doOnNext((element) -> threadLocalValue.set(THREADLOCAL_VALUE.get())) + .contextWrite(Context.of(THREADLOCAL_KEY, "success")) + .block(); + assertThat(threadLocalValue.get()).isEqualTo("success"); + }); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc index 2b8fcccdb419..b1256b684e07 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc @@ -80,4 +80,7 @@ The attributes of the `Resource` can be configured via the configprop:management NOTE: Spring Boot does not provide auto-configuration for OpenTelemetry metrics or logging. OpenTelemetry tracing is only auto-configured when used together with <>. +Observability support relies on the https://github.com/micrometer-metrics/context-propagation[Context Propagation library] for forwarding the current observation across threads and reactive pipelines. +`ThreadLocal` values are automatically reinstated in reactive operators, this behavior is controlled with the configprop:spring.reactor.context-propagation[] property. + The next sections will provide more details about logging, metrics and traces. diff --git a/spring-boot-project/spring-boot-parent/build.gradle b/spring-boot-project/spring-boot-parent/build.gradle index a90f00a6960a..0c0f26a2253c 100644 --- a/spring-boot-project/spring-boot-parent/build.gradle +++ b/spring-boot-project/spring-boot-parent/build.gradle @@ -138,6 +138,13 @@ bom { ] } } + library("Micrometer Context Propagation", "1.0.2") { + group("io.micrometer") { + modules = [ + "context-propagation" + ] + } + } library("MockK", "1.13.5") { group("io.mockk") { modules = [ From 5b129ceb09b3b239366060ec90de8b421117930e Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 4 Oct 2023 09:11:40 +0200 Subject: [PATCH 0532/1215] Switch reactor context propagation from auto to limited - Polish - Fix @since tags - Add a test case - Update context-propagation version Closes gh-34201 --- .../reactor/ReactorAutoConfiguration.java | 4 +-- .../reactor/ReactorProperties.java | 4 +-- ...itional-spring-configuration-metadata.json | 2 +- .../ReactorAutoConfigurationTests.java | 28 ++++++++++++++++--- .../docs/asciidoc/actuator/observability.adoc | 9 +++--- .../spring-boot-parent/build.gradle | 2 +- 6 files changed, 35 insertions(+), 14 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/ReactorAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/ReactorAutoConfiguration.java index c247b1a9c0f1..9323e6eca46a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/ReactorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/ReactorAutoConfiguration.java @@ -27,14 +27,14 @@ * {@link EnableAutoConfiguration Auto-configuration} for Reactor. * * @author Brian Clozel - * @since 3.0.2 + * @since 3.2.0 */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(Hooks.class) @EnableConfigurationProperties(ReactorProperties.class) public class ReactorAutoConfiguration { - public ReactorAutoConfiguration(ReactorProperties properties) { + ReactorAutoConfiguration(ReactorProperties properties) { if (properties.getContextPropagation() == ReactorProperties.ContextPropagationMode.AUTO) { Hooks.enableAutomaticContextPropagation(); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/ReactorProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/ReactorProperties.java index 6725ee1ba4f3..c82da8b52389 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/ReactorProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/ReactorProperties.java @@ -22,7 +22,7 @@ * Configuration properties for Reactor. * * @author Brian Clozel - * @since 3.0.3 + * @since 3.2.0 */ @ConfigurationProperties(prefix = "spring.reactor") public class ReactorProperties { @@ -30,7 +30,7 @@ public class ReactorProperties { /** * Context Propagation support mode for Reactor operators. */ - private ContextPropagationMode contextPropagation = ContextPropagationMode.AUTO; + private ContextPropagationMode contextPropagation = ContextPropagationMode.LIMITED; public ContextPropagationMode getContextPropagation() { return this.contextPropagation; 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 f12c04ea45c0..bfd976e9b926 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 @@ -2092,7 +2092,7 @@ }, { "name": "spring.reactor.context-propagation", - "defaultValue": "auto" + "defaultValue": "limited" }, { "name": "spring.reactor.stacktrace-mode.enabled", diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/reactor/ReactorAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/reactor/ReactorAutoConfigurationTests.java index 73642c5883e9..32897e1b3a86 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/reactor/ReactorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/reactor/ReactorAutoConfigurationTests.java @@ -19,6 +19,7 @@ import java.util.concurrent.atomic.AtomicReference; import io.micrometer.context.ContextRegistry; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; @@ -33,6 +34,7 @@ * Tests for {@link ReactorAutoConfiguration}. * * @author Brian Clozel + * @author Moritz Halbritter */ class ReactorAutoConfigurationTests { @@ -41,7 +43,7 @@ class ReactorAutoConfigurationTests { private static final String THREADLOCAL_KEY = "ReactorAutoConfigurationTests"; - private static final ThreadLocal THREADLOCAL_VALUE = ThreadLocal.withInitial(() -> "failure"); + private static final ThreadLocal THREADLOCAL_VALUE = ThreadLocal.withInitial(() -> "initial"); @BeforeAll static void initializeThreadLocalAccessors() { @@ -49,15 +51,33 @@ static void initializeThreadLocalAccessors() { globalRegistry.registerThreadLocalAccessor(THREADLOCAL_KEY, THREADLOCAL_VALUE); } + @AfterAll + static void removeThreadLocalAccessors() { + ContextRegistry globalRegistry = ContextRegistry.getInstance(); + globalRegistry.removeThreadLocalAccessor(THREADLOCAL_KEY); + } + @Test - void shouldConfigureAutomaticContextPropagation() { + void shouldNotConfigurePropagationByDefault() { AtomicReference threadLocalValue = new AtomicReference<>(); this.contextRunner.run((applicationContext) -> { Mono.just("test") .doOnNext((element) -> threadLocalValue.set(THREADLOCAL_VALUE.get())) - .contextWrite(Context.of(THREADLOCAL_KEY, "success")) + .contextWrite(Context.of(THREADLOCAL_KEY, "updated")) + .block(); + assertThat(threadLocalValue.get()).isEqualTo("initial"); + }); + } + + @Test + void shouldConfigurePropagationIfSetToAuto() { + AtomicReference threadLocalValue = new AtomicReference<>(); + this.contextRunner.withPropertyValues("spring.reactor.context-propagation=auto").run((applicationContext) -> { + Mono.just("test") + .doOnNext((element) -> threadLocalValue.set(THREADLOCAL_VALUE.get())) + .contextWrite(Context.of(THREADLOCAL_KEY, "updated")) .block(); - assertThat(threadLocalValue.get()).isEqualTo("success"); + assertThat(threadLocalValue.get()).isEqualTo("updated"); }); } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc index b1256b684e07..dbf2e3259768 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc @@ -14,7 +14,11 @@ NOTE: Low cardinality key-values will be added to metrics and traces, while high Beans of type `ObservationPredicate`, `GlobalObservationConvention`, `ObservationFilter` and `ObservationHandler` will be automatically registered on the `ObservationRegistry`. You can additionally register any number of `ObservationRegistryCustomizer` beans to further configure the registry. -For more details please see the https://micrometer.io/docs/observation[Micrometer Observation documentation]. +Observability support relies on the https://github.com/micrometer-metrics/context-propagation[Context Propagation library] for forwarding the current observation across threads and reactive pipelines. +By default, `ThreadLocal` values are not automatically reinstated in reactive operators. +This behavior is controlled with the configprop:spring.reactor.context-propagation[] property, which can be set to `auto` to enable automatic propagation. + +For more details about observations please see the https://micrometer.io/docs/observation[Micrometer Observation documentation]. TIP: Observability for JDBC can be configured using a separate project. The https://github.com/jdbc-observations/datasource-micrometer[Datasource Micrometer project] provides a Spring Boot starter which automatically creates observations when JDBC operations are invoked. @@ -80,7 +84,4 @@ The attributes of the `Resource` can be configured via the configprop:management NOTE: Spring Boot does not provide auto-configuration for OpenTelemetry metrics or logging. OpenTelemetry tracing is only auto-configured when used together with <>. -Observability support relies on the https://github.com/micrometer-metrics/context-propagation[Context Propagation library] for forwarding the current observation across threads and reactive pipelines. -`ThreadLocal` values are automatically reinstated in reactive operators, this behavior is controlled with the configprop:spring.reactor.context-propagation[] property. - The next sections will provide more details about logging, metrics and traces. diff --git a/spring-boot-project/spring-boot-parent/build.gradle b/spring-boot-project/spring-boot-parent/build.gradle index 0c0f26a2253c..85e291bf4fcc 100644 --- a/spring-boot-project/spring-boot-parent/build.gradle +++ b/spring-boot-project/spring-boot-parent/build.gradle @@ -138,7 +138,7 @@ bom { ] } } - library("Micrometer Context Propagation", "1.0.2") { + library("Micrometer Context Propagation", "1.0.5") { group("io.micrometer") { modules = [ "context-propagation" From fc9c1c09647b2eb5053513d547845ea264f544f9 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 4 Oct 2023 10:59:34 +0200 Subject: [PATCH 0533/1215] Fix ordering related issue in ReactorAutoConfigurationTests See gh-34201 --- .../reactor/ReactorAutoConfigurationTests.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/reactor/ReactorAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/reactor/ReactorAutoConfigurationTests.java index 32897e1b3a86..1587fc2050db 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/reactor/ReactorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/reactor/ReactorAutoConfigurationTests.java @@ -20,8 +20,11 @@ import io.micrometer.context.ContextRegistry; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; import reactor.util.context.Context; @@ -45,6 +48,12 @@ class ReactorAutoConfigurationTests { private static final ThreadLocal THREADLOCAL_VALUE = ThreadLocal.withInitial(() -> "initial"); + @BeforeEach + @AfterEach + void resetStaticState() { + Hooks.disableAutomaticContextPropagation(); + } + @BeforeAll static void initializeThreadLocalAccessors() { ContextRegistry globalRegistry = ContextRegistry.getInstance(); From 993ac9c16fecc26bb8c5e1c171949bc2299fd023 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 4 Oct 2023 10:24:51 +0100 Subject: [PATCH 0534/1215] Start building against Micrometer 1.12.0 snapshots See gh-37703 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 3f7bac1a79b3..e1c0fe23210a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -998,7 +998,7 @@ bom { ] } } - library("Micrometer", "1.12.0-M3") { + library("Micrometer", "1.12.0-SNAPSHOT") { considerSnapshots() group("io.micrometer") { modules = [ From a630baf32abb558f1b4b8e896ab8ea0dbea924b8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 4 Oct 2023 10:24:56 +0100 Subject: [PATCH 0535/1215] Start building against Micrometer Tracing 1.2.0 snapshots See gh-37704 --- .../spring-boot-actuator-autoconfigure/build.gradle | 1 + .../opentelemetry/OpenTelemetryAutoConfiguration.java | 7 ++++--- .../OpenTelemetryAutoConfigurationTests.java | 11 +++++------ .../tracing/OpenTelemetryAutoConfigurationTests.java | 6 ++---- .../spring-boot-dependencies/build.gradle | 2 +- src/checkstyle/checkstyle.xml | 2 ++ 6 files changed, 15 insertions(+), 14 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index a55874e629f6..38cfeca4648b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -68,6 +68,7 @@ dependencies { optional("io.micrometer:micrometer-registry-signalfx") optional("io.micrometer:micrometer-registry-statsd") optional("io.micrometer:micrometer-registry-wavefront") + optional("io.zipkin.reporter2:zipkin-reporter-brave") optional("io.zipkin.reporter2:zipkin-sender-urlconnection") optional("io.opentelemetry:opentelemetry-exporter-zipkin") optional("io.opentelemetry:opentelemetry-exporter-otlp") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java index b0874f7791f7..3ffe4d0bb386 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java @@ -17,6 +17,7 @@ package org.springframework.boot.actuate.autoconfigure.opentelemetry; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.sdk.OpenTelemetrySdk; @@ -52,6 +53,8 @@ public class OpenTelemetryAutoConfiguration { */ private static final String DEFAULT_APPLICATION_NAME = "application"; + static final AttributeKey ATTRIBUTE_KEY_SERVICE_NAME = AttributeKey.stringKey("service.name"); + @Bean @ConditionalOnMissingBean(OpenTelemetry.class) OpenTelemetrySdk openTelemetry(ObjectProvider tracerProvider, @@ -67,12 +70,10 @@ OpenTelemetrySdk openTelemetry(ObjectProvider tracerProvider, @Bean @ConditionalOnMissingBean - @SuppressWarnings("deprecation") Resource openTelemetryResource(Environment environment, OpenTelemetryProperties properties) { String applicationName = environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME); return Resource.getDefault() - .merge(Resource.create(Attributes - .of(io.opentelemetry.semconv.resource.attributes.ResourceAttributes.SERVICE_NAME, applicationName))) + .merge(Resource.create(Attributes.of(ATTRIBUTE_KEY_SERVICE_NAME, applicationName))) .merge(toResource(properties)); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java index 6a7570563180..167be648bf18 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java @@ -24,6 +24,7 @@ import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.semconv.ResourceAttributes; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -81,22 +82,20 @@ void backsOffOnUserSuppliedBeans() { } @Test - @SuppressWarnings("deprecation") void shouldApplySpringApplicationNameToResource() { this.runner.withPropertyValues("spring.application.name=my-application").run((context) -> { Resource resource = context.getBean(Resource.class); - assertThat(resource.getAttributes().asMap()).contains(entry( - io.opentelemetry.semconv.resource.attributes.ResourceAttributes.SERVICE_NAME, "my-application")); + assertThat(resource.getAttributes().asMap()) + .contains(entry(ResourceAttributes.SERVICE_NAME, "my-application")); }); } @Test - @SuppressWarnings("deprecation") void shouldFallbackToDefaultApplicationNameIfSpringApplicationNameIsNotSet() { this.runner.run((context) -> { Resource resource = context.getBean(Resource.class); - assertThat(resource.getAttributes().asMap()).contains( - entry(io.opentelemetry.semconv.resource.attributes.ResourceAttributes.SERVICE_NAME, "application")); + assertThat(resource.getAttributes().asMap()) + .contains(entry(ResourceAttributes.SERVICE_NAME, "application")); }); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java index 2d02df024c92..64bb18571381 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java @@ -50,6 +50,7 @@ import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.export.SpanExporter; import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.semconv.ResourceAttributes; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -169,7 +170,6 @@ void shouldBackOffOnCustomBeans() { } @Test - @SuppressWarnings("deprecation") void shouldSetupDefaultResourceAttributes() { this.contextRunner .withConfiguration( @@ -182,9 +182,7 @@ void shouldSetupDefaultResourceAttributes() { exporter.await(Duration.ofSeconds(10)); SpanData spanData = exporter.getExportedSpans().get(0); Map, Object> expectedAttributes = Resource.getDefault() - .merge(Resource.create( - Attributes.of(io.opentelemetry.semconv.resource.attributes.ResourceAttributes.SERVICE_NAME, - "application"))) + .merge(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, "application"))) .getAttributes() .asMap(); assertThat(spanData.getResource().getAttributes().asMap()).isEqualTo(expectedAttributes); diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e1c0fe23210a..32391033f4e7 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1011,7 +1011,7 @@ bom { ] } } - library("Micrometer Tracing", "1.2.0-M3") { + library("Micrometer Tracing", "1.2.0-SNAPSHOT") { considerSnapshots() calendarName = "Tracing" group("io.micrometer") { diff --git a/src/checkstyle/checkstyle.xml b/src/checkstyle/checkstyle.xml index 92925689726e..2d5403256356 100644 --- a/src/checkstyle/checkstyle.xml +++ b/src/checkstyle/checkstyle.xml @@ -24,6 +24,8 @@ + From 77e1d8fa20f7601ddb9be7dd3624d5787eb16bb4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 4 Oct 2023 10:25:01 +0100 Subject: [PATCH 0536/1215] Start building against Reactor Bom 2023.0.0 snapshots See gh-37705 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 32391033f4e7..544be38a55e5 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1329,7 +1329,7 @@ bom { ] } } - library("Reactor Bom", "2023.0.0-M3") { + library("Reactor Bom", "2023.0.0-SNAPSHOT") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From f7c3fe165d737f44c62fe0724fbb1d9894426006 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 4 Oct 2023 10:25:06 +0100 Subject: [PATCH 0537/1215] Start building against Spring AMQP 3.1.0 snapshots See gh-37706 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 544be38a55e5..b1b2939b5f2e 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1497,7 +1497,7 @@ bom { ] } } - library("Spring AMQP", "3.1.0-M1") { + library("Spring AMQP", "3.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.amqp") { imports = [ From 429bdb266aebf4b21d1581fcb572754d01bdb00d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 4 Oct 2023 10:25:11 +0100 Subject: [PATCH 0538/1215] Start building against Spring Authorization Server 1.2.0 snapshots See gh-37707 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index b1b2939b5f2e..575f4f957c43 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1505,7 +1505,7 @@ bom { ] } } - library("Spring Authorization Server", "1.2.0-M1") { + library("Spring Authorization Server", "1.2.0-SNAPSHOT") { considerSnapshots() group("org.springframework.security") { modules = [ From 1ce75cd1a9574ab53a21366fbc385b4c76108aff Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 4 Oct 2023 10:25:16 +0100 Subject: [PATCH 0539/1215] Start building against Spring Batch 5.1.0 snapshots See gh-37708 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 575f4f957c43..c072ae5d570a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1513,7 +1513,7 @@ bom { ] } } - library("Spring Batch", "5.1.0-M3") { + library("Spring Batch", "5.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.batch") { imports = [ From a4f6b15ffb619df8e338b66bff549dac52078e9d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 4 Oct 2023 10:25:21 +0100 Subject: [PATCH 0540/1215] Start building against Spring Data Bom 2023.1.0 snapshots See gh-37709 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c072ae5d570a..29347940b527 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1521,7 +1521,7 @@ bom { ] } } - library("Spring Data Bom", "2023.1.0-M3") { + library("Spring Data Bom", "2023.1.0-SNAPSHOT") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From 06f48e8c49a5daf7a44ce087a6fedc1d3ebe4295 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 4 Oct 2023 10:25:26 +0100 Subject: [PATCH 0541/1215] Start building against Spring Framework 6.1.0 snapshots See gh-37710 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 3fd92d3868c0..b0ca86da743d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 kotlinVersion=1.9.10 nativeBuildToolsVersion=0.9.27 -springFrameworkVersion=6.1.0-M5 +springFrameworkVersion=6.1.0-SNAPSHOT tomcatVersion=10.1.13 kotlin.stdlib.default.dependency=false From c45bda4c48d134e6bd28162642682283c3b5efb5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 4 Oct 2023 10:25:31 +0100 Subject: [PATCH 0542/1215] Start building against Spring Integration 6.2.0 snapshots See gh-37711 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 29347940b527..ddd375febfb2 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1556,7 +1556,7 @@ bom { ] } } - library("Spring Integration", "6.2.0-M3") { + library("Spring Integration", "6.2.0-SNAPSHOT") { considerSnapshots() group("org.springframework.integration") { imports = [ From cccd8bfea38acbf1d4bd88626be7edb82e98ce70 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 4 Oct 2023 10:25:36 +0100 Subject: [PATCH 0543/1215] Start building against Spring Kafka 3.1.0 snapshots See gh-37712 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index ddd375febfb2..a17e79e42ea7 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1564,7 +1564,7 @@ bom { ] } } - library("Spring Kafka", "3.1.0-M1") { + library("Spring Kafka", "3.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.kafka") { modules = [ From 26b889f31a6778e69f6ca0730f82e11fabf3609d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 4 Oct 2023 10:25:40 +0100 Subject: [PATCH 0544/1215] Start building against Spring LDAP 3.2.0 snapshots See gh-37713 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index a17e79e42ea7..b98182375318 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1573,7 +1573,7 @@ bom { ] } } - library("Spring LDAP", "3.2.0-M3") { + library("Spring LDAP", "3.2.0-SNAPSHOT") { considerSnapshots() group("org.springframework.ldap") { modules = [ From 5f7bdfc35684cf4463e9ceca7603ea8dab3094c6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 4 Oct 2023 10:25:45 +0100 Subject: [PATCH 0545/1215] Start building against Spring Retry 2.0.4 snapshots See gh-37714 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index b98182375318..5ced6f5d0faa 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1602,7 +1602,7 @@ bom { ] } } - library("Spring Retry", "2.0.3") { + library("Spring Retry", "2.0.4-SNAPSHOT") { considerSnapshots() group("org.springframework.retry") { modules = [ From 4e21896b0de11d5085c1bfcc33f6ef16f228fb4d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 4 Oct 2023 10:25:50 +0100 Subject: [PATCH 0546/1215] Start building against Spring Security 6.2.0 snapshots See gh-37715 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5ced6f5d0faa..1ae332d6e2e2 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1610,7 +1610,7 @@ bom { ] } } - library("Spring Security", "6.2.0-M3") { + library("Spring Security", "6.2.0-SNAPSHOT") { considerSnapshots() group("org.springframework.security") { imports = [ From ee00014dfe882023b5fb6aea07b5411cc36fac0a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 4 Oct 2023 10:25:55 +0100 Subject: [PATCH 0547/1215] Start building against Spring Session 3.2.0 snapshots See gh-37716 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 1ae332d6e2e2..74402a1f3183 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1618,7 +1618,7 @@ bom { ] } } - library("Spring Session", "3.2.0-M1") { + library("Spring Session", "3.2.0-SNAPSHOT") { considerSnapshots() prohibit { startsWith(["Apple-", "Bean-", "Corn-", "Dragonfruit-"]) From 361f737086c3bbfb84932e1f84ad1271a48569a8 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Wed, 4 Oct 2023 13:33:24 -0500 Subject: [PATCH 0548/1215] Fix launcher path in Paketo system tests See gh-37667 --- .../springframework/boot/image/paketo/PaketoBuilderTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java b/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java index 76e642d3f22f..82d0fdd7d80a 100644 --- a/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java +++ b/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java @@ -94,9 +94,9 @@ void executableJarApp() throws Exception { "paketo-buildpacks/executable-jar", "paketo-buildpacks/dist-zip", "paketo-buildpacks/spring-boot"); metadata.processOfType("web") - .containsExactly("java", "org.springframework.boot.loader.launch.launch.JarLauncher"); + .containsExactly("java", "org.springframework.boot.loader.launch.JarLauncher"); metadata.processOfType("executable-jar") - .containsExactly("java", "org.springframework.boot.loader.launch.launch.JarLauncher"); + .containsExactly("java", "org.springframework.boot.loader.launch.JarLauncher"); }); assertImageHasJvmSbomLayer(imageReference, config); assertImageHasDependenciesSbomLayer(imageReference, config, "executable-jar"); From 0a16ec17e9d703a1f9dde88fe80d161d019a20cf Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 4 Oct 2023 15:11:47 +0200 Subject: [PATCH 0549/1215] Add property to enable key verification on PEM SSL bundles Closes gh-37727 --- .../ssl/PemSslBundleProperties.java | 14 +++ .../ssl/PropertiesSslBundle.java | 3 +- .../boot/ssl/pem/KeyVerifier.java | 104 ++++++++++++++++++ .../boot/ssl/pem/PemSslStoreBundle.java | 96 ++++++++++++---- .../boot/ssl/pem/KeyVerifierTests.java | 90 +++++++++++++++ .../boot/ssl/pem/PemSslStoreBundleTests.java | 30 +++++ .../org/springframework/boot/ssl/pem/ca.crt | 19 ++++ .../org/springframework/boot/ssl/pem/ca.pem | 27 +++++ .../org/springframework/boot/ssl/pem/key1.crt | 19 ++++ .../org/springframework/boot/ssl/pem/key1.pem | 28 +++++ .../boot/ssl/pem/key2-chain.crt | 38 +++++++ .../org/springframework/boot/ssl/pem/key2.crt | 19 ++++ .../org/springframework/boot/ssl/pem/key2.pem | 28 +++++ 13 files changed, 494 insertions(+), 21 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/KeyVerifier.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/KeyVerifierTests.java create mode 100644 spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/ca.crt create mode 100644 spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/ca.pem create mode 100644 spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key1.crt create mode 100644 spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key1.pem create mode 100644 spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key2-chain.crt create mode 100644 spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key2.crt create mode 100644 spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key2.pem diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemSslBundleProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemSslBundleProperties.java index 16798cb2cb05..d98939350f96 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemSslBundleProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemSslBundleProperties.java @@ -23,6 +23,7 @@ * * @author Scott Frederick * @author Phillip Webb + * @author Moritz Halbritter * @since 3.1.0 * @see PemSslStoreBundle */ @@ -38,6 +39,11 @@ public class PemSslBundleProperties extends SslBundleProperties { */ private final Store truststore = new Store(); + /** + * Whether to verify that the private key matches the public key. + */ + private boolean verifyKeys; + public Store getKeystore() { return this.keystore; } @@ -46,6 +52,14 @@ public Store getTruststore() { return this.truststore; } + public boolean isVerifyKeys() { + return this.verifyKeys; + } + + public void setVerifyKeys(boolean verifyKeys) { + this.verifyKeys = verifyKeys; + } + /** * Store properties. */ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java index b25b88ad14f6..a1c8e9522f95 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java @@ -109,7 +109,8 @@ public static SslBundle get(JksSslBundleProperties properties) { private static SslStoreBundle asSslStoreBundle(PemSslBundleProperties properties) { PemSslStoreDetails keyStoreDetails = asStoreDetails(properties.getKeystore()); PemSslStoreDetails trustStoreDetails = asStoreDetails(properties.getTruststore()); - return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, properties.getKey().getAlias()); + return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, properties.getKey().getAlias(), null, + properties.isVerifyKeys()); } private static PemSslStoreDetails asStoreDetails(PemSslBundleProperties.Store properties) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/KeyVerifier.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/KeyVerifier.java new file mode 100644 index 000000000000..6b79c0a305e9 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/KeyVerifier.java @@ -0,0 +1,104 @@ +/* + * Copyright 2012-2023 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.ssl.pem; + +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; + +/** + * Performs checks on keys, e.g., if a public key and a private key belong together. + * + * @author Moritz Halbritter + */ +class KeyVerifier { + + private static final byte[] DATA = "Just some piece of data which gets signed".getBytes(StandardCharsets.UTF_8); + + /** + * Checks if the given private key belongs to the given public key. + * @param privateKey the private key + * @param publicKey the public key + * @return whether the keys belong together + */ + Result matches(PrivateKey privateKey, PublicKey publicKey) { + try { + if (!privateKey.getAlgorithm().equals(publicKey.getAlgorithm())) { + // Keys are of different type + return Result.NO; + } + String algorithm = getSignatureAlgorithm(privateKey.getAlgorithm()); + if (algorithm == null) { + return Result.UNKNOWN; + } + byte[] signature = createSignature(privateKey, algorithm); + return verifySignature(publicKey, algorithm, signature); + } + catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException ex) { + return Result.UNKNOWN; + } + } + + private static byte[] createSignature(PrivateKey privateKey, String algorithm) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + Signature signer = Signature.getInstance(algorithm); + signer.initSign(privateKey); + signer.update(DATA); + return signer.sign(); + } + + private static Result verifySignature(PublicKey publicKey, String algorithm, byte[] signature) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + Signature verifier = Signature.getInstance(algorithm); + verifier.initVerify(publicKey); + verifier.update(DATA); + try { + if (verifier.verify(signature)) { + return Result.YES; + } + else { + return Result.NO; + } + } + catch (SignatureException ex) { + return Result.NO; + } + } + + private static String getSignatureAlgorithm(String keyAlgorithm) { + // https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#signature-algorithms + // https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#keypairgenerator-algorithms + return switch (keyAlgorithm) { + case "RSA" -> "SHA256withRSA"; + case "DSA" -> "SHA256withDSA"; + case "EC" -> "SHA256withECDSA"; + case "EdDSA" -> "EdDSA"; + default -> null; + }; + } + + enum Result { + + YES, NO, UNKNOWN + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java index 250030766905..b71750f3612f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java @@ -16,12 +16,16 @@ package org.springframework.boot.ssl.pem; +import java.io.IOException; import java.security.KeyStore; import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; +import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import org.springframework.boot.ssl.SslStoreBundle; +import org.springframework.boot.ssl.pem.KeyVerifier.Result; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -71,8 +75,24 @@ public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails */ public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String keyAlias, String keyPassword) { - this.keyStore = createKeyStore("key", keyStoreDetails, keyAlias, keyPassword); - this.trustStore = createKeyStore("trust", trustStoreDetails, keyAlias, keyPassword); + this(keyStoreDetails, trustStoreDetails, keyAlias, keyPassword, false); + } + + /** + * Create a new {@link PemSslStoreBundle} instance. + * @param keyStoreDetails the key store details + * @param trustStoreDetails the trust store details + * @param keyAlias the key alias to use or {@code null} to use a default alias + * @param keyPassword the password to use for the key + * @param verifyKeys whether to verify that the private key matches the public key + * @since 3.2.0 + */ + public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String keyAlias, + String keyPassword, boolean verifyKeys) { + this.keyStore = createKeyStore("key", keyStoreDetails, (keyAlias != null) ? keyAlias : DEFAULT_KEY_ALIAS, + keyPassword, verifyKeys); + this.trustStore = createKeyStore("trust", trustStoreDetails, (keyAlias != null) ? keyAlias : DEFAULT_KEY_ALIAS, + keyPassword, verifyKeys); } @Override @@ -90,20 +110,25 @@ public KeyStore getTrustStore() { return this.trustStore; } - private KeyStore createKeyStore(String name, PemSslStoreDetails details, String alias, String keyPassword) { + private static KeyStore createKeyStore(String name, PemSslStoreDetails details, String keyAlias, String keyPassword, + boolean verifyKeys) { if (details == null || details.isEmpty()) { return null; } try { Assert.notNull(details.certificate(), "Certificate content must not be null"); - String type = (!StringUtils.hasText(details.type())) ? KeyStore.getDefaultType() : details.type(); - KeyStore store = KeyStore.getInstance(type); - store.load(null); - String certificateContent = PemContent.load(details.certificate()); - String privateKeyContent = PemContent.load(details.privateKey()); - X509Certificate[] certificates = PemCertificateParser.parse(certificateContent); - PrivateKey privateKey = PemPrivateKeyParser.parse(privateKeyContent, details.privateKeyPassword()); - addCertificates(store, certificates, privateKey, (alias != null) ? alias : DEFAULT_KEY_ALIAS, keyPassword); + KeyStore store = createKeyStore(details); + X509Certificate[] certificates = loadCertificates(details); + PrivateKey privateKey = loadPrivateKey(details); + if (privateKey != null) { + if (verifyKeys) { + verifyKeys(privateKey, certificates); + } + addPrivateKey(store, privateKey, keyAlias, keyPassword, certificates); + } + else { + addCertificates(store, certificates, keyAlias); + } return store; } catch (Exception ex) { @@ -111,17 +136,48 @@ private KeyStore createKeyStore(String name, PemSslStoreDetails details, String } } - private void addCertificates(KeyStore keyStore, X509Certificate[] certificates, PrivateKey privateKey, String alias, - String keyPassword) throws KeyStoreException { - if (privateKey != null) { - keyStore.setKeyEntry(alias, privateKey, (keyPassword != null) ? keyPassword.toCharArray() : null, - certificates); - } - else { - for (int index = 0; index < certificates.length; index++) { - keyStore.setCertificateEntry(alias + "-" + index, certificates[index]); + private static void verifyKeys(PrivateKey privateKey, X509Certificate[] certificates) { + KeyVerifier keyVerifier = new KeyVerifier(); + // Key should match one of the certificates + for (X509Certificate certificate : certificates) { + Result result = keyVerifier.matches(privateKey, certificate.getPublicKey()); + if (result == Result.YES) { + return; } } + throw new IllegalStateException("Private key matches none of the certificates"); + } + + private static PrivateKey loadPrivateKey(PemSslStoreDetails details) { + String privateKeyContent = PemContent.load(details.privateKey()); + return PemPrivateKeyParser.parse(privateKeyContent, details.privateKeyPassword()); + } + + private static X509Certificate[] loadCertificates(PemSslStoreDetails details) { + String certificateContent = PemContent.load(details.certificate()); + X509Certificate[] certificates = PemCertificateParser.parse(certificateContent); + Assert.state(certificates != null && certificates.length > 0, "Loaded certificates are empty"); + return certificates; + } + + private static KeyStore createKeyStore(PemSslStoreDetails details) + throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { + String type = StringUtils.hasText(details.type()) ? details.type() : KeyStore.getDefaultType(); + KeyStore store = KeyStore.getInstance(type); + store.load(null); + return store; + } + + private static void addPrivateKey(KeyStore keyStore, PrivateKey privateKey, String alias, String keyPassword, + X509Certificate[] certificates) throws KeyStoreException { + keyStore.setKeyEntry(alias, privateKey, (keyPassword != null) ? keyPassword.toCharArray() : null, certificates); + } + + private static void addCertificates(KeyStore keyStore, X509Certificate[] certificates, String alias) + throws KeyStoreException { + for (int index = 0; index < certificates.length; index++) { + keyStore.setCertificateEntry(alias + "-" + index, certificates[index]); + } } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/KeyVerifierTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/KeyVerifierTests.java new file mode 100644 index 000000000000..4f681ce77f92 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/KeyVerifierTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2023 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.ssl.pem; + +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.ECGenParameterSpec; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Named; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import org.springframework.boot.ssl.pem.KeyVerifier.Result; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link KeyVerifier}. + * + * @author Moritz Halbritter + */ +class KeyVerifierTests { + + private static final List ALGORITHMS = List.of(Algorithm.of("RSA"), Algorithm.of("DSA"), + Algorithm.of("ed25519"), Algorithm.of("ed448"), Algorithm.ec("secp256r1"), Algorithm.ec("secp521r1")); + + private final KeyVerifier keyVerifier = new KeyVerifier(); + + @ParameterizedTest(name = "{0}") + @MethodSource("arguments") + void test(PrivateKey privateKey, PublicKey publicKey, List invalidPublicKeys) { + assertThat(this.keyVerifier.matches(privateKey, publicKey)).isEqualTo(Result.YES); + for (PublicKey invalidPublicKey : invalidPublicKeys) { + assertThat(this.keyVerifier.matches(privateKey, invalidPublicKey)).isEqualTo(Result.NO); + } + } + + static Stream arguments() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + List keyPairs = new LinkedList<>(); + for (Algorithm algorithm : ALGORITHMS) { + KeyPairGenerator generator = KeyPairGenerator.getInstance(algorithm.name()); + if (algorithm.spec() != null) { + generator.initialize(algorithm.spec()); + } + keyPairs.add(generator.generateKeyPair()); + keyPairs.add(generator.generateKeyPair()); + } + return keyPairs.stream() + .map((kp) -> Arguments.arguments(Named.named(kp.getPrivate().getAlgorithm(), kp.getPrivate()), + kp.getPublic(), without(keyPairs, kp).map(KeyPair::getPublic).toList())); + } + + private static Stream without(List keyPairs, KeyPair without) { + return keyPairs.stream().filter((kp) -> !kp.equals(without)); + } + + private record Algorithm(String name, AlgorithmParameterSpec spec) { + static Algorithm of(String name) { + return new Algorithm(name, null); + } + + static Algorithm ec(String curve) { + return new Algorithm("EC", new ECGenParameterSpec(curve)); + } + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java index 29c22a27e0ce..cdb30bbc50e7 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java @@ -24,12 +24,14 @@ import org.springframework.util.function.ThrowingConsumer; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link PemSslStoreBundle}. * * @author Scott Frederick * @author Phillip Webb + * @author Moritz Halbritter */ class PemSslStoreBundleTests { @@ -131,6 +133,34 @@ void whenHasKeyStoreDetailsAndTrustStoreDetailsAndKeyPassword() { .satisfies(storeContainingCertAndKey("test-alias", "keysecret".toCharArray())); } + @Test + void shouldVerifyKeysIfEnabled() { + PemSslStoreDetails keyStoreDetails = PemSslStoreDetails + .forCertificate("classpath:org/springframework/boot/ssl/pem/key1.crt") + .withPrivateKey("classpath:org/springframework/boot/ssl/pem/key1.pem"); + PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, null, "test-alias", "keysecret", true); + assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias", "keysecret".toCharArray())); + } + + @Test + void shouldVerifyKeysIfEnabledAndCertificateChainIsUsed() { + PemSslStoreDetails keyStoreDetails = PemSslStoreDetails + .forCertificate("classpath:org/springframework/boot/ssl/pem/key2-chain.crt") + .withPrivateKey("classpath:org/springframework/boot/ssl/pem/key2.pem"); + PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, null, "test-alias", "keysecret", true); + assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias", "keysecret".toCharArray())); + } + + @Test + void shouldFailIfVerifyKeysIsEnabledAndKeysDontMatch() { + PemSslStoreDetails keyStoreDetails = PemSslStoreDetails + .forCertificate("classpath:org/springframework/boot/ssl/pem/key2.crt") + .withPrivateKey("classpath:org/springframework/boot/ssl/pem/key1.pem"); + assertThatIllegalStateException() + .isThrownBy(() -> new PemSslStoreBundle(keyStoreDetails, null, null, null, true)) + .withMessageContaining("Private key matches none of the certificates"); + } + private Consumer storeContainingCert(String keyAlias) { return storeContainingCert(KeyStore.getDefaultType(), keyAlias); } diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/ca.crt b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/ca.crt new file mode 100644 index 000000000000..b9343e01ef3f --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/ca.crt @@ -0,0 +1,19 @@ +-----BEGIN TRUSTED CERTIFICATE----- +MIIDIDCCAgsCFH3lh1RXOEy2ESqUPyzb+9zxMYUnMA0GCSqGSIb3DQEBCwUAME8x +CzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0Rl +ZmF1bHQgQ29tcGFueSBMdGQxCzAJBgNVBAMMAkNBMCAXDTIzMTAwNTA3MjU1M1oY +DzIxMjMwOTExMDcyNTUzWjBPMQswCQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVs +dCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMQswCQYDVQQDDAJD +QTCCAR4wDQYJKoZIhvcNAQEBBQADggELADCCAQYCgf4NNpc+6B3qvwKcRYgoXmJ4 +3wyWktBK7BdShz/YnW1OlFZ+R845ZiDw0KdzElZWkYqn+BYJus6lPIS5dfLcrGSf +a1e8IK02RpBiY/WJvupetnSk8gKA7emF94NlV4gXr4ICJAhXvXUFyBLpdEUE/lcg +lgCbVJzs5jWUnffEF9mrClzzo0+iXw34zwmyYyBTFmlOEr+QUEdAb6Lr/klpTVit +as2Ddg1QT4EaSIdTEpkVRZp2dyYVdqSxpaBq21xg0viDHsYQrP96IfacmUB7kFFn +HsnptDHFvJj2WSQDX+PRS7tLl4mmfizZg80eGfLD22ShNspRSGnbJc0OzegPiwID +AQABMA0GCSqGSIb3DQEBCwUAA4H/AAnC+FQqdeJaG5I7R+pNjgKplL2UsxW983kA +CVVkv/Dt0+4rbPC67o9/8Tr+g4eo/wUntMNo2ghF3oBItGr7pJE16zPiLwIvha9c +8BDhCEZWyhz3vkamZUi19lOnkm3zTmmDE/nX4WYH6CL4UWjxvniZYwW8AdVSnFXY +ncriuvfliLa3dw1SJ7FtxdcBn4yfzrZWcY+psYNHpftLGYRmQF/VCDSB9EAIEggr +yBcP749u2y8s44WvKAnnwfLcALIrylY25zN0pao/l2X8HI6qHUeA/QbbEBpDoQvR +du/rgaHCVvFFxATefhBJ0CUA1Nn5nrGwyRTKnZWtR080qwUp +-----END TRUSTED CERTIFICATE----- diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/ca.pem b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/ca.pem new file mode 100644 index 000000000000..c5102f84da50 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/ca.pem @@ -0,0 +1,27 @@ +-----BEGIN PRIVATE KEY----- +MIIEqQIBADANBgkqhkiG9w0BAQEFAASCBJMwggSPAgEAAoH+DTaXPugd6r8CnEWI +KF5ieN8MlpLQSuwXUoc/2J1tTpRWfkfOOWYg8NCncxJWVpGKp/gWCbrOpTyEuXXy +3Kxkn2tXvCCtNkaQYmP1ib7qXrZ0pPICgO3phfeDZVeIF6+CAiQIV711BcgS6XRF +BP5XIJYAm1Sc7OY1lJ33xBfZqwpc86NPol8N+M8JsmMgUxZpThK/kFBHQG+i6/5J +aU1YrWrNg3YNUE+BGkiHUxKZFUWadncmFXaksaWgattcYNL4gx7GEKz/eiH2nJlA +e5BRZx7J6bQxxbyY9lkkA1/j0Uu7S5eJpn4s2YPNHhnyw9tkoTbKUUhp2yXNDs3o +D4sCAwEAAQKB/goGHht1EC0kFyDihvbJE79Kx0v7uNT94nuTa1Yzp9bzJeLLKqHU +3qySPlZH1QP7icr/pAhhlZ85GB9yYXoTtopSbs6jo4QHaEWcO4vyL+8GT9tKVafl +1UDyktXw36fIV8Kz/zhA3GQ0clR1Bl9RbFumMHOmbx4xTvieFnbG+TQ2THfFccGS +jCO6+dab6daXs8sBt0rGMh72utIISVsFJc7v3B8BpaNOI4iBMciRSyZeE4Vw/lRg +e3iErAVUmUjBrUK/wBy/l9cbbpkp+rvhQpmTIPtKd5f29AQNL7p6V+2+yRb2woRk +0i1HwOHGOhiCTxXZB9/nZykaT/T2+J9BAn8+DEWCRcfifyNEyuE54G6BvLvgGTgs ++kXWS7p0+wO9CFBDZARu/MXFEfcWt4ZTIj8HtMiKhxNbC1LiGtQnJoLV6AM75E5Q +toh/xyYOnHbhnbhsSNcpJk5iIdqQE6hWh+rYXFr1aJFMRZaWRkcUG8iIxWQQjRvw +qxLm9GQtEhF7An82hAlPCDs+6kT1otBEN8vGaW8qkxWYJf6kSd/I0/TEKRYpIwBa +Ist2BN5GrJTitKhzQIq2ZyT2byHxS0VIvInZJ6sFC+V6fHYpzWbS3zkBy2zswfAZ +UYrdjLVv16qZYsdjUnhkyUaBbBXnrTPlPzxXvgTeqJeJ5tbR6wgeqPUxAn8lcQxE +t00N/UBQE8jjPu4QNc59RVqjsYaQ8POcAZjY6fpdIC6Ytsm0yMl8mNRiuCimws28 +4hOo/eVO8XeSBGgxIidJbdRgWjV2PbtWV85ZCO6v0Sic+TOVfe5AwMv1I2FwnBJ7 +QlVjXB6podDkbnuNJOfkIPJ6QRFP8qu8ksmfAn8mttuZeYIBawLv4eC/IVSgIc3l +UTC7rPfKGgBHMWaYS4lGS2n7mMwektR7IiJVYPBjcIlRgaw5KbDUF50rS2Elissj +uVANDQgpJYoI5KcqRBmlhRCKGmNgdIWA2Ip5hTGNskp3YIymamif71t0SNUEhpgU +u2tqbjlON/e7NkdxAn8VdVYq+4sAWRdU4VJqqyf8dyBx68sysvY6HYlKS2bpfu3C +J3gbPximDZhzMvKx2/CAzMbAT3anyr/DiUImk+QdWSmht+1SLH7A14MDjzQ0D5xt +GgPqWn7PtcJojFMjc/o5/fKgFf4CYkJhv2KycX9UeldBxpqNpNigzFWBLdtu +-----END PRIVATE KEY----- diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key1.crt b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key1.crt new file mode 100644 index 000000000000..e381ab69b3d8 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key1.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDJjCCAhECFHuJXZO0JDPtCSc1/r0llpyc/j9TMA0GCSqGSIb3DQEBCwUAME8x +CzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0Rl +ZmF1bHQgQ29tcGFueSBMdGQxCzAJBgNVBAMMAkNBMCAXDTIzMTAwNTA3Mjg0NVoY +DzIxMjMwOTExMDcyODQ1WjBRMQswCQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVs +dCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMQ0wCwYDVQQDDARr +ZXkxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoYU2afupPq/b6PIy +6MWDOMRdJk5uW51lrw6oudXpWlUQMXKdsaZT4sqbgjGLggfo7WWsPeCzQN3kIX3T +OqBog5EMkXnlQhAfP2Htj0uXPFj97leZ+FqJrzgPnZY8wSqDXfy9/ycR3PgWjRsS +GZJb05hTNVGTU2vpNQDDo+XBKgybB0afGU8Nk/InWfs1xd/Jv0YcVADQiQEmg41w +g18B3LMIBZPWIJUQ1b7wMlhxWaCNXHfB1bUTIYCUAUOZyEaxPaOOiJo32xKmqOlU +TCLM8zgWCBCEgHtQwSD0GMLhUarLPNE5GP3yo5qHBYqOque7BBjP4e58r6wAyBoe +7kMYRQIDAQABMA0GCSqGSIb3DQEBCwUAA4H/AAMIYpTDxgQwpfk+U1IhkqJjb+Uh +hj6KlT5TEdpn/saGYLZQECZAO21MWrUDTsV2Pax2Ee8ezarCg8Cthu4YOtPauPaL +XpyrIagUOgrDcmXr6QxMKUqifiMurLRFaAS7mWXp0TAFNgzDg3WvF9zMJgkjUp/O +gNSG9U7kXuFfxpVtoalyC2C3g3UeieVXSek3a28h5c/0/DomHqLbyqZh5rYwAJ7C +q1bqA5TnZNVvV731SVueycj9+5PKHKG6eeRRh7roZ34l54O9adNEeDAF0Lqn4sbn +a/h4GPK/u6J6Y3nwrdajipZ2DmfiQwoimxprMGNQKuKA0lc025SGHNno +-----END CERTIFICATE----- diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key1.pem b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key1.pem new file mode 100644 index 000000000000..197eabb17264 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key1.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQChhTZp+6k+r9vo +8jLoxYM4xF0mTm5bnWWvDqi51elaVRAxcp2xplPiypuCMYuCB+jtZaw94LNA3eQh +fdM6oGiDkQyReeVCEB8/Ye2PS5c8WP3uV5n4WomvOA+dljzBKoNd/L3/JxHc+BaN +GxIZklvTmFM1UZNTa+k1AMOj5cEqDJsHRp8ZTw2T8idZ+zXF38m/RhxUANCJASaD +jXCDXwHcswgFk9YglRDVvvAyWHFZoI1cd8HVtRMhgJQBQ5nIRrE9o46ImjfbEqao +6VRMIszzOBYIEISAe1DBIPQYwuFRqss80TkY/fKjmocFio6q57sEGM/h7nyvrADI +Gh7uQxhFAgMBAAECggEABUfEGCHkjgqUv2BPnsF6QTxWKT7uJ6uVG+x4Qp8GInBe +d6deFWUxH9xsygxRmb4ldMFaqKk0Yv3+C8Q/yA5fbFGtHgJkpsy9IMbUS9d2ScBF +COovO+nFz4cfJ5E2SkBYDBYLphBCar1ni1RjupdIzjmQGtGgZd1EwflU7AJCVtwG +S7ltIs2nSOqUFGTfjb9j0NiATZvWTDRtavNMhyrZplKK6M6VoH1ZcnmcvEfF7j5L +oSmXrNKYs4iKn1qKypykfCQoEFK0/EEjj5EdnPaSeI9EERrZK1QnHafB2qK38LSr +8cGaWH24mPW6c/26bDQnHkN3SqKLCODXZMBGhPlLDwKBgQDdMqOzRR3SpTx7KPqp +h+P0diBZb1e6c+Ob0lXD/rfJEtkAqyFLqpi8hN9xodxw++JYbhC69kJE7VWtQLIt +Lc+DG72KTS/cbpnvERL1+AoM0TRbO9Ds9aFP4+Zmm/VDxi9rR5yTgl9iAHJ46VrE +BhnG8JQPBm4n5JU5/wJ9qCQCywKBgQC67uWchaewzDHCiefhTVgwTm1BmHiV/OR4 +50Je2x3GPW6VJGFnBjVzlScKrNyFeOYwscvVS8pTmFP8c5laTbQMC3pVqiWs28Ip +6sy6cXfepVyc0njLFGbiek8ab0rjVYU27D0O9tucrxDx4pKOurilds1Gbm4HjfyE +R7pWn/AfLwKBgQC+5wJzKLaJYsQlAwP6pmYtSHm41ihfqb8Jb2lHwyD4r4SLWCZf +OHejVAXH+0rWU/1QFoXn5brh4/cqlIhyB3RtkdZucxlYZDgEJLc5g32g/Dj0eFZi ++8bhvS3O5tCxUm0AaIiQolcRrJMfGT6VqTI8CMuvf/w3/8ZujFCpBCE4KwKBgBiw +lQMnZA6l6ayYKlhHru4ybZvMV6D31fViFhIRPs2AL6rjMzo4R7cMbCusyTOX1E96 +LEHv0LlZ1T3yxr52pOEyYuYNowxBulNu/7tgYUS28pSD+BBakXw4S1pieLGuCfpH +GYlwcXEwbjyEgHb5konINzSmQUIeLswJ7UKjvUNhAoGAXmXvyHqdL04SD99G3B/5 ++azzzAVR1fvGYOvq+/hWZMG5PS0kx2V3txCVyY8E1/lCysp9BuUHtW+vOS8YGhAT +wkZ/X9igZteQvvdVw+E5CXS05b4EBI+7ZViL9ulXFZ4YC70lKcUE52bmaPM+onQJ +Y1s9JWTe2EAkxsuxm+hkjo0= +-----END PRIVATE KEY----- diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key2-chain.crt b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key2-chain.crt new file mode 100644 index 000000000000..3b55b95a96ae --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key2-chain.crt @@ -0,0 +1,38 @@ +-----BEGIN CERTIFICATE----- +MIIDJjCCAhECFFjLlXVdTxDdLlCifzrA0dTHHJ2mMA0GCSqGSIb3DQEBCwUAME8x +CzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0Rl +ZmF1bHQgQ29tcGFueSBMdGQxCzAJBgNVBAMMAkNBMCAXDTIzMTAwNTA3Mjg1MFoY +DzIxMjMwOTExMDcyODUwWjBRMQswCQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVs +dCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMQ0wCwYDVQQDDARr +ZXkyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAspCMUdFGyKkgpMbW ++UwSg4fdKM4qLSH7voTdsdVM9aAvLvYjBQ4gpORxDZNfUz67R0Ua0/oJt9jD49Wp +qcq+tDOnp0dPtn2hFluV5PxM6d+MCSx/frPsfvyt9234okLL1zdLDNFYEbLhSPjA +ku3vHw/OwlJOxCRwTkPqcElIV4+IvIbzAgSffyokzm/wKVKEhoT6NcfeU+6wCkTu +al1X8loJ+27N6jN13oGZfH7EveBqgR8rPs55+54S/OcVG/uqL9ggOGRJiIZ3jUBk +m5cN27wKkaNg/CQwa1UjcU4qshVpknHw1dpgJ2Gbs/yUphwpEZl/FTsZFcK1KCHD +rOp3PQIDAQABMA0GCSqGSIb3DQEBCwUAA4H/AAFmEq86broBFxs0cpImaM884PBT +bvJBSsFhsOg6mi4Gt01G/lPSj/ExNtH3G5bytCYAPaRxNx/dCs7uON3p86ta4zL8 +2PxgyhX1oY/GG63ETwn5s3GKpRaGTNVDWvPIM9RX6+bvX/wOg8eYXVaQlG5XYadC +Ms9lWqHaM1C/iLGNmUTGcdbvhnmQDky2CwPNm+lXogSWbrsGpAmCkXJD1H+0Mx8I +wjDVtGLBwr/8oXI8WbhvISMnS9+dd7+GLm6mU+14Kswi5I7EmBmREvkswi2IVJ6M +GL7EY3qA6iqJWqsseYyLxiMr3nBT0SETphzoDanUQI1/jXQPrWIyjqvs +-----END CERTIFICATE----- +-----BEGIN TRUSTED CERTIFICATE----- +MIIDIDCCAgsCFH3lh1RXOEy2ESqUPyzb+9zxMYUnMA0GCSqGSIb3DQEBCwUAME8x +CzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0Rl +ZmF1bHQgQ29tcGFueSBMdGQxCzAJBgNVBAMMAkNBMCAXDTIzMTAwNTA3MjU1M1oY +DzIxMjMwOTExMDcyNTUzWjBPMQswCQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVs +dCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMQswCQYDVQQDDAJD +QTCCAR4wDQYJKoZIhvcNAQEBBQADggELADCCAQYCgf4NNpc+6B3qvwKcRYgoXmJ4 +3wyWktBK7BdShz/YnW1OlFZ+R845ZiDw0KdzElZWkYqn+BYJus6lPIS5dfLcrGSf +a1e8IK02RpBiY/WJvupetnSk8gKA7emF94NlV4gXr4ICJAhXvXUFyBLpdEUE/lcg +lgCbVJzs5jWUnffEF9mrClzzo0+iXw34zwmyYyBTFmlOEr+QUEdAb6Lr/klpTVit +as2Ddg1QT4EaSIdTEpkVRZp2dyYVdqSxpaBq21xg0viDHsYQrP96IfacmUB7kFFn +HsnptDHFvJj2WSQDX+PRS7tLl4mmfizZg80eGfLD22ShNspRSGnbJc0OzegPiwID +AQABMA0GCSqGSIb3DQEBCwUAA4H/AAnC+FQqdeJaG5I7R+pNjgKplL2UsxW983kA +CVVkv/Dt0+4rbPC67o9/8Tr+g4eo/wUntMNo2ghF3oBItGr7pJE16zPiLwIvha9c +8BDhCEZWyhz3vkamZUi19lOnkm3zTmmDE/nX4WYH6CL4UWjxvniZYwW8AdVSnFXY +ncriuvfliLa3dw1SJ7FtxdcBn4yfzrZWcY+psYNHpftLGYRmQF/VCDSB9EAIEggr +yBcP749u2y8s44WvKAnnwfLcALIrylY25zN0pao/l2X8HI6qHUeA/QbbEBpDoQvR +du/rgaHCVvFFxATefhBJ0CUA1Nn5nrGwyRTKnZWtR080qwUp +-----END TRUSTED CERTIFICATE----- diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key2.crt b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key2.crt new file mode 100644 index 000000000000..127882627896 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key2.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDJjCCAhECFFjLlXVdTxDdLlCifzrA0dTHHJ2mMA0GCSqGSIb3DQEBCwUAME8x +CzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0Rl +ZmF1bHQgQ29tcGFueSBMdGQxCzAJBgNVBAMMAkNBMCAXDTIzMTAwNTA3Mjg1MFoY +DzIxMjMwOTExMDcyODUwWjBRMQswCQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVs +dCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMQ0wCwYDVQQDDARr +ZXkyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAspCMUdFGyKkgpMbW ++UwSg4fdKM4qLSH7voTdsdVM9aAvLvYjBQ4gpORxDZNfUz67R0Ua0/oJt9jD49Wp +qcq+tDOnp0dPtn2hFluV5PxM6d+MCSx/frPsfvyt9234okLL1zdLDNFYEbLhSPjA +ku3vHw/OwlJOxCRwTkPqcElIV4+IvIbzAgSffyokzm/wKVKEhoT6NcfeU+6wCkTu +al1X8loJ+27N6jN13oGZfH7EveBqgR8rPs55+54S/OcVG/uqL9ggOGRJiIZ3jUBk +m5cN27wKkaNg/CQwa1UjcU4qshVpknHw1dpgJ2Gbs/yUphwpEZl/FTsZFcK1KCHD +rOp3PQIDAQABMA0GCSqGSIb3DQEBCwUAA4H/AAFmEq86broBFxs0cpImaM884PBT +bvJBSsFhsOg6mi4Gt01G/lPSj/ExNtH3G5bytCYAPaRxNx/dCs7uON3p86ta4zL8 +2PxgyhX1oY/GG63ETwn5s3GKpRaGTNVDWvPIM9RX6+bvX/wOg8eYXVaQlG5XYadC +Ms9lWqHaM1C/iLGNmUTGcdbvhnmQDky2CwPNm+lXogSWbrsGpAmCkXJD1H+0Mx8I +wjDVtGLBwr/8oXI8WbhvISMnS9+dd7+GLm6mU+14Kswi5I7EmBmREvkswi2IVJ6M +GL7EY3qA6iqJWqsseYyLxiMr3nBT0SETphzoDanUQI1/jXQPrWIyjqvs +-----END CERTIFICATE----- diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key2.pem b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key2.pem new file mode 100644 index 000000000000..9e21a1c3f421 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key2.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCykIxR0UbIqSCk +xtb5TBKDh90oziotIfu+hN2x1Uz1oC8u9iMFDiCk5HENk19TPrtHRRrT+gm32MPj +1ampyr60M6enR0+2faEWW5Xk/Ezp34wJLH9+s+x+/K33bfiiQsvXN0sM0VgRsuFI ++MCS7e8fD87CUk7EJHBOQ+pwSUhXj4i8hvMCBJ9/KiTOb/ApUoSGhPo1x95T7rAK +RO5qXVfyWgn7bs3qM3XegZl8fsS94GqBHys+znn7nhL85xUb+6ov2CA4ZEmIhneN +QGSblw3bvAqRo2D8JDBrVSNxTiqyFWmScfDV2mAnYZuz/JSmHCkRmX8VOxkVwrUo +IcOs6nc9AgMBAAECggEAPN9dDolG1aIeYD3uzCa8Sv2WjdIWe7NRlEXMI9MgvL1i +SGKdVpxV0ZCU37llLkY85tNujWP4SyXIxdMxVxIoR9syJKsBSCd0sl//bgP6nmHY +Zco3HnTswu+VyLtDHuGhhtkxKwn0uXffKBaw44XcVhz38bPIaUI4zN2HPscks8BG +j2MEl0N8P/TVrTkhgdjfoRi73VAisrEe+1wCg74BT7cmR8fEr7iNFrv955sdPGdw +UTmx8U26++wbeYQs1ZE1713SYnRQuCUFs5GGjzOhNFi27zuhI6TafoVm9PO4j+ZC +JUKTyUTBUsRMvm9z1IoHdjM8yInAv2g0J1bAeCTY+wKBgQDuMNMbNVoiXRKsSUry +22T3W6HVLfLNKiYMNxsAkJjOiyyJcC+yg9BErn/haIHSafD2WmuWbW5ASViyl6fn +D8qMluTwEaSrTgHXWI4ahWyapDShDQYp1s4dB75Aa/LVcFCay54YEtyCPzCPlj1K +jz5OBV14NEVVA2cf59fIc/LXCwKBgQC/6m3TefUp5jnN/QUOx2OtZo8Y1pVrsuMB +AuTtb21Khxn/86ZpVzySzg79/DkSNf9/sZhzj0IkviWNP5S8iAAaFC1q08CYhdCX +d7tVnHlzpZmmoHUhG6dlJZayr1duZrURp2rP18+wIsKiFRImAyjc6yswVRpZgAiG +gOkHCB231wKBgGlwXZMWy/6YOtLfYvkcm5ZQDtSCkY+2j78qiZ53Y91SiHWSntqk +NQaiRGOw0n8lfJBhOG0PphV5InV0YtQLDnurtE59UOqwDmqYfddJpujRtaZxUIAm +4XjCW7rCzm0jWdscNbCscMaLWGDHffxKaqc5AsZaRTK73eOmysOmaCI/AoGAf/yd +RZ1dzJWHE0Kb7uE2LlvpLo1clLh1/ySo+1eGMV+sDS+2WSYedWEKSoO8o9JzE/ui +Sd7OI6bTcEFotdqVBs9SAp45IP6Mv5bPziZOMLvNnnv/4RaKKkBJId0hl7TTKHTY +HMg176ce2eznb4ZH6BzFbrQyoGFsThcGUPQurX0CgYBYtkDTp21TI1nuak7xpMIY +BJQpqF5ahBf/+QYWtL0f3ca9MO2++zv5/XXitvt48cY1bCHNrVvSHgRzwSrOorZA +5u7a5zyvfXjY3LY3k0VHddaVjU0mHsjx/1ux0wO2v8wQjOVZpT7XweB3WlUEGV7C +5T/p+rmGg5Y5dTKUVCyvbQ== +-----END PRIVATE KEY----- From fbec06a134122d3d0615e5f8baa02455061b47cb Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 5 Oct 2023 10:44:19 +0200 Subject: [PATCH 0550/1215] Support new CSP auth method for Wavefront Closes gh-37165 --- .../WavefrontPropertiesConfigAdapter.java | 16 ++++++++ .../wavefront/WavefrontProperties.java | 39 +++++++++++++++++++ ...WavefrontPropertiesConfigAdapterTests.java | 20 ++++++++++ 3 files changed, 75 insertions(+) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapter.java index 9ffa626b2bb9..1f64342b161b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapter.java @@ -16,11 +16,13 @@ package org.springframework.boot.actuate.autoconfigure.metrics.export.wavefront; +import com.wavefront.sdk.common.clients.service.token.TokenService.Type; import io.micrometer.wavefront.WavefrontConfig; import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.PushRegistryPropertiesConfigAdapter; import org.springframework.boot.actuate.autoconfigure.wavefront.WavefrontProperties; import org.springframework.boot.actuate.autoconfigure.wavefront.WavefrontProperties.Metrics.Export; +import org.springframework.boot.actuate.autoconfigure.wavefront.WavefrontProperties.TokenType; /** * Adapter to convert {@link WavefrontProperties} to a {@link WavefrontConfig}. @@ -84,4 +86,18 @@ public boolean reportDayDistribution() { return get(Export::isReportDayDistribution, WavefrontConfig.super::reportDayDistribution); } + @Override + public Type apiTokenType() { + TokenType apiTokenType = this.properties.getApiTokenType(); + if (apiTokenType == null) { + return WavefrontConfig.super.apiTokenType(); + } + return switch (apiTokenType) { + case NO_TOKEN -> Type.NO_TOKEN; + case WAVEFRONT_API_TOKEN -> Type.WAVEFRONT_API_TOKEN; + case CSP_API_TOKEN -> Type.CSP_API_TOKEN; + case CSP_CLIENT_CREDENTIALS -> Type.CSP_CLIENT_CREDENTIALS; + }; + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontProperties.java index 19c66c0071c5..77fbd189a69a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontProperties.java @@ -57,6 +57,11 @@ public class WavefrontProperties { */ private String apiToken; + /** + * Type of the API token. + */ + private TokenType apiTokenType; + /** * Application configuration. */ @@ -167,6 +172,14 @@ public void setTraceDerivedCustomTagKeys(Set traceDerivedCustomTagKeys) this.traceDerivedCustomTagKeys = traceDerivedCustomTagKeys; } + public TokenType getApiTokenType() { + return this.apiTokenType; + } + + public void setApiTokenType(TokenType apiTokenType) { + this.apiTokenType = apiTokenType; + } + public static class Application { /** @@ -385,4 +398,30 @@ public void setReportDayDistribution(boolean reportDayDistribution) { } + /** + * Wavefront token type. + * + * @since 3.2.0 + */ + public enum TokenType { + + /** + * No token. + */ + NO_TOKEN, + /** + * Wavefront API token. + */ + WAVEFRONT_API_TOKEN, + /** + * CSP API token. + */ + CSP_API_TOKEN, + /** + * CSP client credentials. + */ + CSP_CLIENT_CREDENTIALS + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapterTests.java index 54234f878714..6fd14b676d51 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapterTests.java @@ -18,11 +18,15 @@ import java.net.URI; +import com.wavefront.sdk.common.clients.service.token.TokenService.Type; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.PushRegistryPropertiesConfigAdapterTests; import org.springframework.boot.actuate.autoconfigure.wavefront.WavefrontProperties; import org.springframework.boot.actuate.autoconfigure.wavefront.WavefrontProperties.Metrics.Export; +import org.springframework.boot.actuate.autoconfigure.wavefront.WavefrontProperties.TokenType; import static org.assertj.core.api.Assertions.assertThat; @@ -107,4 +111,20 @@ void whenPropertiesReportDayDistributionIsSetAdapterReportDayDistributionReturns assertThat(createConfigAdapter(properties).reportDayDistribution()).isTrue(); } + @ParameterizedTest + @CsvSource(textBlock = """ + null, WAVEFRONT_API_TOKEN + NO_TOKEN, NO_TOKEN + WAVEFRONT_API_TOKEN, WAVEFRONT_API_TOKEN + CSP_API_TOKEN, CSP_API_TOKEN + CSP_CLIENT_CREDENTIALS, CSP_CLIENT_CREDENTIALS + """) + void whenTokenTypeIsSetAdapterReturnsIt(String property, String wavefront) { + TokenType propertyToken = property.equals("null") ? null : TokenType.valueOf(property); + Type wavefrontToken = Type.valueOf(wavefront); + WavefrontProperties properties = new WavefrontProperties(); + properties.setApiTokenType(propertyToken); + assertThat(new WavefrontPropertiesConfigAdapter(properties).apiTokenType()).isEqualTo(wavefrontToken); + } + } From d0cadd9c4a075eb3edb178aa588d5460a8b272ec Mon Sep 17 00:00:00 2001 From: Simon Verhoeven Date: Thu, 5 Oct 2023 04:37:55 +0200 Subject: [PATCH 0551/1215] Add reference to the new RestClient class in documentation See gh-37726 --- .../src/docs/asciidoc/native-image/advanced-topics.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/native-image/advanced-topics.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/native-image/advanced-topics.adoc index 884dcb29e883..efaf9c9c8e24 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/native-image/advanced-topics.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/native-image/advanced-topics.adoc @@ -159,7 +159,7 @@ You can then use `@ImportRuntimeHints` on any `@Configuration` class (for exampl If you have classes which need binding (mostly needed when serializing or deserializing JSON), you can use {spring-framework-docs}/core.html#aot-hints-register-reflection-for-binding[`@RegisterReflectionForBinding`] on any bean. Most of the hints are automatically inferred, for example when accepting or returning data from a `@RestController` method. -But when you work with `WebClient` or `RestTemplate` directly, you might need to use `@RegisterReflectionForBinding`. +But when you work with `WebClient`, `RestClient` or `RestTemplate` directly, you might need to use `@RegisterReflectionForBinding`. [[native-image.advanced.custom-hints.testing]] ==== Testing custom hints From 346db8e79514fc0e3761fdbc9fc79155165f6baf Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Fri, 29 Sep 2023 15:59:08 -0700 Subject: [PATCH 0552/1215] Add auto-configuration for SpanAspect See gh-37640 --- .../MicrometerTracingAutoConfiguration.java | 44 ++++++++- ...crometerTracingAutoConfigurationTests.java | 97 +++++++++++++++++-- 2 files changed, 130 insertions(+), 11 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java index 149141c887f9..40721dcbb90a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java @@ -17,17 +17,26 @@ package org.springframework.boot.actuate.autoconfigure.tracing; import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.annotation.DefaultNewSpanParser; +import io.micrometer.tracing.annotation.ImperativeMethodInvocationProcessor; +import io.micrometer.tracing.annotation.MethodInvocationProcessor; +import io.micrometer.tracing.annotation.NewSpanParser; +import io.micrometer.tracing.annotation.SpanAspect; +import io.micrometer.tracing.annotation.SpanTagAnnotationHandler; import io.micrometer.tracing.handler.DefaultTracingObservationHandler; import io.micrometer.tracing.handler.PropagatingReceiverTracingObservationHandler; import io.micrometer.tracing.handler.PropagatingSenderTracingObservationHandler; import io.micrometer.tracing.propagation.Propagator; +import org.aspectj.weaver.Advice; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -35,10 +44,12 @@ * {@link EnableAutoConfiguration Auto-configuration} for the Micrometer Tracing API. * * @author Moritz Halbritter + * @author Jonatan Ivanov * @since 3.0.0 */ @AutoConfiguration @ConditionalOnClass(Tracer.class) +@ConditionalOnBean(Tracer.class) public class MicrometerTracingAutoConfiguration { /** @@ -60,7 +71,6 @@ public class MicrometerTracingAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnBean(Tracer.class) @Order(DEFAULT_TRACING_OBSERVATION_HANDLER_ORDER) public DefaultTracingObservationHandler defaultTracingObservationHandler(Tracer tracer) { return new DefaultTracingObservationHandler(tracer); @@ -68,7 +78,7 @@ public DefaultTracingObservationHandler defaultTracingObservationHandler(Tracer @Bean @ConditionalOnMissingBean - @ConditionalOnBean({ Tracer.class, Propagator.class }) + @ConditionalOnBean(Propagator.class) @Order(SENDER_TRACING_OBSERVATION_HANDLER_ORDER) public PropagatingSenderTracingObservationHandler propagatingSenderTracingObservationHandler(Tracer tracer, Propagator propagator) { @@ -77,11 +87,39 @@ public PropagatingSenderTracingObservationHandler propagatingSenderTracingObs @Bean @ConditionalOnMissingBean - @ConditionalOnBean({ Tracer.class, Propagator.class }) + @ConditionalOnBean(Propagator.class) @Order(RECEIVER_TRACING_OBSERVATION_HANDLER_ORDER) public PropagatingReceiverTracingObservationHandler propagatingReceiverTracingObservationHandler(Tracer tracer, Propagator propagator) { return new PropagatingReceiverTracingObservationHandler<>(tracer, propagator); } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(Advice.class) + static class SpanAspectConfiguration { + + @Bean + @ConditionalOnMissingBean + DefaultNewSpanParser newSpanParser() { + return new DefaultNewSpanParser(); + } + + @Bean + @ConditionalOnMissingBean + ImperativeMethodInvocationProcessor imperativeMethodInvocationProcessor(NewSpanParser newSpanParser, + Tracer tracer, ObjectProvider spanTagAnnotationHandler) { + ImperativeMethodInvocationProcessor methodInvocationProcessor = new ImperativeMethodInvocationProcessor( + newSpanParser, tracer); + spanTagAnnotationHandler.ifAvailable(methodInvocationProcessor::setSpanTagAnnotationHandler); + return methodInvocationProcessor; + } + + @Bean + @ConditionalOnMissingBean + SpanAspect spanAspect(MethodInvocationProcessor methodInvocationProcessor) { + return new SpanAspect(methodInvocationProcessor); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java index d9f94f46396a..3ca8ff2364af 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java @@ -19,18 +19,27 @@ import java.util.List; import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.annotation.DefaultNewSpanParser; +import io.micrometer.tracing.annotation.ImperativeMethodInvocationProcessor; +import io.micrometer.tracing.annotation.MethodInvocationProcessor; +import io.micrometer.tracing.annotation.NewSpanParser; +import io.micrometer.tracing.annotation.SpanAspect; +import io.micrometer.tracing.annotation.SpanTagAnnotationHandler; import io.micrometer.tracing.handler.DefaultTracingObservationHandler; import io.micrometer.tracing.handler.PropagatingReceiverTracingObservationHandler; import io.micrometer.tracing.handler.PropagatingSenderTracingObservationHandler; import io.micrometer.tracing.handler.TracingObservationHandler; import io.micrometer.tracing.propagation.Propagator; +import org.aspectj.weaver.Advice; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -39,6 +48,7 @@ * Tests for {@link MicrometerTracingAutoConfiguration}. * * @author Moritz Halbritter + * @author Jonatan Ivanov */ class MicrometerTracingAutoConfigurationTests { @@ -52,6 +62,9 @@ void shouldSupplyBeans() { assertThat(context).hasSingleBean(DefaultTracingObservationHandler.class); assertThat(context).hasSingleBean(PropagatingReceiverTracingObservationHandler.class); assertThat(context).hasSingleBean(PropagatingSenderTracingObservationHandler.class); + assertThat(context).hasSingleBean(DefaultNewSpanParser.class); + assertThat(context).hasSingleBean(ImperativeMethodInvocationProcessor.class); + assertThat(context).hasSingleBean(SpanAspect.class); }); } @@ -75,14 +88,21 @@ void shouldSupplyBeansInCorrectOrder() { @Test void shouldBackOffOnCustomBeans() { - this.contextRunner.withUserConfiguration(CustomConfiguration.class).run((context) -> { - assertThat(context).hasBean("customDefaultTracingObservationHandler"); - assertThat(context).hasSingleBean(DefaultTracingObservationHandler.class); - assertThat(context).hasBean("customPropagatingReceiverTracingObservationHandler"); - assertThat(context).hasSingleBean(PropagatingReceiverTracingObservationHandler.class); - assertThat(context).hasBean("customPropagatingSenderTracingObservationHandler"); - assertThat(context).hasSingleBean(PropagatingSenderTracingObservationHandler.class); - }); + this.contextRunner.withUserConfiguration(TracerConfiguration.class, CustomConfiguration.class) + .run((context) -> { + assertThat(context).hasBean("customDefaultTracingObservationHandler"); + assertThat(context).hasSingleBean(DefaultTracingObservationHandler.class); + assertThat(context).hasBean("customPropagatingReceiverTracingObservationHandler"); + assertThat(context).hasSingleBean(PropagatingReceiverTracingObservationHandler.class); + assertThat(context).hasBean("customPropagatingSenderTracingObservationHandler"); + assertThat(context).hasSingleBean(PropagatingSenderTracingObservationHandler.class); + assertThat(context).hasBean("customDefaultNewSpanParser"); + assertThat(context).hasSingleBean(DefaultNewSpanParser.class); + assertThat(context).hasBean("customImperativeMethodInvocationProcessor"); + assertThat(context).hasSingleBean(ImperativeMethodInvocationProcessor.class); + assertThat(context).hasBean("customSpanAspect"); + assertThat(context).hasSingleBean(SpanAspect.class); + }); } @Test @@ -91,6 +111,9 @@ void shouldNotSupplyBeansIfMicrometerIsMissing() { assertThat(context).doesNotHaveBean(DefaultTracingObservationHandler.class); assertThat(context).doesNotHaveBean(PropagatingReceiverTracingObservationHandler.class); assertThat(context).doesNotHaveBean(PropagatingSenderTracingObservationHandler.class); + assertThat(context).doesNotHaveBean(DefaultNewSpanParser.class); + assertThat(context).doesNotHaveBean(ImperativeMethodInvocationProcessor.class); + assertThat(context).doesNotHaveBean(SpanAspect.class); }); } @@ -100,17 +123,47 @@ void shouldNotSupplyBeansIfTracerIsMissing() { assertThat(context).doesNotHaveBean(DefaultTracingObservationHandler.class); assertThat(context).doesNotHaveBean(PropagatingReceiverTracingObservationHandler.class); assertThat(context).doesNotHaveBean(PropagatingSenderTracingObservationHandler.class); + assertThat(context).doesNotHaveBean(DefaultNewSpanParser.class); + assertThat(context).doesNotHaveBean(ImperativeMethodInvocationProcessor.class); + assertThat(context).doesNotHaveBean(SpanAspect.class); }); } + @Test + void shouldNotSupplyBeansIfAspectjIsMissing() { + this.contextRunner.withUserConfiguration(TracerConfiguration.class) + .withClassLoader(new FilteredClassLoader(Advice.class)) + .run((context) -> { + assertThat(context).doesNotHaveBean(DefaultNewSpanParser.class); + assertThat(context).doesNotHaveBean(ImperativeMethodInvocationProcessor.class); + assertThat(context).doesNotHaveBean(SpanAspect.class); + }); + } + @Test void shouldNotSupplyBeansIfPropagatorIsMissing() { this.contextRunner.withUserConfiguration(TracerConfiguration.class).run((context) -> { assertThat(context).doesNotHaveBean(PropagatingSenderTracingObservationHandler.class); assertThat(context).doesNotHaveBean(PropagatingReceiverTracingObservationHandler.class); + + assertThat(context).hasSingleBean(DefaultNewSpanParser.class); + assertThat(context).hasSingleBean(ImperativeMethodInvocationProcessor.class); + assertThat(context).hasSingleBean(SpanAspect.class); }); } + @Test + void shouldConfigureSpanTagAnnotationHandler() { + this.contextRunner.withUserConfiguration(TracerConfiguration.class, SpanTagAnnotationHandlerConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(DefaultNewSpanParser.class); + assertThat(context).hasSingleBean(SpanAspect.class); + assertThat(ReflectionTestUtils.getField(context.getBean(ImperativeMethodInvocationProcessor.class), + "spanTagAnnotationHandler")) + .isSameAs(context.getBean(SpanTagAnnotationHandler.class)); + }); + } + @Configuration(proxyBeanMethods = false) private static class TracerConfiguration { @@ -149,6 +202,34 @@ PropagatingSenderTracingObservationHandler customPropagatingSenderTracingObse return mock(PropagatingSenderTracingObservationHandler.class); } + @Bean + DefaultNewSpanParser customDefaultNewSpanParser() { + return new DefaultNewSpanParser(); + } + + @Bean + @ConditionalOnMissingBean + ImperativeMethodInvocationProcessor customImperativeMethodInvocationProcessor(NewSpanParser newSpanParser, + Tracer tracer) { + return new ImperativeMethodInvocationProcessor(newSpanParser, tracer); + } + + @Bean + @ConditionalOnMissingBean + SpanAspect customSpanAspect(MethodInvocationProcessor methodInvocationProcessor) { + return new SpanAspect(methodInvocationProcessor); + } + + } + + @Configuration(proxyBeanMethods = false) + private static class SpanTagAnnotationHandlerConfiguration { + + @Bean + SpanTagAnnotationHandler spanTagAnnotationHandler() { + return new SpanTagAnnotationHandler((aClass) -> null, (aClass) -> null); + } + } } From 6c24ea01f1cf33dd94030a0437227c5331d3dd43 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 5 Oct 2023 21:35:13 -0700 Subject: [PATCH 0553/1215] Add BouncyCastle nested jar verification test including on Oracle JDK Update `spring-boot-loader-tests` with a test that checks verified BouncyCastle jars can be loaded. Currently the Oracle JDK only supports verification if the jar is unpacked. See gh-28837 --- .../spring-boot-loader-tests/build.gradle | 16 ++++++- .../build.gradle | 30 +++++++++++++ .../settings.gradle | 15 +++++++ .../LoaderSignedJarTestApplication.java | 36 ++++++++++++++++ .../boot/loader/LoaderIntegrationTests.java | 43 ++++++++++++++++--- 5 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-signed-jar/build.gradle create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-signed-jar/settings.gradle create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-signed-jar/src/main/java/org/springframework/boot/loaderapp/LoaderSignedJarTestApplication.java diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/build.gradle index e287ed0a16a6..aa88c046b4ae 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/build.gradle +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/build.gradle @@ -18,6 +18,8 @@ dependencies { app project(path: ":spring-boot-project:spring-boot-dependencies", configuration: "mavenRepository") app project(path: ":spring-boot-project:spring-boot-tools:spring-boot-gradle-plugin", configuration: "mavenRepository") app project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter-web", configuration: "mavenRepository") + app project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter", configuration: "mavenRepository") + app("org.bouncycastle:bcprov-jdk18on:1.76") intTestImplementation(enforcedPlatform(project(":spring-boot-project:spring-boot-parent"))) intTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) @@ -43,6 +45,18 @@ task buildApp(type: GradleBuild) { tasks = ["build"] } +task syncSignedJarAppSource(type: org.springframework.boot.build.SyncAppSource) { + sourceDirectory = file("spring-boot-loader-tests-signed-jar") + destinationDirectory = file("${buildDir}/spring-boot-loader-tests-signed-jar") +} + +task buildSignedJarApp(type: GradleBuild) { + dependsOn syncSignedJarAppSource, syncMavenRepository + dir = "${buildDir}/spring-boot-loader-tests-signed-jar" + startParameter.buildCacheEnabled = false + tasks = ["build"] +} + task downloadJdk(type: Download) { def destFolder = new File(rootProject.buildDir, "downloads/jdk/oracle") destFolder.mkdirs() @@ -64,5 +78,5 @@ processIntTestResources { } intTest { - dependsOn buildApp + dependsOn buildApp, buildSignedJarApp } diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-signed-jar/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-signed-jar/build.gradle new file mode 100644 index 000000000000..7ca8a2712496 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-signed-jar/build.gradle @@ -0,0 +1,30 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + +plugins { + id "java" + id "org.springframework.boot" +} + +apply plugin: "io.spring.dependency-management" + +repositories { + maven { url "file:${rootDir}/../int-test-maven-repository"} + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter") + implementation("org.bouncycastle:bcprov-jdk18on:1.76") +} + +tasks.register("bootJarUnpack", BootJar.class) { + mainClass = "org.springframework.boot.loaderapp.LoaderSignedJarTestApplication" + classpath = bootJar.classpath + requiresUnpack '**/bcprov-jdk18on-*.jar' + archiveClassifier.set("unpack") + targetJavaVersion = targetCompatibility +} + +build.dependsOn bootJarUnpack \ No newline at end of file diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-signed-jar/settings.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-signed-jar/settings.gradle new file mode 100644 index 000000000000..06d9554ad0d6 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-signed-jar/settings.gradle @@ -0,0 +1,15 @@ +pluginManagement { + repositories { + maven { url "file:${rootDir}/../int-test-maven-repository"} + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } + } + resolutionStrategy { + eachPlugin { + if (requested.id.id == "org.springframework.boot") { + useModule "org.springframework.boot:spring-boot-gradle-plugin:${requested.version}" + } + } + } +} \ No newline at end of file diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-signed-jar/src/main/java/org/springframework/boot/loaderapp/LoaderSignedJarTestApplication.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-signed-jar/src/main/java/org/springframework/boot/loaderapp/LoaderSignedJarTestApplication.java new file mode 100644 index 000000000000..627a6c3996d3 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-signed-jar/src/main/java/org/springframework/boot/loaderapp/LoaderSignedJarTestApplication.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2023 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.loaderapp; + +import java.security.Security; +import javax.crypto.Cipher; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class LoaderSignedJarTestApplication { + + public static void main(String[] args) throws Exception { + Security.addProvider(new BouncyCastleProvider()); + Cipher.getInstance("AES/CBC/PKCS5Padding","BC"); + System.out.println("Legion of the Bouncy Castle"); + SpringApplication.run(LoaderSignedJarTestApplication.class, args); + } + +} diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java index 3e5c2f023e54..ac6592bc9d6d 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java @@ -37,6 +37,7 @@ import org.springframework.util.Assert; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assumptions.assumeThat; /** * Integration tests loader that supports fat jars. @@ -52,7 +53,7 @@ class LoaderIntegrationTests { @ParameterizedTest @MethodSource("javaRuntimes") void readUrlsWithoutWarning(JavaRuntime javaRuntime) { - try (GenericContainer container = createContainer(javaRuntime)) { + try (GenericContainer container = createContainer(javaRuntime, "spring-boot-loader-tests-app", null)) { container.start(); System.out.println(this.output.toUtf8String()); assertThat(this.output.toUtf8String()).contains(">>>>> 287649 BYTES from") @@ -62,18 +63,46 @@ void readUrlsWithoutWarning(JavaRuntime javaRuntime) { } } - private GenericContainer createContainer(JavaRuntime javaRuntime) { + @ParameterizedTest + @MethodSource("javaRuntimes") + void runSignedJar(JavaRuntime javaRuntime) { + assumeThat(javaRuntime.toString()).isNotEqualTo("Oracle JDK 17"); // gh-28837 + try (GenericContainer container = createContainer(javaRuntime, "spring-boot-loader-tests-signed-jar", + null)) { + container.start(); + System.out.println(this.output.toUtf8String()); + assertThat(this.output.toUtf8String()).contains("Legion of the Bouncy Castle"); + } + } + + @ParameterizedTest + @MethodSource("javaRuntimes") + void runSignedJarWhenUnpack(JavaRuntime javaRuntime) { + try (GenericContainer container = createContainer(javaRuntime, "spring-boot-loader-tests-signed-jar", + "unpack")) { + container.start(); + System.out.println(this.output.toUtf8String()); + assertThat(this.output.toUtf8String()).contains("Legion of the Bouncy Castle"); + } + } + + private GenericContainer createContainer(JavaRuntime javaRuntime, String name, String classifier) { return javaRuntime.getContainer() .withLogConsumer(this.output) - .withCopyFileToContainer(MountableFile.forHostPath(findApplication().toPath()), "/app.jar") + .withCopyFileToContainer(findApplication(name, classifier), "/app.jar") .withStartupCheckStrategy(new OneShotStartupCheckStrategy().withTimeout(Duration.ofMinutes(5))) .withCommand("java", "-jar", "app.jar"); } - private File findApplication() { - String name = String.format("build/%1$s/build/libs/%1$s.jar", "spring-boot-loader-tests-app"); - File jar = new File(name); - Assert.state(jar.isFile(), () -> "Could not find " + name + ". Have you built it?"); + private MountableFile findApplication(String name, String classifier) { + return MountableFile.forHostPath(findJarFile(name, classifier).toPath()); + } + + private File findJarFile(String name, String classifier) { + classifier = (classifier != null) ? "-" + classifier : ""; + String path = String.format("build/%1$s/build/libs/%1$s%2$s.jar", name, classifier); + File jar = new File(path); + Assert.state(jar.isFile(), () -> "Could not find " + path + ". Have you built it?"); return jar; } From 5da31aca461d2580d890f60fb23e55933daabdf9 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 6 Oct 2023 21:47:55 -0700 Subject: [PATCH 0554/1215] Attempt to fix NestedJarFile file lock issues on Windows Update `NestedJarFile.close()` to call `super.close()` so that the outer jar file is closed and files can hopefully be deleted on Windows. See gh-37668 --- .../java/org/springframework/boot/loader/jar/NestedJarFile.java | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java index b3de537026a0..2fe35c46c169 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java @@ -373,6 +373,7 @@ public int size() { @Override public void close() throws IOException { + super.close(); if (this.closed) { return; } From 9e4f160c17a4822ec62a5082816f7fb402f7ab8a Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 6 Oct 2023 23:44:55 -0700 Subject: [PATCH 0555/1215] Attempt to fix NestedJarFile file lock issues on Windows Update `DefaultCleanerTracking` and `@AssertFileChannelDataBlocksClosed` to capture and store the source object if it is a `Cleanable` so that it can be released later. Although the real cleaner cannot keep a reference to `obj`, it is safe for us to do so in tests since we are in control of the object lifecycle and we don't need it to be garbage collected. This commit also updates the `UrlJarFile` to call the cleaner so that it can be tracked. See gh-37668 --- .../loader/net/protocol/jar/UrlJarFile.java | 8 ++++++- .../net/protocol/jar/UrlNestedJarFile.java | 4 +++- .../boot/loader/ref/DefaultCleaner.java | 8 +++---- .../boot/loader/launch/ArchiveTests.java | 15 ++++++++---- .../protocol/jar/UrlJarFileFactoryTests.java | 2 ++ .../loader/ref/DefaultCleanerTracking.java | 4 ++-- ...tFileChannelDataBlocksClosedExtension.java | 24 ++++++++++++++----- 7 files changed, 47 insertions(+), 18 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFile.java index 513d79799c1f..e70af3081a90 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFile.java @@ -24,6 +24,8 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import org.springframework.boot.loader.ref.Cleaner; + /** * A {@link JarFile} subclass returned from a {@link JarUrlConnection}. * @@ -37,6 +39,8 @@ class UrlJarFile extends JarFile { UrlJarFile(File file, Runtime.Version version, Consumer closeAction) throws IOException { super(file, true, ZipFile.OPEN_READ, version); + // Registered only for test cleanup since parent class is JarFile + Cleaner.instance.register(this, null); this.manifest = new UrlJarManifest(super::getManifest); this.closeAction = closeAction; } @@ -53,7 +57,9 @@ public Manifest getManifest() throws IOException { @Override public void close() throws IOException { - this.closeAction.accept(this); + if (this.closeAction != null) { + this.closeAction.accept(this); + } super.close(); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlNestedJarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlNestedJarFile.java index 33bbed2e8375..1f9f62b2a32c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlNestedJarFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlNestedJarFile.java @@ -56,7 +56,9 @@ public JarEntry getEntry(String name) { @Override public void close() throws IOException { - this.closeAction.accept(this); + if (this.closeAction != null) { + this.closeAction.accept(this); + } super.close(); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ref/DefaultCleaner.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ref/DefaultCleaner.java index e592de5c85c5..01c6817a38a0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ref/DefaultCleaner.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ref/DefaultCleaner.java @@ -17,7 +17,7 @@ package org.springframework.boot.loader.ref; import java.lang.ref.Cleaner.Cleanable; -import java.util.function.Consumer; +import java.util.function.BiConsumer; /** * Default {@link Cleaner} implementation that delegates to {@link java.lang.ref.Cleaner}. @@ -28,15 +28,15 @@ class DefaultCleaner implements Cleaner { static final DefaultCleaner instance = new DefaultCleaner(); - static Consumer tracker; + static BiConsumer tracker; private final java.lang.ref.Cleaner cleaner = java.lang.ref.Cleaner.create(); @Override public Cleanable register(Object obj, Runnable action) { - Cleanable cleanable = this.cleaner.register(obj, action); + Cleanable cleanable = (action != null) ? this.cleaner.register(obj, action) : null; if (tracker != null) { - tracker.accept(cleanable); + tracker.accept(obj, cleanable); } return cleanable; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/ArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/ArchiveTests.java index 900511176f1f..77371024b4b2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/ArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/ArchiveTests.java @@ -26,6 +26,7 @@ import org.springframework.boot.loader.launch.Archive.Entry; import org.springframework.boot.loader.testsupport.TestJar; +import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -40,6 +41,7 @@ * * @author Phillip Webb */ +@AssertFileChannelDataBlocksClosed class ArchiveTests { @TempDir @@ -75,8 +77,9 @@ void createFromProtectionDomainCreatesJarArchive() throws Exception { CodeSource codeSource = mock(CodeSource.class); given(protectionDomain.getCodeSource()).willReturn(codeSource); given(codeSource.getLocation()).willReturn(jarFile.toURI().toURL()); - Archive archive = Archive.create(protectionDomain); - assertThat(archive).isInstanceOf(JarFileArchive.class); + try (Archive archive = Archive.create(protectionDomain)) { + assertThat(archive).isInstanceOf(JarFileArchive.class); + } } @Test @@ -99,13 +102,17 @@ void createFromFileWhenFileDoesNotExistThrowsException() { void createFromFileWhenJarFileReturnsJarFileArchive() throws Exception { File target = new File(this.temp, "missing"); TestJar.create(target); - assertThat(Archive.create(target)).isInstanceOf(JarFileArchive.class); + try (Archive archive = Archive.create(target)) { + assertThat(archive).isInstanceOf(JarFileArchive.class); + } } @Test void createFromFileWhenDirectoryReturnsExplodedFileArchive() throws Exception { File target = this.temp; - assertThat(Archive.create(target)).isInstanceOf(ExplodedArchive.class); + try (Archive archive = Archive.create(target)) { + assertThat(archive).isInstanceOf(ExplodedArchive.class); + } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactoryTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactoryTests.java index 5f16d8c6cb78..69ace5f6d752 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactoryTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactoryTests.java @@ -34,6 +34,7 @@ import org.springframework.boot.loader.net.protocol.Handlers; import org.springframework.boot.loader.testsupport.TestJar; +import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed; import static org.assertj.core.api.Assertions.assertThat; @@ -42,6 +43,7 @@ * * @author Phillip Webb */ +@AssertFileChannelDataBlocksClosed class UrlJarFileFactoryTests { @TempDir diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ref/DefaultCleanerTracking.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ref/DefaultCleanerTracking.java index 8049acd7d037..c2342c7a5425 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ref/DefaultCleanerTracking.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/ref/DefaultCleanerTracking.java @@ -17,7 +17,7 @@ package org.springframework.boot.loader.ref; import java.lang.ref.Cleaner.Cleanable; -import java.util.function.Consumer; +import java.util.function.BiConsumer; /** * Utility that allows tests to set a tracker on {@link DefaultCleaner}. @@ -29,7 +29,7 @@ public final class DefaultCleanerTracking { private DefaultCleanerTracking() { } - public static void set(Consumer tracker) { + public static void set(BiConsumer tracker) { DefaultCleaner.tracker = tracker; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/AssertFileChannelDataBlocksClosedExtension.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/AssertFileChannelDataBlocksClosedExtension.java index caa8d23e2162..997e0e02c50c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/AssertFileChannelDataBlocksClosedExtension.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/AssertFileChannelDataBlocksClosedExtension.java @@ -16,6 +16,8 @@ package org.springframework.boot.loader.zip; +import java.io.Closeable; +import java.io.IOException; import java.lang.ref.Cleaner.Cleanable; import java.nio.channels.FileChannel; import java.nio.file.Path; @@ -57,7 +59,9 @@ private static class OpenFilesTracker implements Tracker { private final Set paths = new LinkedHashSet<>(); - private final List cleanup = new ArrayList<>(); + private final List clean = new ArrayList<>(); + + private final List close = new ArrayList<>(); @Override public void openedFileChannel(Path path, FileChannel fileChannel) { @@ -71,16 +75,24 @@ public void closedFileChannel(Path path, FileChannel fileChannel) { void clear() { this.paths.clear(); - this.cleanup.clear(); + this.clean.clear(); } - void assertAllClosed() { - this.cleanup.forEach(Cleanable::clean); + void assertAllClosed() throws IOException { + for (Closeable closeable : this.close) { + closeable.close(); + } + this.clean.forEach(Cleanable::clean); assertThat(this.paths).as("open paths").isEmpty(); } - private void addedCleanable(Cleanable cleanable) { - this.cleanup.add(cleanable); + private void addedCleanable(Object obj, Cleanable cleanable) { + if (cleanable != null) { + this.clean.add(cleanable); + } + if (obj instanceof Closeable closeable) { + this.close.add(closeable); + } } } From dc9d3c2f1efd9f1c137b5c39fec5eb320446972b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Oct 2023 12:13:06 +0100 Subject: [PATCH 0556/1215] Upgrade to Commons Pool2 2.12.0 Closes gh-37770 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 74402a1f3183..246f7af19be7 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -202,7 +202,7 @@ bom { ] } } - library("Commons Pool2", "2.11.1") { + library("Commons Pool2", "2.12.0") { group("org.apache.commons") { modules = [ "commons-pool2" From 925578fa0c0d7a292f5d987806f0239120005967 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Oct 2023 12:13:11 +0100 Subject: [PATCH 0557/1215] Upgrade to Couchbase Client 3.4.11 Closes gh-37771 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 246f7af19be7..e9efe7f5d3b6 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -209,7 +209,7 @@ bom { ] } } - library("Couchbase Client", "3.4.10") { + library("Couchbase Client", "3.4.11") { prohibit { versionRange "[3.4.9]" because "it contains unshaded io.opentelemetry classes that break our Otel integration" From 47d60052d927248a801790eb02f22546924cd489 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Oct 2023 12:13:16 +0100 Subject: [PATCH 0558/1215] Upgrade to Dropwizard Metrics 4.2.20 Closes gh-37772 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e9efe7f5d3b6..d28227d3ae93 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -246,7 +246,7 @@ bom { ] } } - library("Dropwizard Metrics", "4.2.19") { + library("Dropwizard Metrics", "4.2.20") { group("io.dropwizard.metrics") { imports = [ "metrics-bom" From 6625f6519d41304217ba8755e855654c97f03608 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Oct 2023 12:13:21 +0100 Subject: [PATCH 0559/1215] Upgrade to Elasticsearch Client 8.10.2 Closes gh-37773 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d28227d3ae93..fa754d749920 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -268,7 +268,7 @@ bom { ] } } - library("Elasticsearch Client", "8.10.1") { + library("Elasticsearch Client", "8.10.2") { group("org.elasticsearch.client") { modules = [ "elasticsearch-rest-client" { From afb8a76def9dbe9f34f0357cb524ebd007a734c6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Oct 2023 12:13:27 +0100 Subject: [PATCH 0560/1215] Upgrade to Flyway 9.22.2 Closes gh-37774 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index fa754d749920..b866abf789db 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -285,7 +285,7 @@ bom { ] } } - library("Flyway", "9.22.1") { + library("Flyway", "9.22.2") { group("org.flywaydb") { modules = [ "flyway-core", From 9a78fb38c23161c02124d0bfb73a5dc3d1d1b6eb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Oct 2023 12:13:31 +0100 Subject: [PATCH 0561/1215] Upgrade to HttpCore5 5.2.3 Closes gh-37775 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index b866abf789db..2ff6f6b928dc 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -455,7 +455,7 @@ bom { ] } } - library("HttpCore5", "5.2.2") { + library("HttpCore5", "5.2.3") { group("org.apache.httpcomponents.core5") { modules = [ "httpcore5", From e645b0b61ebfd3741c031f652772f71ff1ae0375 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Oct 2023 12:13:37 +0100 Subject: [PATCH 0562/1215] Upgrade to Jedis 5.0.1 Closes gh-37776 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 2ff6f6b928dc..972397d2ee8e 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -656,7 +656,7 @@ bom { ] } } - library("Jedis", "4.4.4") { + library("Jedis", "5.0.1") { group("redis.clients") { modules = [ "jedis" From 48059417b53353d78e88e1ede737d0fcaf956ef4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Oct 2023 12:13:42 +0100 Subject: [PATCH 0563/1215] Upgrade to Kafka 3.6.0 Closes gh-37777 --- spring-boot-project/spring-boot-autoconfigure/build.gradle | 4 +++- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- spring-boot-project/spring-boot-docs/build.gradle | 4 +++- .../spring-boot-smoke-test-kafka/build.gradle | 4 +++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index 25d111149932..452c3e223536 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -241,7 +241,9 @@ dependencies { testImplementation("org.springframework:spring-test") testImplementation("org.springframework:spring-core-test") testImplementation("org.springframework.graphql:spring-graphql-test") - testImplementation("org.springframework.kafka:spring-kafka-test") + testImplementation("org.springframework.kafka:spring-kafka-test") { + exclude group: "commons-logging", module: "commons-logging" + } testImplementation("org.springframework.pulsar:spring-pulsar-cache-provider-caffeine") testImplementation("org.springframework.security:spring-security-test") testImplementation("org.testcontainers:cassandra") diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 972397d2ee8e..7f4231c0419c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -752,7 +752,7 @@ bom { ] } } - library("Kafka", "3.5.1") { + library("Kafka", "3.6.0") { group("org.apache.kafka") { modules = [ "connect", diff --git a/spring-boot-project/spring-boot-docs/build.gradle b/spring-boot-project/spring-boot-docs/build.gradle index 818d34df172f..fed9578b549e 100644 --- a/spring-boot-project/spring-boot-docs/build.gradle +++ b/spring-boot-project/spring-boot-docs/build.gradle @@ -162,7 +162,9 @@ dependencies { implementation("org.springframework.graphql:spring-graphql") implementation("org.springframework.graphql:spring-graphql-test") implementation("org.springframework.kafka:spring-kafka") - implementation("org.springframework.kafka:spring-kafka-test") + implementation("org.springframework.kafka:spring-kafka-test") { + exclude group: "commons-logging", module: "commons-logging" + } implementation("org.springframework.pulsar:spring-pulsar") implementation("org.springframework.pulsar:spring-pulsar-reactive") implementation("org.springframework.restdocs:spring-restdocs-mockmvc") diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/build.gradle index 33a82ff144bd..abe8f0e0f35c 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/build.gradle @@ -11,5 +11,7 @@ dependencies { testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) testImplementation("org.awaitility:awaitility") - testImplementation("org.springframework.kafka:spring-kafka-test") + testImplementation("org.springframework.kafka:spring-kafka-test") { + exclude group: "commons-logging", module: "commons-logging" + } } From 0369d0c40a63f57108bcf5ac14eda7865cdbcd63 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Oct 2023 12:13:47 +0100 Subject: [PATCH 0564/1215] Upgrade to Maven Shade Plugin 3.5.1 Closes gh-37778 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 7f4231c0419c..11c0fa3b146c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -970,7 +970,7 @@ bom { ] } } - library("Maven Shade Plugin", "3.5.0") { + library("Maven Shade Plugin", "3.5.1") { group("org.apache.maven.plugins") { plugins = [ "maven-shade-plugin" From d0fd48b38eec2f574b4107d5139c157f4064b0ec Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Oct 2023 12:13:52 +0100 Subject: [PATCH 0565/1215] Upgrade to Mockito 5.6.0 Closes gh-37779 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 11c0fa3b146c..28a638990826 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1020,7 +1020,7 @@ bom { ] } } - library("Mockito", "5.5.0") { + library("Mockito", "5.6.0") { group("org.mockito") { imports = [ "mockito-bom" From ce707da4167616dda66215edc067a5d33d573443 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Oct 2023 12:13:56 +0100 Subject: [PATCH 0566/1215] Upgrade to Neo4j Java Driver 5.13.0 Closes gh-37780 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 28a638990826..a7b09d02fbff 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1073,7 +1073,7 @@ bom { ] } } - library("Neo4j Java Driver", "5.12.0") { + library("Neo4j Java Driver", "5.13.0") { group("org.neo4j.driver") { modules = [ "neo4j-java-driver" From 0cdf179f3e466531b48bb0341ee5c3351fa7f6e1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Oct 2023 12:14:02 +0100 Subject: [PATCH 0567/1215] Upgrade to Netty 4.1.99.Final Closes gh-37781 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index a7b09d02fbff..e189ab488b6d 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1080,7 +1080,7 @@ bom { ] } } - library("Netty", "4.1.97.Final") { + library("Netty", "4.1.99.Final") { group("io.netty") { imports = [ "netty-bom" From c3e001f8242a20e7c22c3af48ff366a443a3fec1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Oct 2023 12:14:07 +0100 Subject: [PATCH 0568/1215] Upgrade to OpenTelemetry 1.31.0 Closes gh-37782 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e189ab488b6d..dea427404d47 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1094,7 +1094,7 @@ bom { ] } } - library("OpenTelemetry", "1.30.1") { + library("OpenTelemetry", "1.31.0") { group("io.opentelemetry") { imports = [ "opentelemetry-bom" From 4c110e2288570b21b3ee88e38d06557fcbcfff02 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Oct 2023 12:14:12 +0100 Subject: [PATCH 0569/1215] Upgrade to Pooled JMS 3.1.4 Closes gh-37783 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index dea427404d47..29cd973f8c59 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1118,7 +1118,7 @@ bom { ] } } - library("Pooled JMS", "3.1.3") { + library("Pooled JMS", "3.1.4") { group("org.messaginghub") { modules = [ "pooled-jms" From a0fc837c58ca2d4d4088d01a8635f2fdbcffa55e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Oct 2023 12:14:16 +0100 Subject: [PATCH 0570/1215] Upgrade to R2DBC MySQL 1.0.4 Closes gh-37784 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 29cd973f8c59..caa5a64d5662 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1269,7 +1269,7 @@ bom { ] } } - library("R2DBC MySQL", "1.0.3") { + library("R2DBC MySQL", "1.0.4") { group("io.asyncer") { modules = [ "r2dbc-mysql" From 6ff37859dc17741aad58d28cdefde5997b754dca Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Oct 2023 12:14:21 +0100 Subject: [PATCH 0571/1215] Upgrade to Rabbit AMQP Client 5.19.0 Closes gh-37785 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index caa5a64d5662..72c63be9e4b3 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1308,7 +1308,7 @@ bom { ] } } - library("Rabbit AMQP Client", "5.18.0") { + library("Rabbit AMQP Client", "5.19.0") { group("com.rabbitmq") { modules = [ "amqp-client" From 6c4d078c0ad31e080684502c3783d9e7aa20898c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Oct 2023 12:14:26 +0100 Subject: [PATCH 0572/1215] Upgrade to Rabbit Stream Client 0.13.0 Closes gh-37786 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 72c63be9e4b3..5f9e597d8b08 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1315,7 +1315,7 @@ bom { ] } } - library("Rabbit Stream Client", "0.12.0") { + library("Rabbit Stream Client", "0.13.0") { group("com.rabbitmq") { modules = [ "stream-client" From 93c5e75298542390c85caabbecc71bbf557d73bf Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Oct 2023 12:14:32 +0100 Subject: [PATCH 0573/1215] Upgrade to RxJava3 3.1.8 Closes gh-37787 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5f9e597d8b08..7c0d454270eb 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1356,7 +1356,7 @@ bom { ] } } - library("RxJava3", "3.1.7") { + library("RxJava3", "3.1.8") { group("io.reactivex.rxjava3") { modules = [ "rxjava" From 328e154b6bbafad6b0deb62df5a948e5ddbe6063 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Oct 2023 12:14:37 +0100 Subject: [PATCH 0574/1215] Upgrade to Selenium 4.13.0 Closes gh-37788 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 7c0d454270eb..906f935a5666 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1452,7 +1452,7 @@ bom { ] } } - library("Selenium", "4.12.1") { + library("Selenium", "4.13.0") { group("org.seleniumhq.selenium") { imports = [ "selenium-bom" From b76d73952dde3dcfafe37fe2bb84438c8519283a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Oct 2023 12:14:42 +0100 Subject: [PATCH 0575/1215] Upgrade to Selenium HtmlUnit 4.13.0 Closes gh-37789 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 906f935a5666..885c2361b812 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1459,7 +1459,7 @@ bom { ] } } - library("Selenium HtmlUnit", "4.12.0") { + library("Selenium HtmlUnit", "4.13.0") { group("org.seleniumhq.selenium") { modules = [ "htmlunit-driver" From 0169c586644105e526faaa8d3379b73ae36457ed Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Oct 2023 12:14:47 +0100 Subject: [PATCH 0576/1215] Upgrade to Testcontainers 1.19.1 Closes gh-37790 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 885c2361b812..c91b4211d855 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1645,7 +1645,7 @@ bom { ] } } - library("Testcontainers", "1.19.0") { + library("Testcontainers", "1.19.1") { group("org.testcontainers") { imports = [ "testcontainers-bom" From 9233d7f2cfffed504e8e20da58216907c9bb675a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 9 Oct 2023 12:14:52 +0100 Subject: [PATCH 0577/1215] Upgrade to UnboundID LDAPSDK 6.0.10 Closes gh-37791 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c91b4211d855..de77ffdd3001 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1698,7 +1698,7 @@ bom { ] } } - library("UnboundID LDAPSDK", "6.0.9") { + library("UnboundID LDAPSDK", "6.0.10") { group("com.unboundid") { modules = [ "unboundid-ldapsdk" From 0a40fddaa713db6548c3f3ad75913514df2d5386 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Mon, 9 Oct 2023 22:43:10 -0500 Subject: [PATCH 0578/1215] Upgrade to Pulsar Reactive 0.4.0 This commit updates the Reactive client used by Spring Pulsar to version 0.4.0. The updated client fixes an issue where the non-reactive and reactive shaded producer cache had the same relocation prefix. This allows the removal of the shaded relocation prefixes from the checkRuntimeClasspathForConflicts ignore closure. See gh-37801 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- .../spring-boot-starter-pulsar-reactive/build.gradle | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index de77ffdd3001..abdfacd240ca 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1218,7 +1218,7 @@ bom { ] } } - library("Pulsar Reactive", "0.3.0") { + library("Pulsar Reactive", "0.4.0") { group("org.apache.pulsar") { modules = [ "pulsar-client-reactive-adapter", diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-pulsar-reactive/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-pulsar-reactive/build.gradle index 777b69567bb9..22b23cf2aff3 100644 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-pulsar-reactive/build.gradle +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-pulsar-reactive/build.gradle @@ -11,9 +11,6 @@ dependencies { checkRuntimeClasspathForConflicts { ignore { name -> name.startsWith("org/bouncycastle/") || - name.matches("^org\\/apache\\/pulsar\\/.*\\/package-info.class\$") || - name.equals("findbugsExclude.xml") || - name.startsWith("org/springframework/pulsar/shade/com/github/benmanes/caffeine/") || - name.startsWith("org/springframework/pulsar/shade/com/google/errorprone/") || - name.startsWith("org/springframework/pulsar/shade/org/checkerframework/") } + name.matches("^org\\/apache\\/pulsar\\/.*\\/package-info.class\$") || + name.equals("findbugsExclude.xml") } } From 4b5e50ba940848a58982dfe1a4e4e1b9f47e8af9 Mon Sep 17 00:00:00 2001 From: Olga MaciaszekSharma Date: Fri, 29 Sep 2023 15:20:52 +0200 Subject: [PATCH 0579/1215] Instrument user-created DataSource for checkpoint-restore See gh-37630 --- .../jdbc/DataSourceAutoConfiguration.java | 3 +- ...aSourceCheckpointRestoreConfiguration.java | 57 +++++++++++++++++++ .../jdbc/DataSourceConfiguration.java | 8 --- .../HikariDataSourceConfigurationTests.java | 25 ++++++++ 4 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceCheckpointRestoreConfiguration.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java index 5a41f5d0bdef..5ad8ff75ae83 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java @@ -51,13 +51,14 @@ * @author Phillip Webb * @author Stephane Nicoll * @author Kazuki Shimizu + * @author Olga Maciaszek-Sharma * @since 1.0.0 */ @AutoConfiguration(before = SqlInitializationAutoConfiguration.class) @ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) @ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory") @EnableConfigurationProperties(DataSourceProperties.class) -@Import(DataSourcePoolMetadataProvidersConfiguration.class) +@Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceCheckpointRestoreConfiguration.class }) public class DataSourceAutoConfiguration { @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceCheckpointRestoreConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceCheckpointRestoreConfiguration.java new file mode 100644 index 000000000000..6156ea591560 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceCheckpointRestoreConfiguration.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2023 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.jdbc; + +import javax.sql.DataSource; + +import com.zaxxer.hikari.HikariDataSource; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnCheckpointRestore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.jdbc.HikariCheckpointRestoreLifecycle; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Checkpoint-restore specific configuration. + * + * @author Olga Maciaszek-Sharma + * @since 3.2.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnCheckpointRestore +@ConditionalOnBean(DataSource.class) +public class DataSourceCheckpointRestoreConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(HikariDataSource.class) + @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource", + matchIfMissing = true) + static class Hikari { + + @Bean + @ConditionalOnMissingBean + HikariCheckpointRestoreLifecycle hikariCheckpointRestoreLifecycle(DataSource dataSource) { + return new HikariCheckpointRestoreLifecycle(dataSource); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java index dc6ec5d11b62..1cebf37cd5e0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration.java @@ -25,14 +25,12 @@ import oracle.ucp.jdbc.PoolDataSourceImpl; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.condition.ConditionalOnCheckpointRestore; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jdbc.DatabaseDriver; -import org.springframework.boot.jdbc.HikariCheckpointRestoreLifecycle; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.StringUtils; @@ -124,12 +122,6 @@ HikariDataSource dataSource(DataSourceProperties properties, JdbcConnectionDetai return dataSource; } - @Bean - @ConditionalOnCheckpointRestore - HikariCheckpointRestoreLifecycle hikariCheckpointRestoreLifecycle(DataSource hikariDataSource) { - return new HikariCheckpointRestoreLifecycle(hikariDataSource); - } - } /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java index 617f2764562e..17f5183ae335 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java @@ -25,6 +25,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.jdbc.HikariCheckpointRestoreLifecycle; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.testsupport.classpath.ClassPathOverrides; @@ -42,6 +43,7 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Olga Maciaszek-Sharma */ class HikariDataSourceConfigurationTests { @@ -148,6 +150,14 @@ void whenCheckpointRestoreIsNotAvailableHikariAutoConfigDoesNotRegisterLifecycle .run((context) -> assertThat(context).doesNotHaveBean(HikariCheckpointRestoreLifecycle.class)); } + @Test + @ClassPathOverrides("org.crac:crac:1.3.0") + void whenCheckpointRestoreIsAvailableAndDataSourceInstantiatedByUserHikariAutoConfigRegistersLifecycleBean() { + this.contextRunner.withUserConfiguration(UserDataSourceConfiguration.class) + .withPropertyValues("spring.datasource.type=" + HikariDataSource.class.getName()) + .run((context) -> assertThat(context).hasSingleBean(HikariCheckpointRestoreLifecycle.class)); + } + @Configuration(proxyBeanMethods = false) static class ConnectionDetailsConfiguration { @@ -178,4 +188,19 @@ public Object postProcessBeforeInitialization(Object bean, String beanName) thro } + @Configuration(proxyBeanMethods = false) + static class UserDataSourceConfiguration { + + @Bean + DataSource dataSource() { + return DataSourceBuilder.create() + .driverClassName("org.postgresql.Driver") + .url("jdbc:postgresql://localhost:5432/database") + .username("user") + .password("password") + .build(); + } + + } + } From 31008def76ecfc12376f69b491de0d0a0ff3079c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 10 Oct 2023 10:26:37 +0100 Subject: [PATCH 0580/1215] Polish "Instrument user-created DataSource for checkpoint-restore" See gh-37630 --- .../jdbc/DataSourceCheckpointRestoreConfiguration.java | 6 +----- .../jdbc/HikariDataSourceConfigurationTests.java | 6 ++---- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceCheckpointRestoreConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceCheckpointRestoreConfiguration.java index 6156ea591560..79d1a048b86d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceCheckpointRestoreConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceCheckpointRestoreConfiguration.java @@ -24,7 +24,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnCheckpointRestore; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.jdbc.HikariCheckpointRestoreLifecycle; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -33,17 +32,14 @@ * Checkpoint-restore specific configuration. * * @author Olga Maciaszek-Sharma - * @since 3.2.0 */ @Configuration(proxyBeanMethods = false) @ConditionalOnCheckpointRestore @ConditionalOnBean(DataSource.class) -public class DataSourceCheckpointRestoreConfiguration { +class DataSourceCheckpointRestoreConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnClass(HikariDataSource.class) - @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource", - matchIfMissing = true) static class Hikari { @Bean diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java index 17f5183ae335..3019be543757 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/HikariDataSourceConfigurationTests.java @@ -140,21 +140,19 @@ void whenCheckpointRestoreIsAvailableHikariAutoConfigRegistersLifecycleBean() { @ClassPathOverrides("org.crac:crac:1.3.0") void whenCheckpointRestoreIsAvailableAndDataSourceHasBeenWrappedHikariAutoConfigRegistersLifecycleBean() { this.contextRunner.withUserConfiguration(DataSourceWrapperConfiguration.class) - .withPropertyValues("spring.datasource.type=" + HikariDataSource.class.getName()) .run((context) -> assertThat(context).hasSingleBean(HikariCheckpointRestoreLifecycle.class)); } @Test void whenCheckpointRestoreIsNotAvailableHikariAutoConfigDoesNotRegisterLifecycleBean() { - this.contextRunner.withPropertyValues("spring.datasource.type=" + HikariDataSource.class.getName()) + this.contextRunner .run((context) -> assertThat(context).doesNotHaveBean(HikariCheckpointRestoreLifecycle.class)); } @Test @ClassPathOverrides("org.crac:crac:1.3.0") - void whenCheckpointRestoreIsAvailableAndDataSourceInstantiatedByUserHikariAutoConfigRegistersLifecycleBean() { + void whenCheckpointRestoreIsAvailableAndDataSourceIsFromUserConfigurationHikariAutoConfigRegistersLifecycleBean() { this.contextRunner.withUserConfiguration(UserDataSourceConfiguration.class) - .withPropertyValues("spring.datasource.type=" + HikariDataSource.class.getName()) .run((context) -> assertThat(context).hasSingleBean(HikariCheckpointRestoreLifecycle.class)); } From f4ed0a2df5588562cdfb2213a05a837b1887bcca Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 10 Oct 2023 10:56:37 +0100 Subject: [PATCH 0581/1215] Upgrade to Byte Buddy 1.14.9 Closes gh-37802 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index abdfacd240ca..47fb2975bfaa 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -123,7 +123,7 @@ bom { ] } } - library("Byte Buddy", "1.14.8") { + library("Byte Buddy", "1.14.9") { group("net.bytebuddy") { modules = [ "byte-buddy", From 8080c5d4e896bd6e2313e1592378262bddc77b33 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 10 Oct 2023 10:56:56 +0100 Subject: [PATCH 0582/1215] Upgrade to Jetty 12.0.2 Closes gh-37803 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 47fb2975bfaa..16c379b8bf90 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -677,7 +677,7 @@ bom { ] } } - library("Jetty", "12.0.1") { + library("Jetty", "12.0.2") { group("org.eclipse.jetty.ee10") { imports = [ "jetty-ee10-bom" From 7b1059a4b510e9fd58e6a3816e919ea52088546d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 10 Oct 2023 12:27:38 +0100 Subject: [PATCH 0583/1215] Revert "Upgrade to Jetty 12.0.2" This reverts commit 8080c5d4e896bd6e2313e1592378262bddc77b33. See gh-37803 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 16c379b8bf90..47fb2975bfaa 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -677,7 +677,7 @@ bom { ] } } - library("Jetty", "12.0.2") { + library("Jetty", "12.0.1") { group("org.eclipse.jetty.ee10") { imports = [ "jetty-ee10-bom" From 15ee305ef3bf71fd9fcf261fcc83463d8ee38f23 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 10 Oct 2023 15:00:02 +0100 Subject: [PATCH 0584/1215] Upgrade to Jetty 12.0.2 Closes gh-37803 --- .../spring-boot-dependencies/build.gradle | 2 +- .../jetty/JettyServletWebServerFactory.java | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 47fb2975bfaa..16c379b8bf90 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -677,7 +677,7 @@ bom { ] } } - library("Jetty", "12.0.1") { + library("Jetty", "12.0.2") { group("org.eclipse.jetty.ee10") { imports = [ "jetty-ee10-bom" diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java index 43be8f876671..680090e52ae2 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java @@ -55,8 +55,10 @@ import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields.Mutable; +import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.MimeTypes.Wrapper; +import org.eclipse.jetty.http.SetCookieParser; import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.ConnectionFactory; @@ -787,6 +789,8 @@ public boolean handle(Request request, Response response, Callback callback) thr private final class SameSiteCookieHttpStreamWrapper extends HttpStream.Wrapper { + private static final SetCookieParser setCookieParser = SetCookieParser.newInstance(); + private final Request request; private SameSiteCookieHttpStreamWrapper(HttpStream wrapped, Request request) { @@ -799,15 +803,18 @@ public void prepareResponse(Mutable headers) { super.prepareResponse(headers); ListIterator headerFields = headers.listIterator(); while (headerFields.hasNext()) { - HttpCookieUtils.SetCookieHttpField updatedField = applySameSiteIfNecessary(headerFields.next()); + HttpField updatedField = applySameSiteIfNecessary(headerFields.next()); if (updatedField != null) { headerFields.set(updatedField); } } } - private HttpCookieUtils.SetCookieHttpField applySameSiteIfNecessary(HttpField headerField) { - HttpCookie cookie = HttpCookieUtils.getSetCookie(headerField); + private HttpField applySameSiteIfNecessary(HttpField headerField) { + if (headerField.getHeader() != HttpHeader.SET_COOKIE) { + return null; + } + HttpCookie cookie = setCookieParser.parse(headerField.getValue()); if (cookie == null) { return null; } From 1d410dccea0d429707728d8205a9467ae44188e3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 10 Oct 2023 16:06:30 +0100 Subject: [PATCH 0585/1215] Adapt to breaking API change in Framework See gh-37710 --- .../reactor/netty/ReactorNettyConfigurations.java | 2 +- .../autoconfigure/rsocket/RSocketServerAutoConfiguration.java | 2 +- .../web/reactive/ReactiveWebServerFactoryConfiguration.java | 2 +- .../client/ClientHttpConnectorFactoryConfiguration.java | 2 +- .../function/client/ReactorClientHttpConnectorFactory.java | 2 +- .../rsocket/RSocketServerAutoConfigurationTests.java | 2 +- .../client/ClientHttpConnectorAutoConfigurationTests.java | 2 +- .../client/ReactorClientHttpConnectorFactoryTests.java | 2 +- .../MyReactorNettyClientConfiguration.java | 4 ++-- .../boot/rsocket/netty/NettyRSocketServerFactory.java | 2 +- .../web/embedded/netty/NettyReactiveWebServerFactory.java | 2 +- .../boot/web/embedded/netty/NettyWebServer.java | 2 +- .../boot/rsocket/netty/NettyRSocketServerFactoryTests.java | 2 +- .../embedded/netty/NettyReactiveWebServerFactoryTests.java | 2 +- 14 files changed, 15 insertions(+), 15 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/netty/ReactorNettyConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/netty/ReactorNettyConfigurations.java index b6e3ba354805..35867272331f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/netty/ReactorNettyConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/reactor/netty/ReactorNettyConfigurations.java @@ -20,7 +20,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.client.reactive.ReactorResourceFactory; +import org.springframework.http.client.ReactorResourceFactory; /** * Configurations for Reactor Netty. Those should be {@code @Import} in a regular diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfiguration.java index e329ef099e37..c9ae1d033847 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfiguration.java @@ -47,7 +47,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.io.buffer.NettyDataBufferFactory; -import org.springframework.http.client.reactive.ReactorResourceFactory; +import org.springframework.http.client.ReactorResourceFactory; import org.springframework.messaging.rsocket.RSocketStrategies; import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler; import org.springframework.util.unit.DataSize; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryConfiguration.java index 8752f5025fc4..74880e24d3c3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveWebServerFactoryConfiguration.java @@ -39,7 +39,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.http.client.reactive.ReactorResourceFactory; +import org.springframework.http.client.ReactorResourceFactory; /** * Configuration classes for reactive web servers diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorFactoryConfiguration.java index e5ea5cca20ba..d3f9aa1b0d8b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorFactoryConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorFactoryConfiguration.java @@ -27,7 +27,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.http.client.reactive.ReactorResourceFactory; +import org.springframework.http.client.ReactorResourceFactory; /** * Configuration classes for WebClient client connectors. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ReactorClientHttpConnectorFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ReactorClientHttpConnectorFactory.java index b5dcd6b136a1..3f596126ac51 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ReactorClientHttpConnectorFactory.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ReactorClientHttpConnectorFactory.java @@ -31,8 +31,8 @@ import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslManagerBundle; import org.springframework.boot.ssl.SslOptions; +import org.springframework.http.client.ReactorResourceFactory; import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.http.client.reactive.ReactorResourceFactory; import org.springframework.util.function.ThrowingConsumer; /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfigurationTests.java index 0ddf63903d7b..62569319c380 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/rsocket/RSocketServerAutoConfigurationTests.java @@ -34,7 +34,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.codec.CharSequenceEncoder; import org.springframework.core.codec.StringDecoder; -import org.springframework.http.client.reactive.ReactorResourceFactory; +import org.springframework.http.client.ReactorResourceFactory; import org.springframework.messaging.rsocket.RSocketStrategies; import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler; import org.springframework.util.unit.DataSize; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorAutoConfigurationTests.java index b3fe66cea5ce..9c0d2ee1f2b3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorAutoConfigurationTests.java @@ -27,9 +27,9 @@ import org.springframework.boot.web.reactive.function.client.WebClientCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ReactorResourceFactory; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.http.client.reactive.ReactorResourceFactory; import org.springframework.web.reactive.function.client.WebClient; import static org.assertj.core.api.Assertions.assertThat; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ReactorClientHttpConnectorFactoryTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ReactorClientHttpConnectorFactoryTests.java index 632d8f707636..951941d02446 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ReactorClientHttpConnectorFactoryTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ReactorClientHttpConnectorFactoryTests.java @@ -19,7 +19,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.springframework.http.client.reactive.ReactorResourceFactory; +import org.springframework.http.client.ReactorResourceFactory; /** * Tests for {@link ReactorClientHttpConnectorFactory}. diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/httpclients/webclientreactornettycustomization/MyReactorNettyClientConfiguration.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/httpclients/webclientreactornettycustomization/MyReactorNettyClientConfiguration.java index 8b67fb450e23..b47d7dd48408 100644 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/httpclients/webclientreactornettycustomization/MyReactorNettyClientConfiguration.java +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/howto/httpclients/webclientreactornettycustomization/MyReactorNettyClientConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -22,9 +22,9 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ReactorResourceFactory; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.http.client.reactive.ReactorResourceFactory; @Configuration(proxyBeanMethods = false) public class MyReactorNettyClientConfiguration { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactory.java index bd1d64283515..7547b8d68e6f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactory.java @@ -45,7 +45,7 @@ import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.SslStoreProvider; import org.springframework.boot.web.server.WebServerSslBundle; -import org.springframework.http.client.reactive.ReactorResourceFactory; +import org.springframework.http.client.ReactorResourceFactory; import org.springframework.util.Assert; import org.springframework.util.unit.DataSize; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java index 306077ffbdcf..8f7fa831a8cd 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java @@ -33,7 +33,7 @@ import org.springframework.boot.web.server.Shutdown; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.WebServer; -import org.springframework.http.client.reactive.ReactorResourceFactory; +import org.springframework.http.client.ReactorResourceFactory; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; import org.springframework.util.Assert; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java index 208ce2a2e75d..543ffdbd9fa1 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java @@ -43,7 +43,7 @@ import org.springframework.boot.web.server.Shutdown; import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.server.WebServerException; -import org.springframework.http.client.reactive.ReactorResourceFactory; +import org.springframework.http.client.ReactorResourceFactory; import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; import org.springframework.util.Assert; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactoryTests.java index 4d206e26fee1..0c1f8196b200 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactoryTests.java @@ -56,7 +56,7 @@ import org.springframework.core.codec.CharSequenceEncoder; import org.springframework.core.codec.StringDecoder; import org.springframework.core.io.buffer.NettyDataBufferFactory; -import org.springframework.http.client.reactive.ReactorResourceFactory; +import org.springframework.http.client.ReactorResourceFactory; import org.springframework.messaging.rsocket.RSocketRequester; import org.springframework.messaging.rsocket.RSocketStrategies; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java index 652042688c85..3694ee942d8f 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java @@ -40,8 +40,8 @@ import org.springframework.boot.web.server.Shutdown; import org.springframework.boot.web.server.Ssl; import org.springframework.http.MediaType; +import org.springframework.http.client.ReactorResourceFactory; import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.http.client.reactive.ReactorResourceFactory; import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.WebClient; From 5280cfeec053d94dfca1314e415e8310d67a0b5f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 10 Oct 2023 16:39:12 +0100 Subject: [PATCH 0586/1215] Adapt to breaking API change in Micrometer See gh-37703 --- .../spring-boot-actuator-autoconfigure/build.gradle | 2 +- .../jms/JmsTemplateObservationAutoConfiguration.java | 2 +- .../jms/JmsTemplateObservationAutoConfigurationTests.java | 4 ++-- spring-boot-project/spring-boot-actuator/build.gradle | 2 +- spring-boot-project/spring-boot-docs/build.gradle | 2 +- .../MyReactorNettyClientConfiguration.kt | 4 ++-- .../spring-boot-starter-actuator/build.gradle | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index 38cfeca4648b..14d6c88a0b8b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -37,7 +37,7 @@ dependencies { optional("io.dropwizard.metrics:metrics-jmx") optional("io.lettuce:lettuce-core") optional("io.micrometer:micrometer-observation") - optional("io.micrometer:micrometer-core") + optional("io.micrometer:micrometer-jakarta9") optional("io.micrometer:micrometer-tracing") optional("io.micrometer:micrometer-tracing-bridge-brave") optional("io.micrometer:micrometer-tracing-bridge-otel") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfiguration.java index faa7db23e62d..9629468e6051 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfiguration.java @@ -16,7 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.observation.jms; -import io.micrometer.core.instrument.binder.jms.JmsPublishObservationContext; +import io.micrometer.jakarta9.instrument.jms.JmsPublishObservationContext; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import jakarta.jms.Message; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfigurationTests.java index 3abd6c1d64d2..dd097676b2d5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfigurationTests.java @@ -51,8 +51,8 @@ void shouldConfigureObservationRegistryOnTemplate() { } @Test - void shouldBackOffWhenMircrometerCoreIsNotPresent() { - this.contextRunner.withClassLoader(new FilteredClassLoader("io.micrometer.core")).run((context) -> { + void shouldBackOffWhenMicrometerJakartaIsNotPresent() { + this.contextRunner.withClassLoader(new FilteredClassLoader("io.micrometer.jakarta")).run((context) -> { JmsTemplate jmsTemplate = context.getBean(JmsTemplate.class); assertThat(jmsTemplate).extracting("observationRegistry").isNull(); }); diff --git a/spring-boot-project/spring-boot-actuator/build.gradle b/spring-boot-project/spring-boot-actuator/build.gradle index 3f7999ca49d4..e9a027a00e33 100644 --- a/spring-boot-project/spring-boot-actuator/build.gradle +++ b/spring-boot-project/spring-boot-actuator/build.gradle @@ -22,7 +22,7 @@ dependencies { optional("com.zaxxer:HikariCP") optional("io.lettuce:lettuce-core") optional("io.micrometer:micrometer-observation") - optional("io.micrometer:micrometer-core") + optional("io.micrometer:micrometer-jakarta9") optional("io.micrometer:micrometer-tracing") optional("io.micrometer:micrometer-registry-prometheus") optional("io.prometheus:simpleclient_pushgateway") { diff --git a/spring-boot-project/spring-boot-docs/build.gradle b/spring-boot-project/spring-boot-docs/build.gradle index fed9578b549e..f2eac01891d9 100644 --- a/spring-boot-project/spring-boot-docs/build.gradle +++ b/spring-boot-project/spring-boot-docs/build.gradle @@ -89,7 +89,7 @@ dependencies { implementation(project(path: ":spring-boot-project:spring-boot-devtools")) implementation("ch.qos.logback:logback-classic") implementation("com.zaxxer:HikariCP") - implementation("io.micrometer:micrometer-core") + implementation("io.micrometer:micrometer-jakarta9") implementation("io.micrometer:micrometer-tracing") implementation("io.micrometer:micrometer-registry-graphite") implementation("io.micrometer:micrometer-registry-jmx") diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/howto/httpclients/webclientreactornettycustomization/MyReactorNettyClientConfiguration.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/howto/httpclients/webclientreactornettycustomization/MyReactorNettyClientConfiguration.kt index 85c140e3f078..1dac3ab5de0e 100644 --- a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/howto/httpclients/webclientreactornettycustomization/MyReactorNettyClientConfiguration.kt +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/howto/httpclients/webclientreactornettycustomization/MyReactorNettyClientConfiguration.kt @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -22,7 +22,7 @@ import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.http.client.reactive.ClientHttpConnector import org.springframework.http.client.reactive.ReactorClientHttpConnector -import org.springframework.http.client.reactive.ReactorResourceFactory +import org.springframework.http.client.ReactorResourceFactory import reactor.netty.http.client.HttpClient @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-actuator/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-actuator/build.gradle index 3e2a471593a3..0ff356b4e0d9 100644 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-actuator/build.gradle +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-actuator/build.gradle @@ -8,5 +8,5 @@ dependencies { api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) api(project(":spring-boot-project:spring-boot-actuator-autoconfigure")) api("io.micrometer:micrometer-observation") - api("io.micrometer:micrometer-core") + api("io.micrometer:micrometer-jakarta9") } From b3ddec779301dcb48a97ca5a5f8703cca8c72876 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 10 Oct 2023 15:57:32 -0700 Subject: [PATCH 0587/1215] Refactor Jetty SameSiteSupplier cookie support to use a Handler Update `JettyServletWebServerFactory` so that the `SimeSiteSupplier` support is handled using a `Handler` rather than a `HttpStream.Wrapper`. Closes gh-37809 --- .../jetty/JettyServletWebServerFactory.java | 97 +++++++++++-------- 1 file changed, 56 insertions(+), 41 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java index 680090e52ae2..a50ddc044802 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java @@ -33,7 +33,7 @@ import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; -import java.util.ListIterator; +import java.util.Objects; import java.util.Set; import java.util.Spliterator; import java.util.UUID; @@ -52,8 +52,10 @@ import org.eclipse.jetty.ee10.webapp.Configuration; import org.eclipse.jetty.ee10.webapp.WebAppContext; import org.eclipse.jetty.ee10.webapp.WebInfConfiguration; +import org.eclipse.jetty.http.CookieCompliance; import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpField; +import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpFields.Mutable; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.MimeTypes; @@ -68,7 +70,6 @@ import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.HttpCookieUtils; -import org.eclipse.jetty.server.HttpStream; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; @@ -775,6 +776,8 @@ private Class loadClass(WebAppContext context, String c */ private static class SuppliedSameSiteCookieHandlerWrapper extends Handler.Wrapper { + private static final SetCookieParser setCookieParser = SetCookieParser.newInstance(); + private final List suppliers; SuppliedSameSiteCookieHandlerWrapper(List suppliers) { @@ -783,62 +786,74 @@ private static class SuppliedSameSiteCookieHandlerWrapper extends Handler.Wrappe @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { - request.addHttpStreamWrapper((stream) -> new SameSiteCookieHttpStreamWrapper(stream, request)); - return super.handle(request, response, callback); + SuppliedSameSiteCookieResponse wrappedResponse = new SuppliedSameSiteCookieResponse(request, response); + return super.handle(request, wrappedResponse, callback); } - private final class SameSiteCookieHttpStreamWrapper extends HttpStream.Wrapper { + private class SuppliedSameSiteCookieResponse extends Response.Wrapper { - private static final SetCookieParser setCookieParser = SetCookieParser.newInstance(); + private HttpFields.Mutable wrappedHeaders; - private final Request request; + SuppliedSameSiteCookieResponse(Request request, Response wrapped) { + super(request, wrapped); + this.wrappedHeaders = new SuppliedSameSiteCookieHeaders( + request.getConnectionMetaData().getHttpConfiguration().getResponseCookieCompliance(), + wrapped.getHeaders()); + } - private SameSiteCookieHttpStreamWrapper(HttpStream wrapped, Request request) { - super(wrapped); - this.request = request; + @Override + public Mutable getHeaders() { + return this.wrappedHeaders; + } + + } + + private class SuppliedSameSiteCookieHeaders extends HttpFields.Mutable.Wrapper { + + private final CookieCompliance compliance; + + SuppliedSameSiteCookieHeaders(CookieCompliance compliance, HttpFields.Mutable fields) { + super(fields); + this.compliance = compliance; } @Override - public void prepareResponse(Mutable headers) { - super.prepareResponse(headers); - ListIterator headerFields = headers.listIterator(); - while (headerFields.hasNext()) { - HttpField updatedField = applySameSiteIfNecessary(headerFields.next()); - if (updatedField != null) { - headerFields.set(updatedField); - } - } + public HttpField onAddField(HttpField field) { + return (field.getHeader() != HttpHeader.SET_COOKIE) ? field : onAddSetCookieField(field); } - private HttpField applySameSiteIfNecessary(HttpField headerField) { - if (headerField.getHeader() != HttpHeader.SET_COOKIE) { - return null; - } - HttpCookie cookie = setCookieParser.parse(headerField.getValue()); - if (cookie == null) { - return null; - } - SameSite sameSite = getSameSite(cookie); + private HttpField onAddSetCookieField(HttpField field) { + HttpCookie cookie = setCookieParser.parse(field.getValue()); + SameSite sameSite = (cookie != null) ? getSameSite(cookie) : null; if (sameSite == null) { - return null; + return field; } - return new HttpCookieUtils.SetCookieHttpField( - HttpCookie.build(cookie) - .sameSite(org.eclipse.jetty.http.HttpCookie.SameSite.from(sameSite.name())) - .build(), - this.request.getConnectionMetaData().getHttpConfiguration().getResponseCookieCompliance()); + HttpCookie updatedCookie = buildCookieWithUpdatedSameSite(cookie, sameSite); + return new HttpCookieUtils.SetCookieHttpField(updatedCookie, this.compliance); + } + + private HttpCookie buildCookieWithUpdatedSameSite(HttpCookie cookie, SameSite sameSite) { + return HttpCookie.build(cookie) + .sameSite(org.eclipse.jetty.http.HttpCookie.SameSite.from(sameSite.name())) + .build(); } private SameSite getSameSite(HttpCookie cookie) { + return getSameSite(asServletCookie(cookie)); + } + + private SameSite getSameSite(Cookie cookie) { + return SuppliedSameSiteCookieHandlerWrapper.this.suppliers.stream() + .map((supplier) -> supplier.getSameSite(cookie)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + private Cookie asServletCookie(HttpCookie cookie) { Cookie servletCookie = new Cookie(cookie.getName(), cookie.getValue()); cookie.getAttributes().forEach(servletCookie::setAttribute); - for (CookieSameSiteSupplier supplier : SuppliedSameSiteCookieHandlerWrapper.this.suppliers) { - SameSite sameSite = supplier.getSameSite(servletCookie); - if (sameSite != null) { - return sameSite; - } - } - return null; + return servletCookie; } } From 4ab104f5afee57179b6fcfa45355653adebb0aa1 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 10 Oct 2023 16:27:53 -0700 Subject: [PATCH 0588/1215] Use type safe JdbcClient query Update `JdbcClientAutoConfigurationTests` to use a type safe query since Spring Framework now returns `Object` types when no type is specified (see Spring Framework issue 31403). See gh-37710 --- .../autoconfigure/jdbc/JdbcClientAutoConfigurationTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfigurationTests.java index 03222cfbeb86..6c4035dc79e8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/JdbcClientAutoConfigurationTests.java @@ -94,7 +94,7 @@ static class JdbcClientDataSourceMigrationValidator { private final Long count; JdbcClientDataSourceMigrationValidator(JdbcClient jdbcClient) { - this.count = jdbcClient.sql("SELECT COUNT(*) from CITY").query().singleValue(); + this.count = jdbcClient.sql("SELECT COUNT(*) from CITY").query(Long.class).single(); } } From 1bd14652d8b69634aa19422e617878b8282b1752 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 11 Oct 2023 09:12:26 +0200 Subject: [PATCH 0589/1215] Apply Wavefront token type to auto-configured WavefrontSender Closes gh-37165 --- .../WavefrontPropertiesConfigAdapter.java | 12 +--- .../wavefront/WavefrontProperties.java | 21 ++++++- .../WavefrontSenderConfiguration.java | 3 +- .../wavefront/WavefrontPropertiesTests.java | 57 +++++++++++++++---- .../WavefrontSenderConfigurationTests.java | 49 ++++++++++++++++ 5 files changed, 119 insertions(+), 23 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapter.java index 1f64342b161b..bd898ddad050 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapter.java @@ -22,7 +22,6 @@ import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.PushRegistryPropertiesConfigAdapter; import org.springframework.boot.actuate.autoconfigure.wavefront.WavefrontProperties; import org.springframework.boot.actuate.autoconfigure.wavefront.WavefrontProperties.Metrics.Export; -import org.springframework.boot.actuate.autoconfigure.wavefront.WavefrontProperties.TokenType; /** * Adapter to convert {@link WavefrontProperties} to a {@link WavefrontConfig}. @@ -88,16 +87,7 @@ public boolean reportDayDistribution() { @Override public Type apiTokenType() { - TokenType apiTokenType = this.properties.getApiTokenType(); - if (apiTokenType == null) { - return WavefrontConfig.super.apiTokenType(); - } - return switch (apiTokenType) { - case NO_TOKEN -> Type.NO_TOKEN; - case WAVEFRONT_API_TOKEN -> Type.WAVEFRONT_API_TOKEN; - case CSP_API_TOKEN -> Type.CSP_API_TOKEN; - case CSP_CLIENT_CREDENTIALS -> Type.CSP_CLIENT_CREDENTIALS; - }; + return this.properties.getWavefrontApiTokenType(); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontProperties.java index 77fbd189a69a..ce313c3360d1 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontProperties.java @@ -25,6 +25,8 @@ import java.util.Map; import java.util.Set; +import com.wavefront.sdk.common.clients.service.token.TokenService.Type; + import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.PushRegistryProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; @@ -137,7 +139,7 @@ public URI getEffectiveUri() { * @return the API token */ public String getApiTokenOrThrow() { - if (this.apiToken == null && !usesProxy()) { + if (this.apiTokenType != TokenType.NO_TOKEN && this.apiToken == null && !usesProxy()) { throw new InvalidConfigurationPropertyValueException("management.wavefront.api-token", null, "This property is mandatory whenever publishing directly to the Wavefront API"); } @@ -180,6 +182,23 @@ public void setApiTokenType(TokenType apiTokenType) { this.apiTokenType = apiTokenType; } + /** + * Returns the {@link Type Wavefront token type}. + * @return the Wavefront token type + * @since 3.2.0 + */ + public Type getWavefrontApiTokenType() { + if (this.apiTokenType == null) { + return usesProxy() ? Type.NO_TOKEN : Type.WAVEFRONT_API_TOKEN; + } + return switch (this.apiTokenType) { + case NO_TOKEN -> Type.NO_TOKEN; + case WAVEFRONT_API_TOKEN -> Type.WAVEFRONT_API_TOKEN; + case CSP_API_TOKEN -> Type.CSP_API_TOKEN; + case CSP_CLIENT_CREDENTIALS -> Type.CSP_CLIENT_CREDENTIALS; + }; + } + public static class Application { /** diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontSenderConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontSenderConfiguration.java index 438782722118..f7ffa4d06929 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontSenderConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontSenderConfiguration.java @@ -52,7 +52,8 @@ public class WavefrontSenderConfiguration { @ConditionalOnMissingBean @Conditional(WavefrontTracingOrMetricsCondition.class) public WavefrontSender wavefrontSender(WavefrontProperties properties) { - Builder builder = new Builder(properties.getEffectiveUri().toString(), properties.getApiTokenOrThrow()); + Builder builder = new Builder(properties.getEffectiveUri().toString(), properties.getWavefrontApiTokenType(), + properties.getApiTokenOrThrow()); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); WavefrontProperties.Sender sender = properties.getSender(); map.from(sender.getMaxQueueSize()).to(builder::maxQueueSize); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontPropertiesTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontPropertiesTests.java index 02d1ce3b77cc..8eacdc9e29eb 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontPropertiesTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontPropertiesTests.java @@ -18,8 +18,12 @@ import java.net.URI; +import com.wavefront.sdk.common.clients.service.token.TokenService.Type; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.boot.actuate.autoconfigure.wavefront.WavefrontProperties.TokenType; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; import static org.assertj.core.api.Assertions.assertThat; @@ -34,21 +38,54 @@ class WavefrontPropertiesTests { @Test void apiTokenIsOptionalWhenUsingProxy() { - WavefrontProperties sut = new WavefrontProperties(); - sut.setUri(URI.create("proxy://localhost:2878")); - sut.setApiToken(null); - assertThat(sut.getApiTokenOrThrow()).isNull(); - assertThat(sut.getEffectiveUri()).isEqualTo(URI.create("http://localhost:2878")); + WavefrontProperties properties = new WavefrontProperties(); + properties.setUri(URI.create("proxy://localhost:2878")); + properties.setApiToken(null); + assertThat(properties.getApiTokenOrThrow()).isNull(); + assertThat(properties.getEffectiveUri()).isEqualTo(URI.create("http://localhost:2878")); } @Test void apiTokenIsMandatoryWhenNotUsingProxy() { - WavefrontProperties sut = new WavefrontProperties(); - sut.setUri(URI.create("http://localhost:2878")); - sut.setApiToken(null); - assertThat(sut.getEffectiveUri()).isEqualTo(URI.create("http://localhost:2878")); - assertThatThrownBy(sut::getApiTokenOrThrow).isInstanceOf(InvalidConfigurationPropertyValueException.class) + WavefrontProperties properties = new WavefrontProperties(); + properties.setUri(URI.create("http://localhost:2878")); + properties.setApiToken(null); + assertThat(properties.getEffectiveUri()).isEqualTo(URI.create("http://localhost:2878")); + assertThatThrownBy(properties::getApiTokenOrThrow) + .isInstanceOf(InvalidConfigurationPropertyValueException.class) .hasMessageContaining("management.wavefront.api-token"); } + @Test + void shouldNotFailIfTokenTypeIsSetToNoToken() { + WavefrontProperties properties = new WavefrontProperties(); + properties.setUri(URI.create("http://localhost:2878")); + properties.setApiTokenType(TokenType.NO_TOKEN); + properties.setApiToken(null); + assertThat(properties.getApiTokenOrThrow()).isNull(); + } + + @Test + void wavefrontApiTokenTypeWhenUsingProxy() { + WavefrontProperties properties = new WavefrontProperties(); + properties.setUri(URI.create("proxy://localhost:2878")); + assertThat(properties.getWavefrontApiTokenType()).isEqualTo(Type.NO_TOKEN); + } + + @Test + void wavefrontApiTokenTypeWhenNotUsingProxy() { + WavefrontProperties properties = new WavefrontProperties(); + properties.setUri(URI.create("http://localhost:2878")); + assertThat(properties.getWavefrontApiTokenType()).isEqualTo(Type.WAVEFRONT_API_TOKEN); + } + + @ParameterizedTest + @EnumSource(TokenType.class) + void wavefrontApiTokenMapping(TokenType from) { + WavefrontProperties properties = new WavefrontProperties(); + properties.setApiTokenType(from); + Type expected = Type.valueOf(from.name()); + assertThat(properties.getWavefrontApiTokenType()).isEqualTo(expected); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontSenderConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontSenderConfigurationTests.java index 367adebb5c59..6091baf221a7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontSenderConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontSenderConfigurationTests.java @@ -19,6 +19,9 @@ import java.util.concurrent.LinkedBlockingQueue; import com.wavefront.sdk.common.WavefrontSender; +import com.wavefront.sdk.common.clients.service.token.CSPTokenService; +import com.wavefront.sdk.common.clients.service.token.NoopProxyTokenService; +import com.wavefront.sdk.common.clients.service.token.WavefrontTokenService; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; @@ -27,6 +30,7 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.as; import static org.assertj.core.api.Assertions.assertThat; @@ -118,6 +122,51 @@ void allowsWavefrontSenderToBeCustomized() { .run((context) -> assertThat(context).hasSingleBean(WavefrontSender.class).hasBean("customSender")); } + @Test + void shouldApplyTokenTypeWavefrontApiToken() { + this.contextRunner + .withPropertyValues("management.wavefront.api-token-type=WAVEFRONT_API_TOKEN", + "management.wavefront.api-token=abcde") + .run((context) -> { + WavefrontSender sender = context.getBean(WavefrontSender.class); + Object tokenService = ReflectionTestUtils.getField(sender, "tokenService"); + assertThat(tokenService).isInstanceOf(WavefrontTokenService.class); + }); + } + + @Test + void shouldApplyTokenTypeCspApiToken() { + this.contextRunner + .withPropertyValues("management.wavefront.api-token-type=CSP_API_TOKEN", + "management.wavefront.api-token=abcde") + .run((context) -> { + WavefrontSender sender = context.getBean(WavefrontSender.class); + Object tokenService = ReflectionTestUtils.getField(sender, "tokenService"); + assertThat(tokenService).isInstanceOf(CSPTokenService.class); + }); + } + + @Test + void shouldApplyTokenTypeCspClientCredentials() { + this.contextRunner + .withPropertyValues("management.wavefront.api-token-type=CSP_CLIENT_CREDENTIALS", + "management.wavefront.api-token=clientid=cid,clientsecret=csec") + .run((context) -> { + WavefrontSender sender = context.getBean(WavefrontSender.class); + Object tokenService = ReflectionTestUtils.getField(sender, "tokenService"); + assertThat(tokenService).isInstanceOf(CSPTokenService.class); + }); + } + + @Test + void shouldApplyTokenTypeNoToken() { + this.contextRunner.withPropertyValues("management.wavefront.api-token-type=NO_TOKEN").run((context) -> { + WavefrontSender sender = context.getBean(WavefrontSender.class); + Object tokenService = ReflectionTestUtils.getField(sender, "tokenService"); + assertThat(tokenService).isInstanceOf(NoopProxyTokenService.class); + }); + } + @Configuration(proxyBeanMethods = false) static class CustomSenderConfiguration { From 882e29dc7fd870d0990b31cb02d591329a5efb86 Mon Sep 17 00:00:00 2001 From: saravanakumar Ramasamy Date: Sat, 7 Oct 2023 19:28:20 +0530 Subject: [PATCH 0590/1215] Remove @ConditionalOnMissingBean from RestTemplateBuilderConfigurer See gh-37746 --- .../client/RestTemplateAutoConfiguration.java | 1 - .../RestTemplateAutoConfigurationTests.java | 21 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java index ce89524c277c..70bc8c50c6c7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfiguration.java @@ -45,7 +45,6 @@ public class RestTemplateAutoConfiguration { @Bean @Lazy - @ConditionalOnMissingBean public RestTemplateBuilderConfigurer restTemplateBuilderConfigurer( ObjectProvider messageConverters, ObjectProvider restTemplateCustomizers, diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java index a80f93a73720..5365bd047d59 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.support.BeanDefinitionOverrideException; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; @@ -42,6 +43,7 @@ import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; @@ -65,6 +67,15 @@ void restTemplateBuilderConfigurerShouldBeLazilyDefined() { .isTrue()); } + @Test + void assertExceptionWhenCustomRestTemplateConfigurerIsDefined() { + this.contextRunner + .withUserConfiguration(RestTemplateCustomConfigurerConfig.class) + .run((context) -> assertThrows( BeanDefinitionOverrideException.class, () ->{ + context.getBeanFactory().getBeanDefinition("restTemplateBuilderConfigurer"); + })); + } + @Test void restTemplateBuilderShouldBeLazilyDefined() { this.contextRunner @@ -263,6 +274,16 @@ RestTemplateRequestCustomizer restTemplateRequestCustomizer() { } + @Configuration(proxyBeanMethods = false) + static class RestTemplateCustomConfigurerConfig { + + @Bean + RestTemplateBuilderConfigurer restTemplateBuilderConfigurer() { + return new RestTemplateBuilderConfigurer(); + } + + } + static class CustomHttpMessageConverter extends StringHttpMessageConverter { } From 6874a2fb9a01ff1f70b562138f1c8325f4e70d83 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 11 Oct 2023 14:03:12 +0200 Subject: [PATCH 0591/1215] Polish "Remove @ConditionalOnMissingBean from RestTemplateBuilderConfigurer" See gh-37746 --- .../client/RestTemplateAutoConfigurationTests.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java index 5365bd047d59..8e576929f2ce 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java @@ -43,7 +43,6 @@ import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; @@ -68,12 +67,11 @@ void restTemplateBuilderConfigurerShouldBeLazilyDefined() { } @Test - void assertExceptionWhenCustomRestTemplateConfigurerIsDefined() { - this.contextRunner - .withUserConfiguration(RestTemplateCustomConfigurerConfig.class) - .run((context) -> assertThrows( BeanDefinitionOverrideException.class, () ->{ - context.getBeanFactory().getBeanDefinition("restTemplateBuilderConfigurer"); - })); + void shouldFailOnCustomRestTemplateBuilderConfigurer() { + this.contextRunner.withUserConfiguration(RestTemplateCustomConfigurerConfig.class) + .run((context) -> assertThat(context).getFailure() + .isInstanceOf(BeanDefinitionOverrideException.class) + .hasMessageContaining("with name 'restTemplateBuilderConfigurer'")); } @Test From 5556739c8caf63d7a3b9e9c8ea9016675774ff68 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Wed, 11 Oct 2023 15:39:51 -0500 Subject: [PATCH 0592/1215] Add SSL bundle support to Rabbit auto-configuration --- .../amqp/RabbitAutoConfiguration.java | 9 ++- ...RabbitConnectionFactoryBeanConfigurer.java | 49 ++++++++++--- .../autoconfigure/amqp/RabbitProperties.java | 16 ++++- .../SslBundleRabbitConnectionFactoryBean.java | 57 +++++++++++++++ .../amqp/RabbitAutoConfigurationTests.java | 27 +++++++- .../amqp/RabbitPropertiesTests.java | 7 ++ .../SampleAmqpSimpleApplicationSslTests.java | 69 +++++++++++++++++++ .../amqp/SecureRabbitMqContainer.java | 49 +++++++++++++ .../src/test/resources/ssl/rabbitmq.conf | 7 ++ .../src/test/resources/ssl/test-ca.crt | 32 +++++++++ .../src/test/resources/ssl/test-ca.key | 51 ++++++++++++++ .../src/test/resources/ssl/test-client.crt | 24 +++++++ .../src/test/resources/ssl/test-client.key | 27 ++++++++ .../src/test/resources/ssl/test-server.crt | 24 +++++++ .../src/test/resources/ssl/test-server.key | 27 ++++++++ 15 files changed, 460 insertions(+), 15 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/SslBundleRabbitConnectionFactoryBean.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/java/smoketest/amqp/SecureRabbitMqContainer.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/rabbitmq.conf create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-ca.crt create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-ca.key create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-client.crt create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-client.key create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-server.crt create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-server.key diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfiguration.java index bd9fb0daa0ea..e9b57d03a028 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfiguration.java @@ -38,6 +38,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -69,6 +70,7 @@ * @author Chris Bono * @author Moritz Halbritter * @author Andy Wilkinson + * @author Scott Frederick * @since 1.0.0 */ @AutoConfiguration @@ -97,9 +99,10 @@ RabbitConnectionDetails rabbitConnectionDetails() { @ConditionalOnMissingBean RabbitConnectionFactoryBeanConfigurer rabbitConnectionFactoryBeanConfigurer(ResourceLoader resourceLoader, RabbitConnectionDetails connectionDetails, ObjectProvider credentialsProvider, - ObjectProvider credentialsRefreshService) { + ObjectProvider credentialsRefreshService, + ObjectProvider sslBundles) { RabbitConnectionFactoryBeanConfigurer configurer = new RabbitConnectionFactoryBeanConfigurer(resourceLoader, - this.properties, connectionDetails); + this.properties, connectionDetails, sslBundles.getIfAvailable()); configurer.setCredentialsProvider(credentialsProvider.getIfUnique()); configurer.setCredentialsRefreshService(credentialsRefreshService.getIfUnique()); return configurer; @@ -122,7 +125,7 @@ CachingConnectionFactory rabbitConnectionFactory( CachingConnectionFactoryConfigurer rabbitCachingConnectionFactoryConfigurer, ObjectProvider connectionFactoryCustomizers) throws Exception { - RabbitConnectionFactoryBean connectionFactoryBean = new RabbitConnectionFactoryBean(); + RabbitConnectionFactoryBean connectionFactoryBean = new SslBundleRabbitConnectionFactoryBean(); rabbitConnectionFactoryBeanConfigurer.configure(connectionFactoryBean); connectionFactoryBean.afterPropertiesSet(); com.rabbitmq.client.ConnectionFactory connectionFactory = connectionFactoryBean.getObject(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java index 607ac96462a4..2f59e2d8faed 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java @@ -24,6 +24,8 @@ import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean; import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails.Address; import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; import org.springframework.util.unit.DataSize; @@ -35,6 +37,7 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick * @since 2.6.0 */ public class RabbitConnectionFactoryBeanConfigurer { @@ -45,6 +48,8 @@ public class RabbitConnectionFactoryBeanConfigurer { private final RabbitConnectionDetails connectionDetails; + private final SslBundles sslBundles; + private CredentialsProvider credentialsProvider; private CredentialsRefreshService credentialsRefreshService; @@ -65,17 +70,33 @@ public RabbitConnectionFactoryBeanConfigurer(ResourceLoader resourceLoader, Rabb * priority over the properties. * @param resourceLoader the resource loader * @param properties the properties - * @param connectionDetails the connection details. + * @param connectionDetails the connection details * @since 3.1.0 */ public RabbitConnectionFactoryBeanConfigurer(ResourceLoader resourceLoader, RabbitProperties properties, RabbitConnectionDetails connectionDetails) { + this(resourceLoader, properties, connectionDetails, null); + } + + /** + * Creates a new configurer that will use the given {@code resourceLoader}, + * {@code properties}, {@code connectionDetails}, and {@code sslBundles}. The + * connection details have priority over the properties. + * @param resourceLoader the resource loader + * @param properties the properties + * @param connectionDetails the connection details + * @param sslBundles the SSL bundles + * @since 3.2.0 + */ + public RabbitConnectionFactoryBeanConfigurer(ResourceLoader resourceLoader, RabbitProperties properties, + RabbitConnectionDetails connectionDetails, SslBundles sslBundles) { Assert.notNull(resourceLoader, "ResourceLoader must not be null"); Assert.notNull(properties, "Properties must not be null"); Assert.notNull(connectionDetails, "ConnectionDetails must not be null"); this.resourceLoader = resourceLoader; this.rabbitProperties = properties; this.connectionDetails = connectionDetails; + this.sslBundles = sslBundles; } public void setCredentialsProvider(CredentialsProvider credentialsProvider) { @@ -111,15 +132,23 @@ public void configure(RabbitConnectionFactoryBean factory) { RabbitProperties.Ssl ssl = this.rabbitProperties.getSsl(); if (ssl.determineEnabled()) { factory.setUseSSL(true); - map.from(ssl::getAlgorithm).whenNonNull().to(factory::setSslAlgorithm); - map.from(ssl::getKeyStoreType).to(factory::setKeyStoreType); - map.from(ssl::getKeyStore).to(factory::setKeyStore); - map.from(ssl::getKeyStorePassword).to(factory::setKeyStorePassphrase); - map.from(ssl::getKeyStoreAlgorithm).whenNonNull().to(factory::setKeyStoreAlgorithm); - map.from(ssl::getTrustStoreType).to(factory::setTrustStoreType); - map.from(ssl::getTrustStore).to(factory::setTrustStore); - map.from(ssl::getTrustStorePassword).to(factory::setTrustStorePassphrase); - map.from(ssl::getTrustStoreAlgorithm).whenNonNull().to(factory::setTrustStoreAlgorithm); + if (ssl.getBundle() != null) { + SslBundle bundle = this.sslBundles.getBundle(ssl.getBundle()); + if (factory instanceof SslBundleRabbitConnectionFactoryBean sslFactory) { + sslFactory.setSslBundle(bundle); + } + } + else { + map.from(ssl::getAlgorithm).whenNonNull().to(factory::setSslAlgorithm); + map.from(ssl::getKeyStoreType).to(factory::setKeyStoreType); + map.from(ssl::getKeyStore).to(factory::setKeyStore); + map.from(ssl::getKeyStorePassword).to(factory::setKeyStorePassphrase); + map.from(ssl::getKeyStoreAlgorithm).whenNonNull().to(factory::setKeyStoreAlgorithm); + map.from(ssl::getTrustStoreType).to(factory::setTrustStoreType); + map.from(ssl::getTrustStore).to(factory::setTrustStore); + map.from(ssl::getTrustStorePassword).to(factory::setTrustStorePassphrase); + map.from(ssl::getTrustStoreAlgorithm).whenNonNull().to(factory::setTrustStoreAlgorithm); + } map.from(ssl::isValidateServerCertificate) .to((validate) -> factory.setSkipServerCertificateValidation(!validate)); map.from(ssl::getVerifyHostname).to(factory::setEnableHostnameVerification); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java index dd9aae0800f8..4279fb30df0f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java @@ -46,6 +46,7 @@ * @author Franjo Zilic * @author Eddú Meléndez * @author Rafael Carvalho + * @author Scott Frederick * @since 1.0.0 */ @ConfigurationProperties(prefix = "spring.rabbitmq") @@ -400,6 +401,11 @@ public class Ssl { */ private Boolean enabled; + /** + * SSL bundle name. + */ + private String bundle; + /** * Path to the key store that holds the SSL certificate. */ @@ -467,7 +473,7 @@ public Boolean getEnabled() { * @see #getEnabled() () */ public boolean determineEnabled() { - boolean defaultEnabled = Optional.ofNullable(getEnabled()).orElse(false); + boolean defaultEnabled = Optional.ofNullable(getEnabled()).orElse(false) || this.bundle != null; if (CollectionUtils.isEmpty(RabbitProperties.this.parsedAddresses)) { return defaultEnabled; } @@ -479,6 +485,14 @@ public void setEnabled(Boolean enabled) { this.enabled = enabled; } + public String getBundle() { + return this.bundle; + } + + public void setBundle(String bundle) { + this.bundle = bundle; + } + public String getKeyStore() { return this.keyStore; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/SslBundleRabbitConnectionFactoryBean.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/SslBundleRabbitConnectionFactoryBean.java new file mode 100644 index 000000000000..526a187dd428 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/SslBundleRabbitConnectionFactoryBean.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2023 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.amqp; + +import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean; +import org.springframework.boot.ssl.SslBundle; + +/** + * A {@link RabbitConnectionFactoryBean} that can be configured with custom SSL trust + * material from an {@link SslBundle}. + * + * @author Scott Frederick + */ +class SslBundleRabbitConnectionFactoryBean extends RabbitConnectionFactoryBean { + + private SslBundle sslBundle; + + private boolean enableHostnameVerification; + + @Override + protected void setUpSSL() { + if (this.sslBundle != null) { + this.connectionFactory.useSslProtocol(this.sslBundle.createSslContext()); + if (this.enableHostnameVerification) { + this.connectionFactory.enableHostnameVerification(); + } + } + else { + super.setUpSSL(); + } + } + + void setSslBundle(SslBundle sslBundle) { + this.sslBundle = sslBundle; + } + + @Override + public void setEnableHostnameVerification(boolean enable) { + this.enableHostnameVerification = enable; + super.setEnableHostnameVerification(enable); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java index c6c32c7daa9c..e97613b99b89 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java @@ -60,6 +60,7 @@ import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.system.CapturedOutput; @@ -102,12 +103,13 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ @ExtendWith(OutputCaptureExtension.class) class RabbitAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(RabbitAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(RabbitAutoConfiguration.class, SslAutoConfiguration.class)); @Test void testDefaultRabbitConfiguration() { @@ -777,6 +779,16 @@ void enableSsl() { }); } + @Test + void enableSslWithInvalidSslBundleFails() { + this.contextRunner.withUserConfiguration(TestConfiguration.class) + .withPropertyValues("spring.rabbitmq.ssl.bundle=invalid") + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure().hasMessageContaining("SSL bundle name 'invalid' cannot be found"); + }); + } + @Test // Make sure that we at least attempt to load the store void enableSslWithNonExistingKeystoreShouldFail() { @@ -827,6 +839,19 @@ void enableSslWithInvalidTrustStoreTypeShouldFail() { }); } + @Test + void enableSslWithBundle() { + this.contextRunner.withUserConfiguration(TestConfiguration.class) + .withPropertyValues("spring.rabbitmq.ssl.bundle=test-bundle", + "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:test.jks", + "spring.ssl.bundle.jks.test-bundle.keystore.password=secret", + "spring.ssl.bundle.jks.test-bundle.key.password=password") + .run((context) -> { + com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory = getTargetConnectionFactory(context); + assertThat(rabbitConnectionFactory.isSSL()).isTrue(); + }); + } + @Test void enableSslWithKeystoreTypeAndTrustStoreTypeShouldWork() { this.contextRunner.withUserConfiguration(TestConfiguration.class) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitPropertiesTests.java index 2a858e530c5b..502366fdbf4f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitPropertiesTests.java @@ -35,6 +35,7 @@ * @author Andy Wilkinson * @author Stephane Nicoll * @author Rafael Carvalho + * @author Scott Frederick */ class RabbitPropertiesTests { @@ -321,6 +322,12 @@ void determineSslReturnFlagPropertyWhenNoAddresses() { assertThat(this.properties.getSsl().determineEnabled()).isTrue(); } + @Test + void determineSslEnabledIsTrueWhenBundleIsSetAndNoAddresses() { + this.properties.getSsl().setBundle("test"); + assertThat(this.properties.getSsl().determineEnabled()).isTrue(); + } + @Test void propertiesUseConsistentDefaultValues() { ConnectionFactory connectionFactory = new ConnectionFactory(); diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java new file mode 100644 index 000000000000..0ccd87eee4b8 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2023 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 smoketest.amqp; + +import java.time.Duration; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Smoke tests for RabbitMQ with SSL using an SSL bundle for SSL configuration. + * + * @author Scott Frederick + */ +@SpringBootTest(properties = { "spring.rabbitmq.ssl.bundle=client", + "spring.ssl.bundle.pem.client.keystore.certificate=classpath:ssl/test-client.crt", + "spring.ssl.bundle.pem.client.keystore.private-key=classpath:ssl/test-client.key", + "spring.ssl.bundle.pem.client.truststore.certificate=classpath:ssl/test-ca.crt" }) +@Testcontainers(disabledWithoutDocker = true) +@ExtendWith(OutputCaptureExtension.class) +class SampleAmqpSimpleApplicationSslTests { + + @Container + static final SecureRabbitMqContainer rabbit = new SecureRabbitMqContainer(); + + @DynamicPropertySource + static void secureRabbitMqProperties(DynamicPropertyRegistry registry) { + registry.add("spring.rabbitmq.host", rabbit::getHost); + registry.add("spring.rabbitmq.port", rabbit::getAmqpsPort); + registry.add("spring.rabbitmq.username", rabbit::getAdminUsername); + registry.add("spring.rabbitmq.password", rabbit::getAdminPassword); + } + + @Autowired + private Sender sender; + + @Test + void sendSimpleMessage(CapturedOutput output) { + this.sender.send("Test message"); + Awaitility.waitAtMost(Duration.ofMinutes(1)).untilAsserted(() -> assertThat(output).contains("Test message")); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/java/smoketest/amqp/SecureRabbitMqContainer.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/java/smoketest/amqp/SecureRabbitMqContainer.java new file mode 100644 index 000000000000..1a8c8203cd06 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/java/smoketest/amqp/SecureRabbitMqContainer.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2023 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 smoketest.amqp; + +import java.time.Duration; + +import org.testcontainers.containers.RabbitMQContainer; +import org.testcontainers.utility.MountableFile; + +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; + +/** + * A {@link RabbitMQContainer} for RabbitMQ with SSL configuration. + * + * @author Scott Frederick + */ +class SecureRabbitMqContainer extends RabbitMQContainer { + + SecureRabbitMqContainer() { + super(DockerImageNames.rabbit()); + withStartupTimeout(Duration.ofMinutes(4)); + } + + @Override + public void configure() { + withCopyFileToContainer(MountableFile.forClasspathResource("/ssl/rabbitmq.conf"), + "/etc/rabbitmq/rabbitmq.conf"); + withCopyFileToContainer(MountableFile.forClasspathResource("/ssl/test-server.crt"), + "/etc/rabbitmq/server_cert.pem"); + withCopyFileToContainer(MountableFile.forClasspathResource("/ssl/test-server.key"), + "/etc/rabbitmq/server_key.pem"); + withCopyFileToContainer(MountableFile.forClasspathResource("/ssl/test-ca.crt"), "/etc/rabbitmq/ca_cert.pem"); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/rabbitmq.conf b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/rabbitmq.conf new file mode 100644 index 000000000000..3bcc1648bfc2 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/rabbitmq.conf @@ -0,0 +1,7 @@ +listeners.tcp = none +listeners.ssl.default = 5671 +ssl_options.certfile = /etc/rabbitmq/server_cert.pem +ssl_options.keyfile = /etc/rabbitmq/server_key.pem +ssl_options.cacertfile = /etc/rabbitmq/ca_cert.pem +ssl_options.verify = verify_peer +ssl_options.fail_if_no_peer_cert = true \ No newline at end of file diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-ca.crt b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-ca.crt new file mode 100644 index 000000000000..c528ec820c91 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-ca.crt @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFhjCCA26gAwIBAgIUERZP46qinK0dKmJzlCsoD/k1nWYwDQYJKoZIhvcNAQEL +BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MB4XDTIzMDUwMTIwNDkxMFoXDTMzMDQyODIwNDkxMFow +OzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlmaWNh +dGUgQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApWYo +UQjDY98oVOO5HOjheWeBN+C6gozg4aPY0VdRDTKmZ5SzNjuYtX6jsd8e5UF+ceeL +Aw9E3FAKG80F/81c6mtFhFUNUaBCbK2/+igs+Ae6r42i6iLImvgYLbZ0rGpPwszT +KGlwyobsI8n1bRFrVRdtGWVfn3Dfc5k/+dnZ03kOpViv/gd/xNWMcMOlj64F1s8L +6Nx9bfeJvOcsX+5qMiy/B6dZS0lkvXZISJbFhvX/+5Tb/vkP41AnrYff8hO8OBs+ +G2srr2xNAIcgNBSjedDVUaRO+a2WHdX/1fHOlNqz335XMo79FOqRWDCZET3YW36A +hqiSPPiDq8AA7hmVxnq7vxWo/qclaqVuk5Dxp+ZD7d8deSGehTPajeCZCDtNhw6C +jtlU8v/LdwMRhqZp5/fjDlOEkutFh6B/aMjq3ZPYQad4MtQixDifgEs4iwnIMoVS +Wqpn24qn0qddfP0Y00U1F79UuJ2cJpyqdjtMRvbdNv6udWhD0rtrjdLvGFDOryzD +W7xQD2NLWW0IC9YNuXR0FzrJFFqWBW+lfF1u1PdW7ITFtUhj8RcIZZgUS/w1Yh8/ +d6ja18UROEgiJ/Isgvl8sNTe2oNQK9HM6XtyEif5G5J7cv5FAH3si98My5h+rKq9 +AMGfQLtDOM+Ivg7D63iiuxB57Rq91xCsKCC2QNECAwEAAaOBgTB/MB0GA1UdDgQW +BBQuNq1dmybivJy6XnHIFBYqEfqtMDAfBgNVHSMEGDAWgBQuNq1dmybivJy6XnHI +FBYqEfqtMDAPBgNVHRMBAf8EBTADAQH/MCwGA1UdEQQlMCOCC2V4YW1wbGUuY29t +gglsb2NhbGhvc3SCCTEyNy4wLjAuMTANBgkqhkiG9w0BAQsFAAOCAgEAJFpeqQB9 +pJLn4idrp7M1lSCrBJu2tu2UsOVcKTGZ3uqgKgKa+bn0kw9yN1CMISLSOV9B1Zt5 +/+2U24/9iC1nGIzEpk2uQ2GwGaxFsy38gP6FF+UFltEvLbhfHWZ/j8gWQhTRQ/sT +TMd0L0CysmDswoEHcuNgdX+V4WVchPqdHTxp5qLM3GRas5JCuNcVi+vFEWCQsYRh +iTpsCEVfRsVJKUvPKVLR8PSEjSt8S+SQjIuTVWSmdG358uRVxpBzAzMwz9sQw4G6 +Rv3S4LaQpWXUyHVYM1OxQz0fhEug5qgSR75GTFwG1oVd5rdk7iK/J3WbRJZ9FcKx +ipZ3jdl5mmI6p87OjgQVtUInv8KK88AhJmypBXaHE64nn8+YUsh/ud6+Vr8vyMPK +TZJivCtVKoX+nd3Zb3qX2YGORKQmn4GPX551FCk1CFOa+qlGfXtfqV2Z9LEQmqx3 +ygqVnmSf34oTz04sSMdK7m3ULqLyv3RFJJ4F+VsHHAEdJYO+v/GdGz/0FA7ZZ4t+ +7r1qY7uK4NSMRBn+DGlUL9oVp26uss/Qvi1WTI0g9W1YImxYSlaR0tm9jZQckirm +KMLMDyGJFvHqR8LRa3DU6L5pU99LxZSHRxBAY6oexKSYWt7BSE1kwaL3Exjg/RG/ +ap5/GNJS1STNnbgq5TtWUbvZcXuhuBe8ClI= +-----END CERTIFICATE----- diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-ca.key b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-ca.key new file mode 100644 index 000000000000..54a007ea2120 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-ca.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKAIBAAKCAgEApWYoUQjDY98oVOO5HOjheWeBN+C6gozg4aPY0VdRDTKmZ5Sz +NjuYtX6jsd8e5UF+ceeLAw9E3FAKG80F/81c6mtFhFUNUaBCbK2/+igs+Ae6r42i +6iLImvgYLbZ0rGpPwszTKGlwyobsI8n1bRFrVRdtGWVfn3Dfc5k/+dnZ03kOpViv +/gd/xNWMcMOlj64F1s8L6Nx9bfeJvOcsX+5qMiy/B6dZS0lkvXZISJbFhvX/+5Tb +/vkP41AnrYff8hO8OBs+G2srr2xNAIcgNBSjedDVUaRO+a2WHdX/1fHOlNqz335X +Mo79FOqRWDCZET3YW36AhqiSPPiDq8AA7hmVxnq7vxWo/qclaqVuk5Dxp+ZD7d8d +eSGehTPajeCZCDtNhw6CjtlU8v/LdwMRhqZp5/fjDlOEkutFh6B/aMjq3ZPYQad4 +MtQixDifgEs4iwnIMoVSWqpn24qn0qddfP0Y00U1F79UuJ2cJpyqdjtMRvbdNv6u +dWhD0rtrjdLvGFDOryzDW7xQD2NLWW0IC9YNuXR0FzrJFFqWBW+lfF1u1PdW7ITF +tUhj8RcIZZgUS/w1Yh8/d6ja18UROEgiJ/Isgvl8sNTe2oNQK9HM6XtyEif5G5J7 +cv5FAH3si98My5h+rKq9AMGfQLtDOM+Ivg7D63iiuxB57Rq91xCsKCC2QNECAwEA +AQKCAgEAn3AdtxeyeiiZEVO/ku2uxEARYRMB120ELp6qGAqKuCU2Ia1HICVM7M/Z +7lG9z5NV12kzKMzkPVfulqQJf2+wfMzRY2I1h5Tr0yWeZP+rcaDJxgbLn9XN+Qzl +CdPTHo0QvCCEAHW7448yPMGnEu9yvsDpS0zcY68Dx8RX1nq5LtCIXL1kUYVbFhwg +2GbQxvMi79IAkgVR59px7SYPMZ56wkk+EJuySQ/Dy5skzMyCNroWe6cgduYR+ba/ +uNi8+PcrPg6MzRN/Ngg5JiQb1/h5Kak0qRGxi59YkQRELTF+SSGVuQBp//O0ZSBE +4XVfaC5szK3iKWyAI8QP8VUR0HPbWr8dum6HQn/tpbQ1AcX9ObWnUz6TgaoHax0w +3VrnHnsr1kKmTHtqbB0uEeB7/vc6D3IWNIaPnoFT01snyGYDIaWcRLhPWCp/Z3QG +e1tCEVNqxzb5mtsFri1rVSXsOT8169il1V3qP8Wu9M0C/pXM+9XEdZd6ZecgU+SS +MEBAl+qYTBfGS7lJDIjqS0V6/NMNBa0bW2Gg35PruriPMgDhoXiYp3NgN0cuf4KQ +KEinRSwvb2iqfzCevY7D2JRJcTcZ97a518lDd4URIZ+W7o7+8UBObcuns55kBCy1 +NbjkZe2yGBGOODa1gXPaAgG1IBLDmnVPSKPyuHLiS0X+KmC4IAECggEBANCdYFW3 +Nw93w4Olh8tOJA4z9BTsQi64V+q/WOIz5l9aBHXVdyiG7gqFWiK7XsofPvXzU8XA +jP5y4XArO28Bwn3Ipa7YpoOs4J9KF8Il9dDUfUPTcNKkogEGnH8QHVPXUX28othW +NZ9urvP+rSYjM4CUQtGG/RiiGPHssHgQoPvgPm4mrmMgKSm3mKdm5xkIYITccGag +3tmO35cPzBBVap1tDmJ3F8dCMW8OsTKv6ECIjuMSYDbpmSNkxPxBK5YiIEJ8jjdU +5+7Bf3PLIoQNd+LWoSRzHm114QGFoTLq2wPE9TFoc9j+svZBAmDkCzTE9+KwIL+G +6dPcvvtT+NiTFgECggEBAMr32v6NgL8aGKK8nBiyibInUjKl0iCE1FcwGR6NOkK0 +3nJKhXiOWkBM3yeK/rq7HXfds6+pfi3w4VCmHXvF4IY5IIu8P4d0g/sMrFexwq2x +Qs400aomAVtlTQ46iL2vw5XOwMTw1SXvaNX/AgR0b9qiI1UfFZeox9UiHR+KdWPV +rKYDbHIHOk4Nxe950cK08KOReV3kO15RvBf6bdUAJwGWIdKLUr0y858s4H5GUZK8 +qKuC/toCE7Emy0k+q+NV/CApchhzQ5gwhVdc8qdhKlJtZDouopAOjOOq6l9C3GFT +qX7CVJppe7YbURni4Y7dXZzi2hn8wb7nSxmQq95FStECggEAY6/gefVMHVsYlY8D +HfagKh1PdLQVSCgU8vsu6SDt5ACrAvfXsgkQNPzWPqSUvjdCKdt125iQh4K0EZrH +EtufaeX4rl2e7GsvB08rnT3wgjMYDNI8Jpw/Qgg7vkggC5FnwpLiqkg/5YjJl5TK +ft/xW279owxDY4MKMojtJuKjWtkkXBSl3n5ezS2Lh+sXYZHsNXD1UUVsWD/6vj/x +Ppjikomrhwfr1+7cmnpF2LfQXw4iYYXFblggMpaTvwsRXfO+wKaueuha0G+sjNO0 +EbAx6ravWDCeiKX8uHJ3vlIWCG4U0OBeA4JqWFxmW5B9fmDlJ3EMpRk+IVxp8sWE +s1FOAQKCAQB4UlSloLcZEtxV5N/YmEaesUa+NaUKmBPVF/NcNDa8gsJ4GItlO2Zv +ReLoazK0+eXvQCOcWCswCuNXTxKdZGHE0CrmC5PRthXjhtDIL94L39CNs6wzZNJb +HwN+Et8rK/4TWfzXAzoogfOxILpOb8Q7ZPDzLjk7rdfBFrcTEp6ir3Ho/JCWTIiY +6vtTCvF5rpAVN1EugvVa5bNOt6vSoIN/IkQsr2E+Pe1EiHMRCJilF2gaPM7d6GtK +EohihF+bpkaPvmIf8ny4xNLXRoenCCfxs12+TBUctzN4Z8MG8/j3TYRmW8eRvkST +YUBDy0cRzVMIhUbsLvWgOTdBEY2Bd6xxAoIBACQhVhwLXDUSGe96p8QCPQ2SMo8/ +lU4oPQ8MIc/gYEJUUYvJfkvCy0fnot9P/ZPppksJPQidqZDhDmzbPxuaIwiel6RU +KTEwRbg7M8YtCngAGjUSxTWZp1sklFFXxbtDW438QzLAtMvGCZ1l0QEd6ajG1BHi +fm96oJqaKEhcg4tthz3NyXihvQ7/ZrLpvcyR25Dzjlx3X6/0DTT4hdUiQOW5a3Uo +/YjAC2J8MeKJK6UYW2spcmQ5NmVhG/+8UoGN94DWRWpgl2dtB2HGssLPmB27TOdQ +wezcsubDEHZCtTc2y22l/MMwCwLZu5GBUNUy4EzDjPxoC7FtHSdsJ9sUdsg= +-----END RSA PRIVATE KEY----- diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-client.crt b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-client.crt new file mode 100644 index 000000000000..40a184bdf322 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-client.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEGjCCAgKgAwIBAgIUCNvMLf/1EZcO6R9L/PQVWN8agbkwDQYJKoZIhvcNAQEL +BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MB4XDTIzMDUwMTIwNDkxMFoXDTI0MDQzMDIwNDkxMFow +LzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDESMBAGA1UEAwwJbG9jYWxob3N0 +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzP5NGFAhk6hAVr3YshRJ +YGxS2IGphFaq/c99QZQ62JbcSwceFo0p8Px9JiaT38n7NejEy6t6U0PQP2B9r3pJ +p0RwplvITLd1lp96DdMQeGXKa2rqJ62u9//u/XxFboVU6QYC90Pnqi6sRWejKEI8 +Yowg6erjNMCQiIAKqhWPfdsJOxf79102gdahuTT8A89p551u7a84oTRtX4fLksP2 +x0BVFb0/Dirz5ngwm6YHpN+8z7BYIyj4dLzzFjaqU1gptxtGygap1GtD1X9fJ61l +k6K8vMww4+/zYOoGratUTNeKHOvvXf9SnjoqyMTvJFyTX+5snkyL81q3+XgXJOYL +ZQIDAQABoyIwIDALBgNVHQ8EBAMCBaAwEQYJYIZIAYb4QgEBBAQDAgeAMA0GCSqG +SIb3DQEBCwUAA4ICAQAq5Em7EVkGhPgIMDmxhm398Kv8OivFxX6x5aGnJ+m8+mZV ++wrkjRvpqN/+CtTsid2q4+qYdlov8hJ2oxwVhfnrF5b7Xj7caC2FJifPXPiaMogT +5VI4uCABBuVQR0kDtnPF8bRiTWCKC3DC84GqMp0cUs3Qyf1dLcjhcc9dSROn00y8 +/qmIz8roJ2esnqG12rTGdIAaWSgBCMKFjrV8YmxLf+z72VHSx6uC5CARG+UYa5Mu +vga0Q77QmwSstKBvGUBtvzQoML3/UFCikdfOxDgvJbr8Q0yEEw8hK7vGZLaj00zB +U4B5+DfV285RW09ihp2YMxuz3mL2tM5++RYJphB9/VTN3/f+geKt2pPA3Rkk11Ug +LP3NdpT5ZnQL9ehtmIExk2NVBi+RmGCcP7KcMtlq44FdyRF7p6qdg/Eq5n/sOMxQ +DnamgWDQltm6cuZ49haCXLZIbfqM2cHARIw/Sv3Dgd9SSDL2pooWI2U82fQ9A71q +u/hUlNDZm0v51IfgzJcbAtlAYd2OVlgCkkkFtbgdOaQUShIkcCKcpxtgQzpynNMO +DJoO41VXpMzBN7/ppVi0JrF7RkaXGeoNsqfvcmjQEuXUOluge2q8kHDf7gEUddKa +ijPHtkFQF2ujCGr/AVYjCMSlOk5WhRh8ZVxN0KbiWZJUN8akX4gU4KIpTe1big== +-----END CERTIFICATE----- diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-client.key b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-client.key new file mode 100644 index 000000000000..a31717ac4d53 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-client.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAzP5NGFAhk6hAVr3YshRJYGxS2IGphFaq/c99QZQ62JbcSwce +Fo0p8Px9JiaT38n7NejEy6t6U0PQP2B9r3pJp0RwplvITLd1lp96DdMQeGXKa2rq +J62u9//u/XxFboVU6QYC90Pnqi6sRWejKEI8Yowg6erjNMCQiIAKqhWPfdsJOxf7 +9102gdahuTT8A89p551u7a84oTRtX4fLksP2x0BVFb0/Dirz5ngwm6YHpN+8z7BY +Iyj4dLzzFjaqU1gptxtGygap1GtD1X9fJ61lk6K8vMww4+/zYOoGratUTNeKHOvv +Xf9SnjoqyMTvJFyTX+5snkyL81q3+XgXJOYLZQIDAQABAoIBAFNG/Arkgr95mqmi +dmXh1+1UFFPgWP1qOAzkPf5mOYHDx7qzKYX/0woTiMP26BwB8gv0g/45q3goFHGq +wWSISWOqahkrMDP6U8rc/rifBhHjSFhbFsUHygz17CEOWyaLA/OmfY32CCcazuFj +OOUiA2YFh1mAEs1bbVwGqE5wc9qsZtBlJxudSWtSZoJuFECDNqLfQXkJ39KnKhp4 +D337nOR/xww81202mlfF/vvhRMfUIUS2Ij9USndp9huBHFSxf1mYjD1ljjx6U7el +new8TPf76J7nuy/6SxZ9wF6P2dk/eQcN5AnIcDGq0WzS3VcJc/KG/+maflCvH0dB +SLfx4AECgYEA7e+5/UhWZ62BfF1/Nat95+t+bh8UYN8gPEUos7oS/cUrme7YAPQT +MTWNulpmgGCRDxeXU9XBaPGyF7cU5bx28sK64ZUe8D1ySgGpVeSEQtjCLFEf6eat +801TQVNaH2WlDZTm+Onfr7ppFN1pLrBY+83m9TDJd6v4qHsvtNkcx38CgYEA3I5U +OvvoTEj8+Xc0U296NU+aWJLNrkDH6lFtdXsLyoumxh0DDbKSw8ia28Z5+8tz0mdB +33sIsnnsQ+83YoiXyopM9GFZdZH3luKrXgOGH8QFygJI8xGqqcLjeWNkW0b0KCkv +AoiedqOOmCdRMUfy3v5irH+4O90ZmW6VxNKbfxsCgYEAtjjFOQwAWHCR3TwBo4nN +6CL7dbzJr5LSLjZNAK/9wWoShVZdCQXj+OjpvRFktOa/0U4g7+yhrgyEdxMYpwUa +F7s4wnCg/B4i/Difhg93l3ZH5wbOKSUojU/n9fyu5aLDsE4cQf9i90MNHRSgbEhU +Law4OAmAEe2bhvSoyZkJKGMCgYBgW25BNr0OVvTuqD2cFh/2Goj8GWbysiqlHF4N +7WwBWXHLK/Ghklq8XnAJhHTWpNQ9IA+Pa1kpYErwgxpXWgW23yUvvzguPU9GBFGK +CVAXoLRGxSjJyPYepJ5s8hduKVmSEiwPl1Bj1KD/qG24cg6RjeHeKw56WOZOOhoE +m16D8QKBgBHXU31OJ2KMDnwjsMW2SlpYKoIQlJyTg3qvN7gu+ZGo5B7hviqh5wN1 +y577N/NT9No8qGNEGTZl35hkyw8DmB4RAZp7G1qbVCGszUBt/vS6Guv82/EgMVo2 +ZgiQBkI1kEOtj5LMVBfOKTRBEpyAm5fSZ+eQtSIc5LCbQ8aEvio4 +-----END RSA PRIVATE KEY----- diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-server.crt b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-server.crt new file mode 100644 index 000000000000..06c8906b9157 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-server.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEGjCCAgKgAwIBAgIUCNvMLf/1EZcO6R9L/PQVWN8agbgwDQYJKoZIhvcNAQEL +BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MB4XDTIzMDUwMTIwNDkxMFoXDTI0MDQzMDIwNDkxMFow +LzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDESMBAGA1UEAwwJbG9jYWxob3N0 +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqgji6StU0UkWfYmZumQO +L7SnFg7/xBM5ubMtXJsBOS0RaRWJ0WwIsQ2ksDOf2ybDyXiePplbtR/4GsnXPyNp +H1vgY/Mt/PeiP/lHw9dDTdSx6YMMxGVoILsHkblaeHwh8yVGCg2gdoRROscgjS+e +j7gTr4H2UBlepHsjZBKc+hamDrIC3vK42iQUyzbClJ8lpY+KbL4R4KhsuhTb2jRL +wye3m2w+YU1jvE+IioQfozlZTAw0SX7whcCw0B3hLVQg6hsdSeSYkCUZiZY72ySR +fI+mDcnJVcetH2ShK1zVFBpDs9qkJSA9YumO1ZKVDdDseeuHHsEUG2/pszQ2cHH6 +EwIDAQABoyIwIDALBgNVHQ8EBAMCBaAwEQYJYIZIAYb4QgEBBAQDAgZAMA0GCSqG +SIb3DQEBCwUAA4ICAQACFGGWNTEDCvkfEuZZT84zT8JQ9O5wDzgYDX/xRSXbB1Jv +fd9QQfwlVFXg3jewIgWZG0TgQt/7yF6RYOtU+GRP6meJhSm9/11KnYYLNlHQU1QE +7imreHAnsJiueHXPmpe9EL4jv2mQt7GSccABMf1pfBQ+C0dETnUoH68oO3LttU16 +f43H1royvOm3G6LnJb83rLYVe07P1PTjk/37gaFCf54J1eDfqntVDiSq8H6fV+nL +9ZvsVuC4BcREnB3oY7vsJFBhGeK/3+QFX4Zr3DTwLxiWe2pqSQfUbn4+d6+uwIY7 +pixgNorpebKQn0vX/G4llVjOmBNjlgSzDyVTYObBz316GojF7yRk3oBbxK//3w/t +XVhLwrPpqB5Jehh2HsKKZrdfnjB1Gn+pDpSEMVDrCbWxzAJz4WOu2ihCYYsF3Gts +lzI1ZzD+UpFyeHG/1wQHzyQwADBiaYfh1oAnpNcOvJhT1S6IVGImcOBNa8u14aVG +NjvnJWVn3v3dcvAVO1ZUwX9TdHP11oIpn7fGYZzSxCDrhGaFeW0tscxddHRrXdwk +IHyHZ3o2RgivhaSc4C04nuZEX00ohTgtKo2rpK1SP+gn64Yh+u+O6AH8r+q7cZy2 +gZNscwHAmkEalP78D5vnOFRUYEVrNc/X2f+rwFoQD7B8GNGa/visAkD7myg7JQ== +-----END CERTIFICATE----- diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-server.key b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-server.key new file mode 100644 index 000000000000..8dcb542a2ebf --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/test/resources/ssl/test-server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAqgji6StU0UkWfYmZumQOL7SnFg7/xBM5ubMtXJsBOS0RaRWJ +0WwIsQ2ksDOf2ybDyXiePplbtR/4GsnXPyNpH1vgY/Mt/PeiP/lHw9dDTdSx6YMM +xGVoILsHkblaeHwh8yVGCg2gdoRROscgjS+ej7gTr4H2UBlepHsjZBKc+hamDrIC +3vK42iQUyzbClJ8lpY+KbL4R4KhsuhTb2jRLwye3m2w+YU1jvE+IioQfozlZTAw0 +SX7whcCw0B3hLVQg6hsdSeSYkCUZiZY72ySRfI+mDcnJVcetH2ShK1zVFBpDs9qk +JSA9YumO1ZKVDdDseeuHHsEUG2/pszQ2cHH6EwIDAQABAoIBAQCLTuiJ3OSK63Sv +udLncR5mW34hhnxqas36pSBfJOflrlT7YZgeqoKcfO8XJdSsup/iKx6Lbx5B0UV2 +vTPLGPoBpUa83PoqrcCS5Wu0umL8G20AQkxthB/B4TocXF4RJLK0AS/XAL8dGt9q +Zsb2pbMlUM1gF/x0N7Tg0bp3PQC7rAgYe7JFvArxRrmDP38FE9Cg5EIAVMN8Fw2b +dxKZxJ+mqj1t1bU4/bsrYBs9QpNrBjQc0KTFOamwkvWI7FhHXQtIZfJvvBj8mN7z +He7B5j/JcfGC5LN1UpL4tziOrKwMGGIvpAnpbVEv29SWxOG5Vbccb4ghBN+VJqSH +6WON791hAoGBAN7Q5nuCk+L/8BQh29WHZuP6dbLyMMjWMyuDm2xEYD0fjjacvU7r +KIQDcQY3E7bXu6OXKQmxARFY7HuZUyGg8R4QBeAEVfDPjRKzGZgA1+gF325eQwAQ +giXqg0paE2ePfbawi21NfQPCMMhb4n3QzpYd4eEsFFwMvt4oZCPkHubJAoGBAMNb +pGajPKW19dFWP5OsKc1U6itej78RQRjO7zpQ3JWvNuMa/SZzEa2blFuks585u6M2 +XdVPhhspc0TwS+asizNEMDYaPpAjmg9X9LY87hcYTC0FXT0Axx+7A/JtmMAVF3Pn +4lvhfdB5XSV5jo/BtUJ3vDx5FSFIHQbbj1agGpv7AoGAdv6pmJyLzldRJ+9NMCQ3 +1tkTspWVaCy89yg6AQAjRYFsuc3LbDI6WQZdfiw74xIjq6I20G4vW8xZv0iLFRKW +sq9r889c9lZhyPLNYFhS9h7szEybC5XFa+pqY3Lnmg8P3Fk8nQsdELzMwLQRqY+y +RImA8HhSBzbnWE3J7UEPH8ECgYAXyNGEOX2Jw1SRTwnghcZ1HFCCRToFDim5xn/z +vqKMis+I6OFHTB0r4NQ4MB46VYIVxem4rbzrE6nYC9WB2SH9dODVxW42iE8abR/7 +DAIEx82Gca+/XJfhshgx7Mv7HtZDI0k43IQ/3HbNuDX2JKRX2lINnsRG0AvQqOyT +pFx4/wKBgQCXU0LGSCgNwuqdhXHoaFEzAzzspDjCI+9KDuchkvoYWfCWElX035O9 +TbEybMjCuv08eAqeJv++a1jnTmJwf+w+WhBG+DpYcro1JXmo8Lu9KAbiq0lJGQP6 +tX9gr0XY3IC+L5ndOANuFH6mjGlnp7Z+J8i7HFFoSa+MI2JkoQ5yVA== +-----END RSA PRIVATE KEY----- From d72fb8e127e9d87b41b2c0549838b26b6a7f4f83 Mon Sep 17 00:00:00 2001 From: Vedran Pavic Date: Mon, 25 Sep 2023 16:50:09 +0200 Subject: [PATCH 0593/1215] Add support for configuring non-standard JMS acknowledge modes Prior to this commit, `spring.jms.listener.session.acknowledge-mode` and `spring.jms.template.session.acknowledge-mode` accepted only a predefined set of values representing standard JMS acknowledge modes. This commit adds support for also using arbitrary integer values to these configuration properties, which allows vendor-specific JMS acknowledge modes to be configured. See gh-37576 --- ...JmsListenerContainerFactoryConfigurer.java | 4 +- .../jms/JmsAcknowledgeModeMapper.java | 46 +++++++++++++++ .../jms/JmsAutoConfiguration.java | 5 +- .../boot/autoconfigure/jms/JmsProperties.java | 57 +++---------------- ...itional-spring-configuration-metadata.json | 34 +++++++++++ 5 files changed, 93 insertions(+), 53 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAcknowledgeModeMapper.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java index 2e5f1cc58a0f..fb9ef42f8300 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java @@ -111,7 +111,9 @@ public void configure(DefaultJmsListenerContainerFactory factory, ConnectionFact map.from(this.destinationResolver).to(factory::setDestinationResolver); map.from(this.messageConverter).to(factory::setMessageConverter); map.from(this.exceptionListener).to(factory::setExceptionListener); - map.from(sessionProperties.getAcknowledgeMode()::getMode).to(factory::setSessionAcknowledgeMode); + map.from(sessionProperties.getAcknowledgeMode()) + .as(JmsAcknowledgeModeMapper::map) + .to(factory::setSessionAcknowledgeMode); if (this.transactionManager == null && sessionProperties.getTransacted() == null) { factory.setSessionTransacted(true); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAcknowledgeModeMapper.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAcknowledgeModeMapper.java new file mode 100644 index 000000000000..66991fe5ab88 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAcknowledgeModeMapper.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2023 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.jms; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.jms.Session; + +/** + * Helper class used to map JMS acknowledge modes. + * + * @author Vedran Pavic + */ +final class JmsAcknowledgeModeMapper { + + private static final Map acknowledgeModes = new HashMap<>(3); + + static { + acknowledgeModes.put("auto", Session.AUTO_ACKNOWLEDGE); + acknowledgeModes.put("client", Session.CLIENT_ACKNOWLEDGE); + acknowledgeModes.put("dups_ok", Session.DUPS_OK_ACKNOWLEDGE); + } + + private JmsAcknowledgeModeMapper() { + } + + static int map(String acknowledgeMode) { + return acknowledgeModes.computeIfAbsent(acknowledgeMode.toLowerCase(), Integer::parseInt); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java index 389023053ea0..9eaa347e81d5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java @@ -28,7 +28,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; -import org.springframework.boot.autoconfigure.jms.JmsProperties.AcknowledgeMode; import org.springframework.boot.autoconfigure.jms.JmsProperties.DeliveryMode; import org.springframework.boot.autoconfigure.jms.JmsProperties.Template; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -91,8 +90,8 @@ public JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) { private void mapTemplateProperties(Template properties, JmsTemplate template) { PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(properties.getSession()::getAcknowledgeMode) - .asInt(AcknowledgeMode::getMode) - .to(template::setSessionAcknowledgeMode); + .to((acknowledgeMode) -> template + .setSessionAcknowledgeMode(JmsAcknowledgeModeMapper.map(acknowledgeMode))); map.from(properties.getSession()::isTransacted).to(template::setSessionTransacted); map.from(properties::getDefaultDestination).whenNonNull().to(template::setDefaultDestinationName); map.from(properties::getDeliveryDelay).whenNonNull().as(Duration::toMillis).to(template::setDeliveryDelay); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java index cc6d3140690f..d0eee224307f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java @@ -171,12 +171,12 @@ public void setAutoStartup(boolean autoStartup) { @Deprecated(since = "3.2.0", forRemoval = true) @DeprecatedConfigurationProperty(replacement = "spring.jms.listener.session.acknowledge-mode", since = "3.2.0") - public AcknowledgeMode getAcknowledgeMode() { + public String getAcknowledgeMode() { return this.session.getAcknowledgeMode(); } @Deprecated(since = "3.2.0", forRemoval = true) - public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) { + public void setAcknowledgeMode(String acknowledgeMode) { this.session.setAcknowledgeMode(acknowledgeMode); } @@ -232,7 +232,7 @@ public static class Session { /** * Acknowledge mode of the listener container. */ - private AcknowledgeMode acknowledgeMode = AcknowledgeMode.AUTO; + private String acknowledgeMode = "auto"; /** * Whether the listener container should use transacted JMS sessions. Defaults @@ -240,11 +240,11 @@ public static class Session { */ private Boolean transacted; - public AcknowledgeMode getAcknowledgeMode() { + public String getAcknowledgeMode() { return this.acknowledgeMode; } - public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) { + public void setAcknowledgeMode(String acknowledgeMode) { this.acknowledgeMode = acknowledgeMode; } @@ -376,18 +376,18 @@ public static class Session { /** * Acknowledge mode used when creating sessions. */ - private AcknowledgeMode acknowledgeMode = AcknowledgeMode.AUTO; + private String acknowledgeMode = "auto"; /** * Whether to use transacted sessions. */ private boolean transacted = false; - public AcknowledgeMode getAcknowledgeMode() { + public String getAcknowledgeMode() { return this.acknowledgeMode; } - public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) { + public void setAcknowledgeMode(String acknowledgeMode) { this.acknowledgeMode = acknowledgeMode; } @@ -403,47 +403,6 @@ public void setTransacted(boolean transacted) { } - /** - * Translate the acknowledge modes defined on the {@link jakarta.jms.Session}. - * - *

    - * {@link jakarta.jms.Session#SESSION_TRANSACTED} is not defined as we take care of - * this already through a call to {@code setSessionTransacted}. - */ - public enum AcknowledgeMode { - - /** - * Messages sent or received from the session are automatically acknowledged. This - * is the simplest mode and enables once-only message delivery guarantee. - */ - AUTO(1), - - /** - * Messages are acknowledged once the message listener implementation has called - * {@link jakarta.jms.Message#acknowledge()}. This mode gives the application - * (rather than the JMS provider) complete control over message acknowledgement. - */ - CLIENT(2), - - /** - * Similar to auto acknowledgment except that said acknowledgment is lazy. As a - * consequence, the messages might be delivered more than once. This mode enables - * at-least-once message delivery guarantee. - */ - DUPS_OK(3); - - private final int mode; - - AcknowledgeMode(int mode) { - this.mode = mode; - } - - public int getMode() { - return this.mode; - } - - } - public enum DeliveryMode { /** 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 bfd976e9b926..5f8c301fd27d 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 @@ -3011,6 +3011,40 @@ } ] }, + { + "name": "spring.jms.listener.session.acknowledge-mode", + "values": [ + { + "value": "auto", + "description": "Messages sent or received from the session are automatically acknowledged. This is the simplest mode and enables once-only message delivery guarantee." + }, + { + "value": "client", + "description": "Messages are acknowledged once the message listener implementation has called \"jakarta.jms.Message#acknowledge()\". This mode gives the application (rather than the JMS provider) complete control over message acknowledgement." + }, + { + "value": "dups_ok", + "description": "Similar to auto acknowledgment except that said acknowledgment is lazy. As a consequence, the messages might be delivered more than once. This mode enables at-least-once message delivery guarantee." + } + ] + }, + { + "name": "spring.jms.template.session.acknowledge-mode", + "values": [ + { + "value": "auto", + "description": "Messages sent or received from the session are automatically acknowledged. This is the simplest mode and enables once-only message delivery guarantee." + }, + { + "value": "client", + "description": "Messages are acknowledged once the message listener implementation has called \"jakarta.jms.Message#acknowledge()\". This mode gives the application (rather than the JMS provider) complete control over message acknowledgement." + }, + { + "value": "dups_ok", + "description": "Similar to auto acknowledgment except that said acknowledgment is lazy. As a consequence, the messages might be delivered more than once. This mode enables at-least-once message delivery guarantee." + } + ] + }, { "name": "spring.jmx.server", "providers": [ From 6fbc328b4cd00e4ad9d4ded852dd6064c24d4d25 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 Sep 2023 18:11:50 +0100 Subject: [PATCH 0594/1215] Polish "Add support for configuring non-standard JMS acknowledge modes" See gh-37576 --- .../autoconfigure/jms/AcknowledgeMode.java | 105 ++++++++++++++++++ ...JmsListenerContainerFactoryConfigurer.java | 4 +- .../jms/JmsAcknowledgeModeMapper.java | 46 -------- .../jms/JmsAutoConfiguration.java | 23 +++- .../boot/autoconfigure/jms/JmsProperties.java | 16 +-- .../jms/AcknowledgeModeTests.java | 88 +++++++++++++++ .../jms/JmsAutoConfigurationTests.java | 35 ++++++ 7 files changed, 257 insertions(+), 60 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/AcknowledgeMode.java delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAcknowledgeModeMapper.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/AcknowledgeModeTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/AcknowledgeMode.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/AcknowledgeMode.java new file mode 100644 index 000000000000..f3c3240d7e2d --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/AcknowledgeMode.java @@ -0,0 +1,105 @@ +/* + * Copyright 2012-2023 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.jms; + +import java.util.HashMap; +import java.util.Map; + +import jakarta.jms.Session; + +import org.springframework.jms.support.JmsAccessor; + +/** + * Acknowledge modes for a JMS Session. Supports the acknowledge modes defined by + * {@link jakarta.jms.Session} as well as other, non-standard modes. + * + *

    + * Note that {@link jakarta.jms.Session#SESSION_TRANSACTED} is not defined. It should be + * handled through a call to {@link JmsAccessor#setSessionTransacted(boolean)}. + * + * @author Andy Wilkinson + * @since 3.2.0 + */ +public final class AcknowledgeMode { + + private static final Map knownModes = new HashMap<>(3); + + /** + * Messages sent or received from the session are automatically acknowledged. This is + * the simplest mode and enables once-only message delivery guarantee. + */ + public static final AcknowledgeMode AUTO = new AcknowledgeMode(Session.AUTO_ACKNOWLEDGE); + + /** + * Messages are acknowledged once the message listener implementation has called + * {@link jakarta.jms.Message#acknowledge()}. This mode gives the application (rather + * than the JMS provider) complete control over message acknowledgement. + */ + public static final AcknowledgeMode CLIENT = new AcknowledgeMode(Session.CLIENT_ACKNOWLEDGE); + + /** + * Similar to auto acknowledgment except that said acknowledgment is lazy. As a + * consequence, the messages might be delivered more than once. This mode enables + * at-least-once message delivery guarantee. + */ + public static final AcknowledgeMode DUPS_OK = new AcknowledgeMode(Session.DUPS_OK_ACKNOWLEDGE); + + static { + knownModes.put("auto", AUTO); + knownModes.put("client", CLIENT); + knownModes.put("dupsok", DUPS_OK); + } + + private final int mode; + + private AcknowledgeMode(int mode) { + this.mode = mode; + } + + public int getMode() { + return this.mode; + } + + /** + * Creates an {@code AcknowledgeMode} of the given {@code mode}. The mode may be + * {@code auto}, {@code client}, {@code dupsok} or a non-standard acknowledge mode + * that can be {@link Integer#parseInt parsed as an integer}. + * @param mode the mode + * @return the acknowledge mode + */ + public static AcknowledgeMode of(String mode) { + String canonicalMode = canonicalize(mode); + AcknowledgeMode knownMode = knownModes.get(canonicalMode); + try { + return (knownMode != null) ? knownMode : new AcknowledgeMode(Integer.parseInt(canonicalMode)); + } + catch (NumberFormatException ex) { + throw new IllegalArgumentException("'" + mode + + "' is neither a known acknowledge mode (auto, client, or dups_ok) nor an integer value"); + } + } + + private static String canonicalize(String input) { + StringBuilder canonicalName = new StringBuilder(input.length()); + input.chars() + .filter(Character::isLetterOrDigit) + .map(Character::toLowerCase) + .forEach((c) -> canonicalName.append((char) c)); + return canonicalName.toString(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java index fb9ef42f8300..2e5f1cc58a0f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java @@ -111,9 +111,7 @@ public void configure(DefaultJmsListenerContainerFactory factory, ConnectionFact map.from(this.destinationResolver).to(factory::setDestinationResolver); map.from(this.messageConverter).to(factory::setMessageConverter); map.from(this.exceptionListener).to(factory::setExceptionListener); - map.from(sessionProperties.getAcknowledgeMode()) - .as(JmsAcknowledgeModeMapper::map) - .to(factory::setSessionAcknowledgeMode); + map.from(sessionProperties.getAcknowledgeMode()::getMode).to(factory::setSessionAcknowledgeMode); if (this.transactionManager == null && sessionProperties.getTransacted() == null) { factory.setSessionTransacted(true); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAcknowledgeModeMapper.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAcknowledgeModeMapper.java deleted file mode 100644 index 66991fe5ab88..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAcknowledgeModeMapper.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2012-2023 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.jms; - -import java.util.HashMap; -import java.util.Map; - -import jakarta.jms.Session; - -/** - * Helper class used to map JMS acknowledge modes. - * - * @author Vedran Pavic - */ -final class JmsAcknowledgeModeMapper { - - private static final Map acknowledgeModes = new HashMap<>(3); - - static { - acknowledgeModes.put("auto", Session.AUTO_ACKNOWLEDGE); - acknowledgeModes.put("client", Session.CLIENT_ACKNOWLEDGE); - acknowledgeModes.put("dups_ok", Session.DUPS_OK_ACKNOWLEDGE); - } - - private JmsAcknowledgeModeMapper() { - } - - static int map(String acknowledgeMode) { - return acknowledgeModes.computeIfAbsent(acknowledgeMode.toLowerCase(), Integer::parseInt); - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java index 9eaa347e81d5..2c2ba4b5a0b9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java @@ -17,10 +17,15 @@ package org.springframework.boot.autoconfigure.jms; import java.time.Duration; +import java.util.List; import jakarta.jms.ConnectionFactory; import jakarta.jms.Message; +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeReference; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -28,6 +33,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration.JmsRuntimeHints; import org.springframework.boot.autoconfigure.jms.JmsProperties.DeliveryMode; import org.springframework.boot.autoconfigure.jms.JmsProperties.Template; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -35,6 +41,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.jms.core.JmsMessageOperations; import org.springframework.jms.core.JmsMessagingTemplate; import org.springframework.jms.core.JmsOperations; @@ -55,6 +62,7 @@ @ConditionalOnBean(ConnectionFactory.class) @EnableConfigurationProperties(JmsProperties.class) @Import(JmsAnnotationDrivenConfiguration.class) +@ImportRuntimeHints(JmsRuntimeHints.class) public class JmsAutoConfiguration { @Configuration(proxyBeanMethods = false) @@ -89,9 +97,7 @@ public JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) { private void mapTemplateProperties(Template properties, JmsTemplate template) { PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(properties.getSession()::getAcknowledgeMode) - .to((acknowledgeMode) -> template - .setSessionAcknowledgeMode(JmsAcknowledgeModeMapper.map(acknowledgeMode))); + map.from(properties.getSession().getAcknowledgeMode()::getMode).to(template::setSessionAcknowledgeMode); map.from(properties.getSession()::isTransacted).to(template::setSessionTransacted); map.from(properties::getDefaultDestination).whenNonNull().to(template::setDefaultDestinationName); map.from(properties::getDeliveryDelay).whenNonNull().as(Duration::toMillis).to(template::setDeliveryDelay); @@ -125,4 +131,15 @@ private void mapTemplateProperties(Template properties, JmsMessagingTemplate mes } + static class JmsRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + hints.reflection() + .registerType(TypeReference.of(AcknowledgeMode.class), (type) -> type.withMethod("of", + List.of(TypeReference.of(String.class)), ExecutableMode.INVOKE)); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java index d0eee224307f..08b7a6d5ee5e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java @@ -171,12 +171,12 @@ public void setAutoStartup(boolean autoStartup) { @Deprecated(since = "3.2.0", forRemoval = true) @DeprecatedConfigurationProperty(replacement = "spring.jms.listener.session.acknowledge-mode", since = "3.2.0") - public String getAcknowledgeMode() { + public AcknowledgeMode getAcknowledgeMode() { return this.session.getAcknowledgeMode(); } @Deprecated(since = "3.2.0", forRemoval = true) - public void setAcknowledgeMode(String acknowledgeMode) { + public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) { this.session.setAcknowledgeMode(acknowledgeMode); } @@ -232,7 +232,7 @@ public static class Session { /** * Acknowledge mode of the listener container. */ - private String acknowledgeMode = "auto"; + private AcknowledgeMode acknowledgeMode = AcknowledgeMode.AUTO; /** * Whether the listener container should use transacted JMS sessions. Defaults @@ -240,11 +240,11 @@ public static class Session { */ private Boolean transacted; - public String getAcknowledgeMode() { + public AcknowledgeMode getAcknowledgeMode() { return this.acknowledgeMode; } - public void setAcknowledgeMode(String acknowledgeMode) { + public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) { this.acknowledgeMode = acknowledgeMode; } @@ -376,18 +376,18 @@ public static class Session { /** * Acknowledge mode used when creating sessions. */ - private String acknowledgeMode = "auto"; + private AcknowledgeMode acknowledgeMode = AcknowledgeMode.AUTO; /** * Whether to use transacted sessions. */ private boolean transacted = false; - public String getAcknowledgeMode() { + public AcknowledgeMode getAcknowledgeMode() { return this.acknowledgeMode; } - public void setAcknowledgeMode(String acknowledgeMode) { + public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) { this.acknowledgeMode = acknowledgeMode; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/AcknowledgeModeTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/AcknowledgeModeTests.java new file mode 100644 index 000000000000..77957f5a9674 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/AcknowledgeModeTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2023 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.jms; + +import jakarta.jms.Session; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link AcknowledgeMode}. + * + * @author Andy Wilkinson + */ +class AcknowledgeModeTests { + + @ParameterizedTest + @EnumSource(Mapping.class) + void stringIsMappedToInt(Mapping mapping) { + assertThat(AcknowledgeMode.of(mapping.actual)).extracting(AcknowledgeMode::getMode).isEqualTo(mapping.expected); + } + + @Test + void mapShouldThrowWhenMapIsCalledWithUnknownNonIntegerString() { + assertThatIllegalArgumentException().isThrownBy(() -> AcknowledgeMode.of("some-string")) + .withMessage( + "'some-string' is neither a known acknowledge mode (auto, client, or dups_ok) nor an integer value"); + } + + private enum Mapping { + + AUTO_LOWER_CASE("auto", Session.AUTO_ACKNOWLEDGE), + + CLIENT_LOWER_CASE("client", Session.CLIENT_ACKNOWLEDGE), + + DUPS_OK_LOWER_CASE("dups_ok", Session.DUPS_OK_ACKNOWLEDGE), + + AUTO_UPPER_CASE("AUTO", Session.AUTO_ACKNOWLEDGE), + + CLIENT_UPPER_CASE("CLIENT", Session.CLIENT_ACKNOWLEDGE), + + DUPS_OK_UPPER_CASE("DUPS_OK", Session.DUPS_OK_ACKNOWLEDGE), + + AUTO_MIXED_CASE("AuTo", Session.AUTO_ACKNOWLEDGE), + + CLIENT_MIXED_CASE("CliEnT", Session.CLIENT_ACKNOWLEDGE), + + DUPS_OK_MIXED_CASE("dUPs_Ok", Session.DUPS_OK_ACKNOWLEDGE), + + DUPS_OK_KEBAB_CASE("DUPS-OK", Session.DUPS_OK_ACKNOWLEDGE), + + DUPS_OK_NO_SEPARATOR_UPPER_CASE("DUPSOK", Session.DUPS_OK_ACKNOWLEDGE), + + DUPS_OK_NO_SEPARATOR_LOWER_CASE("dupsok", Session.DUPS_OK_ACKNOWLEDGE), + + DUPS_OK_NO_SEPARATOR_MIXED_CASE("duPSok", Session.DUPS_OK_ACKNOWLEDGE), + + INTEGER("36", 36); + + private final String actual; + + private final int expected; + + Mapping(String actual, int expected) { + this.actual = actual; + this.expected = expected; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java index 49f5ddb3b272..b9b6025bae46 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java @@ -24,14 +24,18 @@ import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory; import org.junit.jupiter.api.Test; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; +import org.springframework.context.aot.ApplicationContextAotGenerator; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.jms.annotation.EnableJms; import org.springframework.jms.config.DefaultJmsListenerContainerFactory; @@ -160,6 +164,16 @@ private void testJmsListenerContainerFactoryWithCustomSettings(AssertableApplica assertThat(container).hasFieldOrPropertyWithValue("receiveTimeout", 2000L); } + @Test + void testJmsListenerContainerFactoryWithNonStandardAcknowledgeMode() { + this.contextRunner.withUserConfiguration(EnableJmsConfiguration.class) + .withPropertyValues("spring.jms.listener.session.acknowledge-mode=9") + .run((context) -> { + DefaultMessageListenerContainer container = getContainer(context, "jmsListenerContainerFactory"); + assertThat(container.getSessionAcknowledgeMode()).isEqualTo(9); + }); + } + @Test void testJmsListenerContainerFactoryWithDefaultSettings() { this.contextRunner.withUserConfiguration(EnableJmsConfiguration.class) @@ -300,6 +314,16 @@ void testJmsTemplateFullCustomization() { }); } + @Test + void testJmsTemplateWithNonStandardAcknowledgeMode() { + this.contextRunner.withUserConfiguration(EnableJmsConfiguration.class) + .withPropertyValues("spring.jms.template.session.acknowledge-mode=7") + .run((context) -> { + JmsTemplate jmsTemplate = context.getBean(JmsTemplate.class); + assertThat(jmsTemplate.getSessionAcknowledgeMode()).isEqualTo(7); + }); + } + @Test void testJmsMessagingTemplateUseConfiguredDefaultDestination() { this.contextRunner.withPropertyValues("spring.jms.template.default-destination=testQueue").run((context) -> { @@ -367,6 +391,17 @@ void enableJmsAutomatically() { .hasBean(JmsListenerConfigUtils.JMS_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME)); } + @Test + void runtimeHintsAreRegisteredForBindingOfAcknowledgeMode() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.register(ArtemisAutoConfiguration.class, JmsAutoConfiguration.class); + TestGenerationContext generationContext = new TestGenerationContext(); + new ApplicationContextAotGenerator().processAheadOfTime(context, generationContext); + assertThat(RuntimeHintsPredicates.reflection().onMethod(AcknowledgeMode.class, "of").invoke()) + .accepts(generationContext.getRuntimeHints()); + } + } + @Configuration(proxyBeanMethods = false) static class TestConfiguration { From 446677375e0c7311d9bfbaecc8dbc1e5aefb1f3d Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Wed, 11 Oct 2023 23:07:33 +0900 Subject: [PATCH 0595/1215] Polish gh-35082 See gh-37831 --- .../export/otlp/OtlpMetricsConnectionDetails.java | 2 +- .../otlp/OtlpMetricsExportAutoConfiguration.java | 2 +- .../tracing/otlp/OtlpTracingConfigurations.java | 7 ++----- .../tracing/otlp/OtlpTracingConnectionDetails.java | 6 +++--- ...MetricsDockerComposeConnectionDetailsFactory.java | 8 ++++---- ...TracingDockerComposeConnectionDetailsFactory.java | 2 +- ...poseConnectionDetailsFactoryIntegrationTests.java | 2 +- ...poseConnectionDetailsFactoryIntegrationTests.java | 2 +- ...tryMetricsContainerConnectionDetailsFactory.java} | 10 +++++----- ...tryTracingContainerConnectionDetailsFactory.java} | 12 ++++++------ .../src/main/resources/META-INF/spring.factories | 4 ++-- ...nerConnectionDetailsFactoryIntegrationTests.java} | 4 ++-- ...nerConnectionDetailsFactoryIntegrationTests.java} | 4 ++-- .../testsupport/testcontainers/DockerImageNames.java | 5 +++-- 14 files changed, 34 insertions(+), 36 deletions(-) rename spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/{OpenTelemetryMetricsConnectionDetailsFactory.java => OpenTelemetryMetricsContainerConnectionDetailsFactory.java} (87%) rename spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/{OpenTelemetryTracingConnectionDetailsFactory.java => OpenTelemetryTracingContainerConnectionDetailsFactory.java} (81%) rename spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/{OpenTelemetryMetricsConnectionDetailsFactoryIntegrationTests.java => OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java} (96%) rename spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/{OpenTelemetryTracingConnectionDetailsFactoryIntegrationTests.java => OpenTelemetryTracingContainerConnectionDetailsFactoryIntegrationTests.java} (93%) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsConnectionDetails.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsConnectionDetails.java index 16f968a89de0..eeef0ae685bc 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsConnectionDetails.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsConnectionDetails.java @@ -19,7 +19,7 @@ import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; /** - * Details required to establish a connection to a OpenTelemetry Collector service. + * Details required to establish a connection to an OpenTelemetry Collector service. * * @author Eddú Meléndez * @since 3.2.0 diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java index 546503c6d9ad..c7da21f488d1 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpMetricsExportAutoConfiguration.java @@ -57,7 +57,7 @@ public class OtlpMetricsExportAutoConfiguration { } @Bean - @ConditionalOnMissingBean(OtlpMetricsConnectionDetails.class) + @ConditionalOnMissingBean OtlpMetricsConnectionDetails otlpMetricsConnectionDetails() { return new PropertiesOtlpMetricsConnectionDetails(this.properties); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConfigurations.java index eb136aac2118..492e43792d02 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConfigurations.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConfigurations.java @@ -33,16 +33,13 @@ * * @author Moritz Halbritter */ -final class OtlpTracingConfigurations { - - private OtlpTracingConfigurations() { - } +class OtlpTracingConfigurations { @Configuration(proxyBeanMethods = false) static class ConnectionDetails { @Bean - @ConditionalOnMissingBean(OtlpTracingConnectionDetails.class) + @ConditionalOnMissingBean @ConditionalOnProperty(prefix = "management.otlp.tracing", name = "endpoint") OtlpTracingConnectionDetails otlpTracingConnectionDetails(OtlpProperties properties) { return new PropertiesOtlpTracingConnectionDetails(properties); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConnectionDetails.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConnectionDetails.java index fb73b615bb87..a84b11d64da3 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConnectionDetails.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConnectionDetails.java @@ -19,7 +19,7 @@ import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; /** - * Details required to establish a connection to a OpenTelemetry service. + * Details required to establish a connection to an OpenTelemetry service. * * @author Eddú Meléndez * @since 3.2.0 @@ -27,8 +27,8 @@ public interface OtlpTracingConnectionDetails extends ConnectionDetails { /** - * Address to where metrics will be published. - * @return the address to where metrics will be published + * Address to where tracing will be published. + * @return the address to where tracing will be published */ String getUrl(); diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactory.java index 2df408c968d8..49913297040c 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactory.java @@ -23,7 +23,7 @@ /** * {@link DockerComposeConnectionDetailsFactory} to create - * {@link OtlpMetricsConnectionDetails} for a {@code OTLP} service. + * {@link OtlpMetricsConnectionDetails} for an OTLP service. * * @author Eddú Meléndez */ @@ -39,17 +39,17 @@ class OpenTelemetryMetricsDockerComposeConnectionDetailsFactory @Override protected OtlpMetricsConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { - return new OpenTelemetryContainerMetricsConnectionDetails(source.getRunningService()); + return new OpenTelemetryMetricsDockerComposeConnectionDetails(source.getRunningService()); } - private static final class OpenTelemetryContainerMetricsConnectionDetails extends DockerComposeConnectionDetails + private static final class OpenTelemetryMetricsDockerComposeConnectionDetails extends DockerComposeConnectionDetails implements OtlpMetricsConnectionDetails { private final String host; private final int port; - private OpenTelemetryContainerMetricsConnectionDetails(RunningService source) { + private OpenTelemetryMetricsDockerComposeConnectionDetails(RunningService source) { super(source); this.host = source.host(); this.port = source.ports().get(OTLP_PORT); diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactory.java index 359f4df19389..20e5b06b3daa 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactory.java @@ -23,7 +23,7 @@ /** * {@link DockerComposeConnectionDetailsFactory} to create - * {@link OtlpTracingConnectionDetails} for a {@code OTLP} service. + * {@link OtlpTracingConnectionDetails} for an OTLP service. * * @author Eddú Meléndez */ diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java index b2fa3e93e786..7f303d5082f9 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -30,7 +30,7 @@ * * @author Eddú Meléndez */ -public class OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests +class OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { OpenTelemetryMetricsDockerComposeConnectionDetailsFactoryIntegrationTests() { diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests.java index 7afae26469f1..720b90a014f2 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/otlp/OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -30,7 +30,7 @@ * * @author Eddú Meléndez */ -public class OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests +class OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { OpenTelemetryTracingDockerComposeConnectionDetailsFactoryIntegrationTests() { diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactory.java similarity index 87% rename from spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsConnectionDetailsFactory.java rename to spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactory.java index a200750e1b16..8dd417ccd1b7 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactory.java @@ -32,10 +32,10 @@ * * @author Eddú Meléndez */ -class OpenTelemetryMetricsConnectionDetailsFactory +class OpenTelemetryMetricsContainerConnectionDetailsFactory extends ContainerConnectionDetailsFactory, OtlpMetricsConnectionDetails> { - OpenTelemetryMetricsConnectionDetailsFactory() { + OpenTelemetryMetricsContainerConnectionDetailsFactory() { super("otel/opentelemetry-collector-contrib", "org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsExportAutoConfiguration"); } @@ -43,13 +43,13 @@ class OpenTelemetryMetricsConnectionDetailsFactory @Override protected OtlpMetricsConnectionDetails getContainerConnectionDetails( ContainerConnectionSource> source) { - return new OpenTelemetryContainerMetricsConnectionDetails(source); + return new OpenTelemetryMetricsContainerConnectionDetails(source); } - private static final class OpenTelemetryContainerMetricsConnectionDetails + private static final class OpenTelemetryMetricsContainerConnectionDetails extends ContainerConnectionDetails> implements OtlpMetricsConnectionDetails { - private OpenTelemetryContainerMetricsConnectionDetails(ContainerConnectionSource> source) { + private OpenTelemetryMetricsContainerConnectionDetails(ContainerConnectionSource> source) { super(source); } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingContainerConnectionDetailsFactory.java similarity index 81% rename from spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingConnectionDetailsFactory.java rename to spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingContainerConnectionDetailsFactory.java index 4f4a394cd3a8..6c3e72ac797c 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingContainerConnectionDetailsFactory.java @@ -32,10 +32,10 @@ * * @author Eddú Meléndez */ -class OpenTelemetryTracingConnectionDetailsFactory +class OpenTelemetryTracingContainerConnectionDetailsFactory extends ContainerConnectionDetailsFactory, OtlpTracingConnectionDetails> { - OpenTelemetryTracingConnectionDetailsFactory() { + OpenTelemetryTracingContainerConnectionDetailsFactory() { super("otel/opentelemetry-collector-contrib", "org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpAutoConfiguration"); } @@ -43,13 +43,13 @@ class OpenTelemetryTracingConnectionDetailsFactory @Override protected OtlpTracingConnectionDetails getContainerConnectionDetails( ContainerConnectionSource> source) { - return new OpenTelemetryTracingConnectionDetails(source); + return new OpenTelemetryTracingContainerConnectionDetails(source); } - private static final class OpenTelemetryTracingConnectionDetails extends ContainerConnectionDetails> - implements OtlpTracingConnectionDetails { + private static final class OpenTelemetryTracingContainerConnectionDetails + extends ContainerConnectionDetails> implements OtlpTracingConnectionDetails { - private OpenTelemetryTracingConnectionDetails(ContainerConnectionSource> source) { + private OpenTelemetryTracingContainerConnectionDetails(ContainerConnectionSource> source) { super(source); } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories index ae8435cb3f7e..53afedfc51e1 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories @@ -19,8 +19,8 @@ org.springframework.boot.testcontainers.service.connection.kafka.KafkaContainerC org.springframework.boot.testcontainers.service.connection.liquibase.LiquibaseContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.mongo.MongoContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.neo4j.Neo4jContainerConnectionDetailsFactory,\ -org.springframework.boot.testcontainers.service.connection.otlp.OpenTelemetryMetricsConnectionDetailsFactory,\ -org.springframework.boot.testcontainers.service.connection.otlp.OpenTelemetryTracingConnectionDetailsFactory,\ +org.springframework.boot.testcontainers.service.connection.otlp.OpenTelemetryMetricsContainerConnectionDetailsFactory,\ +org.springframework.boot.testcontainers.service.connection.otlp.OpenTelemetryTracingContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.pulsar.PulsarContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.r2dbc.MariaDbR2dbcContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.r2dbc.MySqlR2dbcContainerConnectionDetailsFactory,\ diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java similarity index 96% rename from spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsConnectionDetailsFactoryIntegrationTests.java rename to spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java index c1a88d38df5f..b1194f0ca6b0 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java @@ -48,7 +48,7 @@ import static org.hamcrest.Matchers.matchesPattern; /** - * Tests for {@link OpenTelemetryMetricsConnectionDetailsFactory}. + * Tests for {@link OpenTelemetryMetricsContainerConnectionDetailsFactory}. * * @author Eddú Meléndez * @author Jonatan Ivanov @@ -57,7 +57,7 @@ @TestPropertySource(properties = { "management.otlp.metrics.export.resource-attributes.service.name=test", "management.otlp.metrics.export.step=1s" }) @Testcontainers(disabledWithoutDocker = true) -class OpenTelemetryMetricsConnectionDetailsFactoryIntegrationTests { +class OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests { private static final String OPENMETRICS_001 = "application/openmetrics-text; version=0.0.1; charset=utf-8"; diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingContainerConnectionDetailsFactoryIntegrationTests.java similarity index 93% rename from spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingConnectionDetailsFactoryIntegrationTests.java rename to spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingContainerConnectionDetailsFactoryIntegrationTests.java index 4367967b5a78..ab41e680c555 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryTracingContainerConnectionDetailsFactoryIntegrationTests.java @@ -33,13 +33,13 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link OpenTelemetryTracingConnectionDetailsFactory}. + * Tests for {@link OpenTelemetryTracingContainerConnectionDetailsFactory}. * * @author Eddú Meléndez */ @SpringJUnitConfig @Testcontainers(disabledWithoutDocker = true) -class OpenTelemetryTracingConnectionDetailsFactoryIntegrationTests { +class OpenTelemetryTracingContainerConnectionDetailsFactoryIntegrationTests { @Container @ServiceConnection diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java index 2d005f007b84..3e6feccdaa49 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java @@ -158,8 +158,9 @@ public static DockerImageName oracleXe() { } /** - * Return a {@link DockerImageName} suitable for running the Oracle database. - * @return a docker image name for running the Oracle database + * Return a {@link DockerImageName} suitable for running OpenTelemetry. + * @return a docker image name for running OpenTelemetry + * @since 3.2.0 */ public static DockerImageName opentelemetry() { return DockerImageName.parse("otel/opentelemetry-collector-contrib").withTag(OPENTELEMETRY_VERSION); From 7b5126e009d4261fd351a0fc7c6f769d158bc357 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 12 Oct 2023 15:27:26 +0100 Subject: [PATCH 0596/1215] Upgrade to Classmate 1.6.0 Closes gh-37862 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 43903c218aaa..c409d07a6fcd 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -165,7 +165,7 @@ bom { ] } } - library("Classmate", "1.5.1") { + library("Classmate", "1.6.0") { group("com.fasterxml") { modules = [ "classmate" From 003eefee6c48af978889f6729ab23a424e469260 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 12 Oct 2023 15:27:27 +0100 Subject: [PATCH 0597/1215] Upgrade to Elasticsearch Client 8.10.3 Closes gh-37863 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c409d07a6fcd..8df45827ee02 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -264,7 +264,7 @@ bom { ] } } - library("Elasticsearch Client", "8.10.2") { + library("Elasticsearch Client", "8.10.3") { group("org.elasticsearch.client") { modules = [ "elasticsearch-rest-client" { From 6656379f35f739e774f4ce0ad23166e38bc84136 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 12 Oct 2023 15:27:28 +0100 Subject: [PATCH 0598/1215] Upgrade to Flyway 9.22.3 Closes gh-37864 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 8df45827ee02..894bfedd3b00 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -281,7 +281,7 @@ bom { ] } } - library("Flyway", "9.22.2") { + library("Flyway", "9.22.3") { group("org.flywaydb") { modules = [ "flyway-core", From c3a779c6425876249843f98e42c2a21309b897fa Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 12 Oct 2023 15:27:29 +0100 Subject: [PATCH 0599/1215] Upgrade to Infinispan 14.0.19.Final Closes gh-37865 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 894bfedd3b00..389ec48e6090 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -460,7 +460,7 @@ bom { ] } } - library("Infinispan", "14.0.17.Final") { + library("Infinispan", "14.0.19.Final") { group("org.infinispan") { imports = [ "infinispan-bom" From 72f4fcc86027400c5d000a3d8000c2170c58aa8d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 12 Oct 2023 15:27:29 +0100 Subject: [PATCH 0600/1215] Upgrade to jOOQ 3.18.7 Closes gh-37866 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 389ec48e6090..97f9339bb328 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -692,7 +692,7 @@ bom { ] } } - library("jOOQ", "3.18.6") { + library("jOOQ", "3.18.7") { group("org.jooq") { modules = [ "jooq", From 61fb23a7fe7dfe0eccb1f9dd9a9647be8e4245d3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 12 Oct 2023 15:27:30 +0100 Subject: [PATCH 0601/1215] Upgrade to Micrometer 1.12.0-RC1 Closes gh-37703 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 97f9339bb328..f7549bf16a02 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -994,7 +994,7 @@ bom { ] } } - library("Micrometer", "1.12.0-SNAPSHOT") { + library("Micrometer", "1.12.0-RC1") { considerSnapshots() group("io.micrometer") { modules = [ From e7d12e4599e21e10296a425fc0d4e2372dd0da17 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 12 Oct 2023 15:27:30 +0100 Subject: [PATCH 0602/1215] Upgrade to Micrometer Tracing 1.2.0-RC1 Closes gh-37704 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f7549bf16a02..57f8d5679f75 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1007,7 +1007,7 @@ bom { ] } } - library("Micrometer Tracing", "1.2.0-SNAPSHOT") { + library("Micrometer Tracing", "1.2.0-RC1") { considerSnapshots() calendarName = "Tracing" group("io.micrometer") { From d42512cf26093035d80719007d52460e177db42e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 12 Oct 2023 15:27:37 +0100 Subject: [PATCH 0603/1215] Upgrade to Netty 4.1.100.Final Closes gh-37867 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 57f8d5679f75..74f2fe6e2b1b 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1076,7 +1076,7 @@ bom { ] } } - library("Netty", "4.1.99.Final") { + library("Netty", "4.1.100.Final") { group("io.netty") { imports = [ "netty-bom" From b0c66c07e23a162fc4e11aa973cdb3a8246ccf9a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 12 Oct 2023 15:27:43 +0100 Subject: [PATCH 0604/1215] Upgrade to R2DBC MySQL 1.0.5 Closes gh-37868 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 74f2fe6e2b1b..37b86ecc3478 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1265,7 +1265,7 @@ bom { ] } } - library("R2DBC MySQL", "1.0.4") { + library("R2DBC MySQL", "1.0.5") { group("io.asyncer") { modules = [ "r2dbc-mysql" From 712e7690cf48c8bb8444b67b21d2f6f910142a74 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 12 Oct 2023 15:27:43 +0100 Subject: [PATCH 0605/1215] Upgrade to Reactor Bom 2023.0.0-RC1 Closes gh-37705 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 37b86ecc3478..c16458a8c8c2 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1325,7 +1325,7 @@ bom { ] } } - library("Reactor Bom", "2023.0.0-SNAPSHOT") { + library("Reactor Bom", "2023.0.0-RC1") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From 73674af547a7d17cdfb833e5f061ea1bba406e7a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 12 Oct 2023 15:27:50 +0100 Subject: [PATCH 0606/1215] Upgrade to Selenium 4.14.0 Closes gh-37869 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c16458a8c8c2..075c41cbc2b0 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1448,7 +1448,7 @@ bom { ] } } - library("Selenium", "4.13.0") { + library("Selenium", "4.14.0") { group("org.seleniumhq.selenium") { imports = [ "selenium-bom" From 1c825d841279a49c70306e9463db963aca93d3ba Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 12 Oct 2023 15:27:50 +0100 Subject: [PATCH 0607/1215] Upgrade to Spring Framework 6.1.0-RC1 Closes gh-37710 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 128505f72f32..a4d4b139bd51 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ junitJupiterVersion=5.10.0 kotlinVersion=1.9.10 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.27 -springFrameworkVersion=6.1.0-SNAPSHOT +springFrameworkVersion=6.1.0-RC1 tomcatVersion=10.1.13 kotlin.stdlib.default.dependency=false From 6880fb0fc80ad8ac992850f70146a285bb3bba0e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 12 Oct 2023 15:27:56 +0100 Subject: [PATCH 0608/1215] Upgrade to Undertow 2.3.9.Final Closes gh-37870 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 075c41cbc2b0..2abeb6c94685 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1701,7 +1701,7 @@ bom { ] } } - library("Undertow", "2.3.8.Final") { + library("Undertow", "2.3.9.Final") { group("io.undertow") { modules = [ "undertow-core", From fcf77ed65d6f79a57e177d06ebff5b3a48cf5407 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 12 Oct 2023 16:26:17 +0200 Subject: [PATCH 0609/1215] Add property to stop the JVM from exiting spring.main.keep-alive=true will spawn a non-daemon thread which stops if the context is closed Closes gh-37736 --- .../asciidoc/features/spring-application.adoc | 14 +++++ .../boot/SpringApplication.java | 59 +++++++++++++++++++ ...itional-spring-configuration-metadata.json | 7 +++ .../boot/SpringApplicationTests.java | 29 +++++++++ 4 files changed, 109 insertions(+) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc index 5255d703817f..75c199c49213 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc @@ -376,3 +376,17 @@ Spring Boot ships with the `BufferingApplicationStartup` variant; this implement Applications can ask for the bean of type `BufferingApplicationStartup` in any component. Spring Boot can also be configured to expose a {spring-boot-actuator-restapi-docs}/#startup[`startup` endpoint] that provides this information as a JSON document. + + + +[[features.spring-application.virtual-threads]] +=== Virtual threads +If you're running on Java 21 or up, you can enable virtual threads by setting the property configprop:spring.threads.virtual.enabled[] to `true`. + +WARNING: One side effect of virtual threads is that these threads are daemon threads. +A JVM will exit if there are no non-daemon threads. +This behavior can be a problem when you rely on, e.g. `@Scheduled` beans to keep your application alive. +If you use virtual threads, the scheduler thread is a virtual thread and therefore a daemon thread and won't keep the JVM alive. +This does not only affect scheduling, but can be the case with other technologies, too! +To keep the JVM running in all cases, it is recommended to set the property configprop:spring.main.keep-alive[] to `true`. +This ensures that the JVM is kept alive, even if all threads are virtual threads. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index 69f42ed0eac2..8da2a18b0e32 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -65,6 +65,7 @@ import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; import org.springframework.context.annotation.ConfigurationClassPostProcessor; import org.springframework.context.aot.AotApplicationContextInitializer; +import org.springframework.context.event.ContextClosedEvent; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.GenericTypeResolver; @@ -163,6 +164,7 @@ * @author Brian Clozel * @author Ethan Rubinson * @author Chris Bono + * @author Moritz Halbritter * @since 1.0.0 * @see #run(Class, String[]) * @see #run(Class[], String[]) @@ -240,6 +242,8 @@ public class SpringApplication { private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT; + private boolean keepAlive; + /** * Create a new {@link SpringApplication} instance. The application context will load * beans from the specified primary sources (see {@link SpringApplication class-level} @@ -409,6 +413,11 @@ private void prepareContext(DefaultBootstrapContext bootstrapContext, Configurab if (this.lazyInitialization) { context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor()); } + if (this.keepAlive) { + KeepAlive keepAlive = new KeepAlive(); + keepAlive.start(); + context.addApplicationListener(keepAlive); + } context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context)); if (!AotDetector.useGeneratedArtifacts()) { // Load the sources @@ -1277,6 +1286,26 @@ public ApplicationStartup getApplicationStartup() { return this.applicationStartup; } + /** + * Whether to keep the application alive even if there are no more non-daemon threads. + * @return whether to keep the application alive even if there are no more non-daemon + * threads + * @since 3.2.0 + */ + public boolean isKeepAlive() { + return this.keepAlive; + } + + /** + * Whether to keep the application alive even if there are no more non-daemon threads. + * @param keepAlive whether to keep the application alive even if there are no more + * non-daemon threads + * @since 3.2.0 + */ + public void setKeepAlive(boolean keepAlive) { + this.keepAlive = keepAlive; + } + /** * Return a {@link SpringApplicationShutdownHandlers} instance that can be used to add * or remove handlers that perform actions before the JVM is shutdown. @@ -1601,4 +1630,34 @@ public SpringApplicationRunListener getRunListener(SpringApplication springAppli } + /** + * A non-daemon thread to keep the JVM alive. Reacts to {@link ContextClosedEvent} to + * stop itself when the application context is closed. + */ + private static final class KeepAlive extends Thread implements ApplicationListener { + + KeepAlive() { + setName("keep-alive"); + setDaemon(false); + } + + @Override + public void onApplicationEvent(ContextClosedEvent event) { + interrupt(); + } + + @Override + public void run() { + while (true) { + try { + Thread.sleep(Long.MAX_VALUE); + } + catch (InterruptedException ex) { + break; + } + } + } + + } + } diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 12f70410dd73..3484dd4ad100 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -387,6 +387,13 @@ "type": "org.springframework.boot.cloud.CloudPlatform", "description": "Override the Cloud Platform auto-detection." }, + { + "name": "spring.main.keep-alive", + "type": "java.lang.Boolean", + "sourceType": "org.springframework.boot.SpringApplication", + "description": "Whether to keep the application alive even if there are no more non-daemon threads.", + "defaultValue": false + }, { "name": "spring.main.lazy-initialization", "type": "java.lang.Boolean", diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java index 9523dc903c27..8011aff1f2fd 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java @@ -158,6 +158,7 @@ * @author Nguyen Bao Sach * @author Chris Bono * @author Sebastien Deleuze + * @author Moritz Halbritter */ @ExtendWith(OutputCaptureExtension.class) class SpringApplicationTests { @@ -1390,6 +1391,30 @@ void fromWithMultipleApplicationsOnlyAppliesAdditionalSourcesOnce() { assertThatNoException().isThrownBy(() -> this.context.getBean(SingleUseAdditionalConfig.class)); } + @Test + void shouldStartDaemonThreadIfKeepAliveIsEnabled() { + SpringApplication application = new SpringApplication(ExampleConfig.class); + application.setWebApplicationType(WebApplicationType.NONE); + this.context = application.run("--spring.main.keep-alive=true"); + Set threads = getCurrentThreads(); + assertThat(threads).filteredOn((thread) -> thread.getName().equals("keep-alive")) + .singleElement() + .satisfies((thread) -> assertThat(thread.isDaemon()).isFalse()); + } + + @Test + void shouldStopKeepAliveThreadIfContextIsClosed() { + SpringApplication application = new SpringApplication(ExampleConfig.class); + application.setWebApplicationType(WebApplicationType.NONE); + application.setKeepAlive(true); + this.context = application.run(); + Set threadsBeforeClose = getCurrentThreads(); + assertThat(threadsBeforeClose).filteredOn((thread) -> thread.getName().equals("keep-alive")).isNotEmpty(); + this.context.close(); + Set threadsAfterClose = getCurrentThreads(); + assertThat(threadsAfterClose).filteredOn((thread) -> thread.getName().equals("keep-alive")).isEmpty(); + } + private ArgumentMatcher isAvailabilityChangeEventWithState( S state) { return (argument) -> (argument instanceof AvailabilityChangeEvent) @@ -1432,6 +1457,10 @@ public boolean matches(ConfigurableApplicationContext value) { }; } + private Set getCurrentThreads() { + return Thread.getAllStackTraces().keySet(); + } + static class TestEventListener implements SmartApplicationListener { private final Class eventType; From db50de3c1db86ad01a68d753f71ec9cfc2300f71 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 13 Oct 2023 07:43:17 +0100 Subject: [PATCH 0610/1215] Upgrade to MongoDB 4.11.0 Closes gh-37874 --- .../mongo/MongoReactiveAutoConfiguration.java | 13 ++--- .../MongoReactiveAutoConfigurationTests.java | 50 ++++++++----------- .../spring-boot-dependencies/build.gradle | 2 +- 3 files changed, 30 insertions(+), 35 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoReactiveAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoReactiveAutoConfiguration.java index 1157e002945a..5426b6f17fc9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoReactiveAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoReactiveAutoConfiguration.java @@ -18,7 +18,7 @@ import com.mongodb.MongoClientSettings; import com.mongodb.MongoClientSettings.Builder; -import com.mongodb.connection.netty.NettyStreamFactoryFactory; +import com.mongodb.connection.TransportSettings; import com.mongodb.reactivestreams.client.MongoClient; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; @@ -113,11 +113,10 @@ static final class NettyDriverMongoClientSettingsBuilderCustomizer @Override public void customize(Builder builder) { - if (!isStreamFactoryFactoryDefined(this.settings.getIfAvailable())) { + if (!isCustomTransportConfiguration(this.settings.getIfAvailable())) { NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup(); this.eventLoopGroup = eventLoopGroup; - builder - .streamFactoryFactory(NettyStreamFactoryFactory.builder().eventLoopGroup(eventLoopGroup).build()); + builder.transportSettings(TransportSettings.nettyBuilder().eventLoopGroup(eventLoopGroup).build()); } } @@ -130,8 +129,10 @@ public void destroy() { } } - private boolean isStreamFactoryFactoryDefined(MongoClientSettings settings) { - return settings != null && settings.getStreamFactoryFactory() != null; + @SuppressWarnings("deprecation") + private boolean isCustomTransportConfiguration(MongoClientSettings settings) { + return settings != null + && (settings.getTransportSettings() != null || settings.getStreamFactoryFactory() != null); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoReactiveAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoReactiveAutoConfigurationTests.java index 7f3fe1ef0b12..df46e4b45933 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoReactiveAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoReactiveAutoConfigurationTests.java @@ -23,11 +23,9 @@ import com.mongodb.MongoClientSettings; import com.mongodb.MongoCredential; import com.mongodb.ReadPreference; -import com.mongodb.connection.AsynchronousSocketChannelStreamFactoryFactory; +import com.mongodb.connection.NettyTransportSettings; import com.mongodb.connection.SslSettings; -import com.mongodb.connection.StreamFactory; -import com.mongodb.connection.StreamFactoryFactory; -import com.mongodb.connection.netty.NettyStreamFactoryFactory; +import com.mongodb.connection.TransportSettings; import com.mongodb.reactivestreams.client.MongoClient; import com.mongodb.reactivestreams.client.internal.MongoClientImpl; import io.netty.channel.EventLoopGroup; @@ -39,12 +37,8 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; /** * Tests for {@link MongoReactiveAutoConfiguration}. @@ -85,7 +79,7 @@ void settingsSslConfig() { assertThat(context).hasSingleBean(MongoClient.class); MongoClientSettings settings = getSettings(context); assertThat(settings.getApplicationName()).isEqualTo("test-config"); - assertThat(settings.getStreamFactoryFactory()).isSameAs(context.getBean("myStreamFactoryFactory")); + assertThat(settings.getTransportSettings()).isSameAs(context.getBean("myTransportSettings")); }); } @@ -212,13 +206,13 @@ void configuresCredentialsFromUriPropertyWithAuthDatabase() { } @Test - void nettyStreamFactoryFactoryIsConfiguredAutomatically() { + void nettyTransportSettingsAreConfiguredAutomatically() { AtomicReference eventLoopGroupReference = new AtomicReference<>(); this.contextRunner.run((context) -> { assertThat(context).hasSingleBean(MongoClient.class); - StreamFactoryFactory factory = getSettings(context).getStreamFactoryFactory(); - assertThat(factory).isInstanceOf(NettyStreamFactoryFactory.class); - EventLoopGroup eventLoopGroup = (EventLoopGroup) ReflectionTestUtils.getField(factory, "eventLoopGroup"); + TransportSettings transportSettings = getSettings(context).getTransportSettings(); + assertThat(transportSettings).isInstanceOf(NettyTransportSettings.class); + EventLoopGroup eventLoopGroup = ((NettyTransportSettings) transportSettings).getEventLoopGroup(); assertThat(eventLoopGroup.isShutdown()).isFalse(); eventLoopGroupReference.set(eventLoopGroup); }); @@ -226,14 +220,17 @@ void nettyStreamFactoryFactoryIsConfiguredAutomatically() { } @Test - void customizerOverridesAutoConfig() { + @SuppressWarnings("deprecation") + void customizerWithTransportSettingsOverridesAutoConfig() { this.contextRunner.withPropertyValues("spring.data.mongodb.uri:mongodb://localhost/test?appname=auto-config") - .withUserConfiguration(SimpleCustomizerConfig.class) + .withUserConfiguration(SimpleTransportSettingsCustomizerConfig.class) .run((context) -> { assertThat(context).hasSingleBean(MongoClient.class); MongoClientSettings settings = getSettings(context); - assertThat(settings.getApplicationName()).isEqualTo("overridden-name"); - assertThat(settings.getStreamFactoryFactory()).isEqualTo(SimpleCustomizerConfig.streamFactoryFactory); + assertThat(settings.getApplicationName()).isEqualTo("custom-transport-settings"); + assertThat(settings.getTransportSettings()) + .isSameAs(SimpleTransportSettingsCustomizerConfig.transportSettings); + assertThat(settings.getStreamFactoryFactory()).isNull(); }); } @@ -278,32 +275,29 @@ MongoClientSettings mongoClientSettings() { static class SslSettingsConfig { @Bean - MongoClientSettings mongoClientSettings(StreamFactoryFactory streamFactoryFactory) { + MongoClientSettings mongoClientSettings(TransportSettings transportSettings) { return MongoClientSettings.builder() .applicationName("test-config") - .streamFactoryFactory(streamFactoryFactory) + .transportSettings(transportSettings) .build(); } @Bean - StreamFactoryFactory myStreamFactoryFactory() { - StreamFactoryFactory streamFactoryFactory = mock(StreamFactoryFactory.class); - given(streamFactoryFactory.create(any(), any())).willReturn(mock(StreamFactory.class)); - return streamFactoryFactory; + TransportSettings myTransportSettings() { + return TransportSettings.nettyBuilder().build(); } } @Configuration(proxyBeanMethods = false) - static class SimpleCustomizerConfig { + static class SimpleTransportSettingsCustomizerConfig { - private static final StreamFactoryFactory streamFactoryFactory = new AsynchronousSocketChannelStreamFactoryFactory.Builder() - .build(); + private static final TransportSettings transportSettings = TransportSettings.nettyBuilder().build(); @Bean MongoClientSettingsBuilderCustomizer customizer() { - return (clientSettingsBuilder) -> clientSettingsBuilder.applicationName("overridden-name") - .streamFactoryFactory(streamFactoryFactory); + return (clientSettingsBuilder) -> clientSettingsBuilder.applicationName("custom-transport-settings") + .transportSettings(transportSettings); } } diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 2abeb6c94685..f63ba9835521 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1023,7 +1023,7 @@ bom { ] } } - library("MongoDB", "4.10.2") { + library("MongoDB", "4.11.0") { group("org.mongodb") { modules = [ "bson", From 1fa0835cf5e07ff97fc01e2864b4fbc7fe2cae35 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 13 Oct 2023 07:43:23 +0100 Subject: [PATCH 0611/1215] Upgrade to Selenium 4.14.1 Closes gh-37875 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f63ba9835521..e46ebd6fab66 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1448,7 +1448,7 @@ bom { ] } } - library("Selenium", "4.14.0") { + library("Selenium", "4.14.1") { group("org.seleniumhq.selenium") { imports = [ "selenium-bom" From c88fe3b3efd4d7eda85a4d3747fbf573ff5dedca Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 13 Oct 2023 07:43:27 +0100 Subject: [PATCH 0612/1215] Upgrade to Spring HATEOAS 2.2.0-RC1 Closes gh-37876 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e46ebd6fab66..cf242a9ea650 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1544,7 +1544,7 @@ bom { ] } } - library("Spring HATEOAS", "2.2.0-M2") { + library("Spring HATEOAS", "2.2.0-RC1") { considerSnapshots() group("org.springframework.hateoas") { modules = [ From 726cbd50b337764e730b5ca52a6a42f518579644 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 13 Oct 2023 07:43:32 +0100 Subject: [PATCH 0613/1215] Upgrade to SQLite JDBC 3.43.2.0 Closes gh-37877 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index cf242a9ea650..421274650b4c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1634,7 +1634,7 @@ bom { ] } } - library("SQLite JDBC", "3.43.0.0") { + library("SQLite JDBC", "3.43.2.0") { group("org.xerial") { modules = [ "sqlite-jdbc" From c72456a27eaa189a138da01598070a8112ace188 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 13 Oct 2023 09:51:01 +0100 Subject: [PATCH 0614/1215] Revert "Start building against Spring Security 6.2.0 snapshots" This reverts commit 4e21896b0de11d5085c1bfcc33f6ef16f228fb4d. See gh-37715 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 421274650b4c..f1dbbb4a110b 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1606,7 +1606,7 @@ bom { ] } } - library("Spring Security", "6.2.0-SNAPSHOT") { + library("Spring Security", "6.2.0-M3") { considerSnapshots() group("org.springframework.security") { imports = [ From e6b2e19628ed356856b6a9b58c12b66d8877a1ed Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 13 Oct 2023 10:24:30 +0100 Subject: [PATCH 0615/1215] Upgrade to Spring LDAP 3.2.0-RC1 Closes gh-37713 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f1dbbb4a110b..0da0a93885f1 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1569,7 +1569,7 @@ bom { ] } } - library("Spring LDAP", "3.2.0-SNAPSHOT") { + library("Spring LDAP", "3.2.0-RC1") { considerSnapshots() group("org.springframework.ldap") { modules = [ From 5397ad0822cdbe998e6a79b0e087fa01b87f70c5 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Thu, 12 Oct 2023 23:12:14 +0900 Subject: [PATCH 0616/1215] Avoid ObjectMappear creation in WebSocketMessagingAutoConfiguration See gh-37861 --- .../websocket/servlet/WebSocketMessagingAutoConfiguration.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfiguration.java index 939827774f3f..c6d24d6b4a58 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfiguration.java @@ -64,8 +64,7 @@ static class WebSocketMessageConverterConfiguration implements WebSocketMessageB @Override public boolean configureMessageConverters(List messageConverters) { - MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(); - converter.setObjectMapper(this.objectMapper); + MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(this.objectMapper); DefaultContentTypeResolver resolver = new DefaultContentTypeResolver(); resolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON); converter.setContentTypeResolver(resolver); From 932fe4fcf892d7250df43f123b5d4cc9f1f5f596 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 13 Oct 2023 10:41:07 +0100 Subject: [PATCH 0617/1215] Polish "Avoid ObjectMappear creation in WebSocketMessagingAutoConfiguration" See gh-37861 --- .../websocket/servlet/WebSocketMessagingAutoConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfiguration.java index c6d24d6b4a58..b65ed91535ed 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. From e01e4f19129ba671ddcbb388efbb2eb2f7ae7d35 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sat, 14 Oct 2023 19:43:41 -0700 Subject: [PATCH 0618/1215] Search implemented interfaces on superclass for @ServiceConnection Refine original fix to also search interfaces on the superclass. Fixes gh-37671 --- .../ServiceConnectionContextCustomizerFactory.java | 6 +++++- ...rviceConnectionContextCustomizerFactoryTests.java | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizerFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizerFactory.java index 9c991328b4dc..c54c77b04517 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizerFactory.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizerFactory.java @@ -54,7 +54,10 @@ public ContextCustomizer createContextCustomizer(Class testClass, } private void findSources(Class clazz, List> sources) { - ReflectionUtils.doWithFields(clazz, (field) -> { + if (clazz == Object.class || clazz == null) { + return; + } + ReflectionUtils.doWithLocalFields(clazz, (field) -> { MergedAnnotations annotations = MergedAnnotations.from(field); annotations.stream(ServiceConnection.class) .forEach((annotation) -> sources.add(createSource(field, annotation))); @@ -65,6 +68,7 @@ private void findSources(Class clazz, List> sour for (Class implementedInterface : clazz.getInterfaces()) { findSources(implementedInterface, sources); } + findSources(clazz.getSuperclass(), sources); } @SuppressWarnings("unchecked") diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizerFactoryTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizerFactoryTests.java index dc487d12c7e5..ba3071e8aa3d 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizerFactoryTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizerFactoryTests.java @@ -94,6 +94,14 @@ void createContextCustomizerWhenImplementedInterfaceHasServiceConnectionsReturns assertThat(customizer.getSources()).hasSize(2); } + @Test + void createContextCustomizerWhenInheritedImplementedInterfaceHasServiceConnectionsReturnsCustomizer() { + ServiceConnectionContextCustomizer customizer = (ServiceConnectionContextCustomizer) this.factory + .createContextCustomizer(ServiceConnectionsImplSubclass.class, null); + assertThat(customizer).isNotNull(); + assertThat(customizer.getSources()).hasSize(2); + } + @Test void createContextCustomizerWhenClassHasNonStaticServiceConnectionFailsWithHelpfulException() { assertThatIllegalStateException() @@ -186,6 +194,10 @@ static class ServiceConnectionsImpl implements ServiceConnectionsInterface { } + static class ServiceConnectionsImplSubclass extends ServiceConnectionsImpl { + + } + static class NonStaticServiceConnection { @ServiceConnection From efd9aa9b64cb96b6b886f9a237665151815d236c Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sat, 14 Oct 2023 19:51:42 -0700 Subject: [PATCH 0619/1215] Polish --- ...viceConnectionContextCustomizerFactory.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizerFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizerFactory.java index c54c77b04517..1f18f06c1a0d 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizerFactory.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ServiceConnectionContextCustomizerFactory.java @@ -49,26 +49,26 @@ class ServiceConnectionContextCustomizerFactory implements ContextCustomizerFact public ContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { List> sources = new ArrayList<>(); - findSources(testClass, sources); + collectSources(testClass, sources); return new ServiceConnectionContextCustomizer(sources); } - private void findSources(Class clazz, List> sources) { - if (clazz == Object.class || clazz == null) { + private void collectSources(Class candidate, List> sources) { + if (candidate == Object.class || candidate == null) { return; } - ReflectionUtils.doWithLocalFields(clazz, (field) -> { + ReflectionUtils.doWithLocalFields(candidate, (field) -> { MergedAnnotations annotations = MergedAnnotations.from(field); annotations.stream(ServiceConnection.class) .forEach((annotation) -> sources.add(createSource(field, annotation))); }); - if (TestContextAnnotationUtils.searchEnclosingClass(clazz)) { - findSources(clazz.getEnclosingClass(), sources); + if (TestContextAnnotationUtils.searchEnclosingClass(candidate)) { + collectSources(candidate.getEnclosingClass(), sources); } - for (Class implementedInterface : clazz.getInterfaces()) { - findSources(implementedInterface, sources); + for (Class implementedInterface : candidate.getInterfaces()) { + collectSources(implementedInterface, sources); } - findSources(clazz.getSuperclass(), sources); + collectSources(candidate.getSuperclass(), sources); } @SuppressWarnings("unchecked") From 1edd1d507832611d6e1352e32eb4956402c2caab Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sat, 14 Oct 2023 23:43:07 -0700 Subject: [PATCH 0620/1215] Protect against NPE when groups property is missing Closes gh-37888 --- .../properties/CheckAdditionalSpringConfigurationMetadata.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckAdditionalSpringConfigurationMetadata.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckAdditionalSpringConfigurationMetadata.java index d45b1d4e9299..6d8a7f07ec20 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckAdditionalSpringConfigurationMetadata.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CheckAdditionalSpringConfigurationMetadata.java @@ -93,7 +93,7 @@ private Report createReport() throws IOException, JsonParseException, JsonMappin @SuppressWarnings("unchecked") private void check(String key, Map json, Analysis analysis) { - List> groups = (List>) json.get(key); + List> groups = (List>) json.getOrDefault(key, Collections.emptyList()); List names = groups.stream().map((group) -> (String) group.get("name")).toList(); List sortedNames = sortedCopy(names); for (int i = 0; i < names.size(); i++) { From 4c3a0f09d785c764f1c110ffa9f01ba91da600da Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sat, 14 Oct 2023 23:44:45 -0700 Subject: [PATCH 0621/1215] Support parallel initialization of Testcontainers Add support for a `spring.testcontainers.startup` property that can be set to "sequential" or "parallel" to change how containers are started. Closes gh-37073 --- .../DocumentConfigurationProperties.java | 10 +- .../spring-boot-docs/build.gradle | 2 + .../docs/asciidoc/application-properties.adoc | 2 + .../src/docs/asciidoc/features/testing.adoc | 3 + .../spring-boot-testcontainers/build.gradle | 1 + ...ifecycleApplicationContextInitializer.java | 3 +- ...tcontainersLifecycleBeanPostProcessor.java | 41 ++++-- .../lifecycle/TestcontainersStartup.java | 94 ++++++++++++++ ...itional-spring-configuration-metadata.json | 10 ++ ...cleApplicationContextInitializerTests.java | 21 +++ .../lifecycle/TestcontainersStartupTests.java | 122 ++++++++++++++++++ 11 files changed, 293 insertions(+), 16 deletions(-) create mode 100644 spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersStartup.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/additional-spring-configuration-metadata.json create mode 100644 spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersStartupTests.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 cedb706ea076..6245d5b9da5e 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 @@ -78,8 +78,10 @@ void documentConfigurationProperties() throws IOException { snippets.add("application-properties.security", "Security Properties", this::securityPrefixes); snippets.add("application-properties.rsocket", "RSocket Properties", this::rsocketPrefixes); snippets.add("application-properties.actuator", "Actuator Properties", this::actuatorPrefixes); - snippets.add("application-properties.docker-compose", "Docker Compose Properties", this::dockerComposePrefixes); snippets.add("application-properties.devtools", "Devtools Properties", this::devtoolsPrefixes); + snippets.add("application-properties.docker-compose", "Docker Compose Properties", this::dockerComposePrefixes); + snippets.add("application-properties.testcontainers", "Testcontainers Properties", + this::testcontainersPrefixes); snippets.add("application-properties.testing", "Testing Properties", this::testingPrefixes); snippets.writeTo(this.outputDir.toPath()); } @@ -224,7 +226,11 @@ private void devtoolsPrefixes(Config prefix) { } private void testingPrefixes(Config prefix) { - prefix.accept("spring.test"); + prefix.accept("spring.test."); + } + + private void testcontainersPrefixes(Config prefix) { + prefix.accept("spring.testcontainers."); } } diff --git a/spring-boot-project/spring-boot-docs/build.gradle b/spring-boot-project/spring-boot-docs/build.gradle index f2eac01891d9..5cbee491b503 100644 --- a/spring-boot-project/spring-boot-docs/build.gradle +++ b/spring-boot-project/spring-boot-docs/build.gradle @@ -61,6 +61,7 @@ dependencies { asciidoctorExtensions(project(path: ":spring-boot-project:spring-boot-autoconfigure")) asciidoctorExtensions(project(path: ":spring-boot-project:spring-boot-devtools")) asciidoctorExtensions(project(path: ":spring-boot-project:spring-boot-docker-compose")) + asciidoctorExtensions(project(path: ":spring-boot-project:spring-boot-testcontainers")) autoConfiguration(project(path: ":spring-boot-project:spring-boot-autoconfigure", configuration: "autoConfigurationMetadata")) autoConfiguration(project(path: ":spring-boot-project:spring-boot-actuator-autoconfigure", configuration: "autoConfigurationMetadata")) @@ -74,6 +75,7 @@ dependencies { configurationProperties(project(path: ":spring-boot-project:spring-boot-docker-compose", configuration: "configurationPropertiesMetadata")) configurationProperties(project(path: ":spring-boot-project:spring-boot-devtools", configuration: "configurationPropertiesMetadata")) configurationProperties(project(path: ":spring-boot-project:spring-boot-test-autoconfigure", configuration: "configurationPropertiesMetadata")) + configurationProperties(project(path: ":spring-boot-project:spring-boot-testcontainers", configuration: "configurationPropertiesMetadata")) gradlePluginDocumentation(project(path: ":spring-boot-project:spring-boot-tools:spring-boot-gradle-plugin", configuration: "documentation")) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/application-properties.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/application-properties.adoc index 7c51da9f53fc..ab3b79f7eb71 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/application-properties.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/application-properties.adoc @@ -47,4 +47,6 @@ include::application-properties/devtools.adoc[] include::application-properties/docker-compose.adoc[] +include::application-properties/testcontainers.adoc[] + include::application-properties/testing.adoc[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc index 61923636363f..ea0400bdf569 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc @@ -1072,6 +1072,9 @@ include::code:test/MyContainersConfiguration[] NOTE: The lifecycle of `Container` beans is automatically managed by Spring Boot. Containers will be started and stopped automatically. +TIP: You can use the configprop:spring.testcontainers.startup[] property to change how containers are started. +By default `sequential` startup is used, but you may also choose `parallel` if you wish to start multiple containers in parallel. + Once you have defined your test configuration, you can use the `with(...)` method to attach it to your test launcher: include::code:test/TestMyApplication[] diff --git a/spring-boot-project/spring-boot-testcontainers/build.gradle b/spring-boot-project/spring-boot-testcontainers/build.gradle index a0f64a191d88..de0546e45075 100644 --- a/spring-boot-project/spring-boot-testcontainers/build.gradle +++ b/spring-boot-project/spring-boot-testcontainers/build.gradle @@ -1,6 +1,7 @@ plugins { id "java-library" id "org.springframework.boot.auto-configuration" + id "org.springframework.boot.configuration-properties" id "org.springframework.boot.conventions" id "org.springframework.boot.deployed" id "org.springframework.boot.optional-dependencies" diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleApplicationContextInitializer.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleApplicationContextInitializer.java index 087c4d71e605..41420bbb6f42 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleApplicationContextInitializer.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleApplicationContextInitializer.java @@ -47,7 +47,8 @@ public void initialize(ConfigurableApplicationContext applicationContext) { } ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory(); applicationContext.addBeanFactoryPostProcessor(new TestcontainersLifecycleBeanFactoryPostProcessor()); - beanFactory.addBeanPostProcessor(new TestcontainersLifecycleBeanPostProcessor(beanFactory)); + TestcontainersStartup startup = TestcontainersStartup.get(applicationContext.getEnvironment()); + beanFactory.addBeanPostProcessor(new TestcontainersLifecycleBeanPostProcessor(beanFactory, startup)); } } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java index 9a5ec1d10aff..7033b961a298 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java @@ -16,9 +16,11 @@ package org.springframework.boot.testcontainers.lifecycle; +import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -58,48 +60,61 @@ class TestcontainersLifecycleBeanPostProcessor implements DestructionAwareBeanPo private final ConfigurableListableBeanFactory beanFactory; + private final TestcontainersStartup startup; + private volatile boolean containersInitialized = false; - TestcontainersLifecycleBeanPostProcessor(ConfigurableListableBeanFactory beanFactory) { + TestcontainersLifecycleBeanPostProcessor(ConfigurableListableBeanFactory beanFactory, + TestcontainersStartup startup) { this.beanFactory = beanFactory; + this.startup = startup; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof Startable startable) { - startable.start(); - } - if (this.beanFactory.isConfigurationFrozen()) { + if (!this.containersInitialized && this.beanFactory.isConfigurationFrozen()) { initializeContainers(); } return bean; } private void initializeContainers() { - if (this.containersInitialized) { - return; - } - this.containersInitialized = true; Set beanNames = new LinkedHashSet<>(); beanNames.addAll(List.of(this.beanFactory.getBeanNamesForType(ContainerState.class, false, false))); beanNames.addAll(List.of(this.beanFactory.getBeanNamesForType(Startable.class, false, false))); + initializeContainers(beanNames); + } + + private void initializeContainers(Set beanNames) { + List beans = new ArrayList<>(beanNames.size()); for (String beanName : beanNames) { try { - this.beanFactory.getBean(beanName); + beans.add(this.beanFactory.getBean(beanName)); } catch (BeanCreationException ex) { if (ex.contains(BeanCurrentlyInCreationException.class)) { - this.containersInitialized = false; return; } throw ex; } } - if (!beanNames.isEmpty()) { - logger.debug(LogMessage.format("Initialized container beans '%s'", beanNames)); + if (!this.containersInitialized) { + this.containersInitialized = true; + if (!beanNames.isEmpty()) { + logger.debug(LogMessage.format("Initialized container beans '%s'", beanNames)); + } + start(beans); } } + private void start(List beans) { + Set startables = beans.stream() + .filter(Startable.class::isInstance) + .map(Startable.class::cast) + .collect(Collectors.toCollection(LinkedHashSet::new)); + this.startup.start(startables); + } + @Override public boolean requiresDestruction(Object bean) { return bean instanceof Startable; diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersStartup.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersStartup.java new file mode 100644 index 000000000000..4554dbc83be7 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersStartup.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-2023 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.testcontainers.lifecycle; + +import java.util.Collection; + +import org.testcontainers.lifecycle.Startable; +import org.testcontainers.lifecycle.Startables; + +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; + +/** + * Testcontainers startup strategies. The strategy to use can be configured in the Spring + * {@link Environment} with a {@value #PROPERTY} property. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public enum TestcontainersStartup { + + /** + * Startup containers sequentially. + */ + SEQUENTIAL { + + @Override + void start(Collection startables) { + startables.forEach(Startable::start); + } + + }, + + /** + * Startup containers in parallel. + */ + PARALLEL { + + @Override + void start(Collection startables) { + Startables.deepStart(startables).join(); + } + + }; + + /** + * The {@link Environment} property used to change the {@link TestcontainersStartup} + * strategy. + */ + public static final String PROPERTY = "spring.testcontainers.startup"; + + abstract void start(Collection startables); + + static TestcontainersStartup get(ConfigurableEnvironment environment) { + return get((environment != null) ? environment.getProperty(PROPERTY) : null); + } + + private static TestcontainersStartup get(String value) { + if (value == null) { + return SEQUENTIAL; + } + String canonicalName = getCanonicalName(value); + for (TestcontainersStartup candidate : values()) { + if (candidate.name().equalsIgnoreCase(canonicalName)) { + return candidate; + } + } + throw new IllegalArgumentException("Unknown '%s' property value '%s'".formatted(PROPERTY, value)); + } + + private static String getCanonicalName(String name) { + StringBuilder canonicalName = new StringBuilder(name.length()); + name.chars() + .filter(Character::isLetterOrDigit) + .map(Character::toLowerCase) + .forEach((c) -> canonicalName.append((char) c)); + return canonicalName.toString(); + } + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 000000000000..708bb7854907 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,10 @@ +{ + "properties": [ + { + "name": "spring.testcontainers.startup", + "type": "org.springframework.boot.testcontainers.lifecycle.TestcontainersStartup", + "description": "Testcontainers startup modes.", + "defaultValue": "sequential" + } + ] +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleApplicationContextInitializerTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleApplicationContextInitializerTests.java index ee27dae34f70..dca0b39d1dc1 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleApplicationContextInitializerTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleApplicationContextInitializerTests.java @@ -16,13 +16,18 @@ package org.springframework.boot.testcontainers.lifecycle; +import java.util.Map; + import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.lifecycle.Startable; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.support.AbstractBeanFactory; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.MapPropertySource; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -104,6 +109,22 @@ void dealsWithBeanCurrentlyInCreationException() { applicationContext.refresh(); } + @Test + void setupStartupBasedOnEnvironmentProperty() { + AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + applicationContext.getEnvironment() + .getPropertySources() + .addLast(new MapPropertySource("test", Map.of("spring.testcontainers.startup", "parallel"))); + new TestcontainersLifecycleApplicationContextInitializer().initialize(applicationContext); + AbstractBeanFactory beanFactory = (AbstractBeanFactory) applicationContext.getBeanFactory(); + BeanPostProcessor beanPostProcessor = beanFactory.getBeanPostProcessors() + .stream() + .filter(TestcontainersLifecycleBeanPostProcessor.class::isInstance) + .findFirst() + .get(); + assertThat(beanPostProcessor).extracting("startup").isEqualTo(TestcontainersStartup.PARALLEL); + } + private AnnotationConfigApplicationContext createApplicationContext(Startable container) { AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); new TestcontainersLifecycleApplicationContextInitializer().initialize(applicationContext); diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersStartupTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersStartupTests.java new file mode 100644 index 000000000000..fd88210ccaba --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersStartupTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2012-2023 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.testcontainers.lifecycle; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; +import org.testcontainers.lifecycle.Startable; + +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link TestcontainersStartup}. + * + * @author Phillip Webb + */ +class TestcontainersStartupTests { + + private static final String PROPERTY = TestcontainersStartup.PROPERTY; + + private final AtomicInteger counter = new AtomicInteger(); + + @Test + void startWhenSquentialStartsSequentially() { + List startables = createTestStartables(100); + TestcontainersStartup.SEQUENTIAL.start(startables); + for (int i = 0; i < startables.size(); i++) { + assertThat(startables.get(i).getIndex()).isEqualTo(i); + assertThat(startables.get(i).getThreadName()).isEqualTo(Thread.currentThread().getName()); + } + } + + @Test + void startWhenParallelStartsInParallel() { + List startables = createTestStartables(100); + TestcontainersStartup.PARALLEL.start(startables); + assertThat(startables.stream().map(TestStartable::getThreadName)).hasSizeGreaterThan(1); + } + + @Test + void getWhenNoPropertyReturnsDefault() { + MockEnvironment environment = new MockEnvironment(); + assertThat(TestcontainersStartup.get(environment)).isEqualTo(TestcontainersStartup.SEQUENTIAL); + } + + @Test + void getWhenPropertyReturnsBasedOnValue() { + MockEnvironment environment = new MockEnvironment(); + assertThat(TestcontainersStartup.get(environment.withProperty(PROPERTY, "SEQUENTIAL"))) + .isEqualTo(TestcontainersStartup.SEQUENTIAL); + assertThat(TestcontainersStartup.get(environment.withProperty(PROPERTY, "sequential"))) + .isEqualTo(TestcontainersStartup.SEQUENTIAL); + assertThat(TestcontainersStartup.get(environment.withProperty(PROPERTY, "SEQuenTIaL"))) + .isEqualTo(TestcontainersStartup.SEQUENTIAL); + assertThat(TestcontainersStartup.get(environment.withProperty(PROPERTY, "S-E-Q-U-E-N-T-I-A-L"))) + .isEqualTo(TestcontainersStartup.SEQUENTIAL); + assertThat(TestcontainersStartup.get(environment.withProperty(PROPERTY, "parallel"))) + .isEqualTo(TestcontainersStartup.PARALLEL); + } + + @Test + void getWhenUnknownPropertyThrowsException() { + MockEnvironment environment = new MockEnvironment(); + assertThatIllegalArgumentException() + .isThrownBy(() -> TestcontainersStartup.get(environment.withProperty(PROPERTY, "bad"))) + .withMessage("Unknown 'spring.testcontainers.startup' property value 'bad'"); + } + + private List createTestStartables(int size) { + List testStartables = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + testStartables.add(new TestStartable()); + } + return testStartables; + } + + private class TestStartable implements Startable { + + private int index; + + private String threadName; + + @Override + public void start() { + this.index = TestcontainersStartupTests.this.counter.getAndIncrement(); + this.threadName = Thread.currentThread().getName(); + } + + @Override + public void stop() { + } + + int getIndex() { + return this.index; + } + + String getThreadName() { + return this.threadName; + } + + } + +} From 339f75d309c0a70f5ce8ac093255505905423255 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 16 Oct 2023 12:11:31 +0200 Subject: [PATCH 0622/1215] Fix GraphQL WebSocket HandlerMapping bean ordering Prior to this commit, the GraphQL WebSocket HandlerMapping bean would be ordered at position "2", before the RouterFunction variant defined by Spring Framework at position "3". Since then, the Spring Framework team changed the default order value for this one at "-1", see spring-projects/spring-framework#30278. This prevents the WebSocket upgrade, as the request is handled by the RouterFunction instead of the WebSocket handler. This commit updates the handlermapping order and introduces a test to prevent issues in the future. Fixes gh-37892 --- .../servlet/GraphQlWebMvcAutoConfiguration.java | 2 +- .../servlet/GraphQlWebMvcAutoConfigurationTests.java | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfiguration.java index 29d0c53c248f..4c82ba3b5126 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfiguration.java @@ -195,7 +195,7 @@ public HandlerMapping graphQlWebSocketMapping(GraphQlWebSocketHandler handler, G mapping.setWebSocketUpgradeMatch(true); mapping.setUrlMap(Collections.singletonMap(path, handler.initWebSocketHttpRequestHandler(new DefaultHandshakeHandler()))); - mapping.setOrder(2); // Ahead of HTTP endpoint ("routerFunctionMapping" bean) + mapping.setOrder(-2); // Ahead of HTTP endpoint ("routerFunctionMapping" bean) return mapping; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfigurationTests.java index e99c778746a8..6df0fdff742a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfigurationTests.java @@ -45,7 +45,11 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.support.RouterFunctionMapping; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.socket.server.support.WebSocketHandlerMapping; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; @@ -162,8 +166,12 @@ void shouldSupportCors() { @Test void shouldConfigureWebSocketBeans() { - this.contextRunner.withPropertyValues("spring.graphql.websocket.path=/ws") - .run((context) -> assertThat(context).hasSingleBean(GraphQlWebSocketHandler.class)); + this.contextRunner.withPropertyValues("spring.graphql.websocket.path=/ws").run((context) -> { + assertThat(context).hasSingleBean(GraphQlWebSocketHandler.class); + assertThat(context.getBeanProvider(HandlerMapping.class).orderedStream().toList()).containsSubsequence( + context.getBean(WebSocketHandlerMapping.class), context.getBean(RouterFunctionMapping.class), + context.getBean(RequestMappingHandlerMapping.class)); + }); } @Test From 0e3a196af5c58954cfa7d3920b5782125ef6a1ff Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Oct 2023 11:27:01 +0100 Subject: [PATCH 0623/1215] Fix binding of classpath*: to resource arrays and collections Fixes gh-15835 --- .../properties/bind/BindConverter.java | 20 +++++-- .../ConfigurationPropertiesTests.java | 57 +++++++++++++++++++ 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java index cdabe1d990d5..d98c31135c7a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -42,6 +42,7 @@ import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalGenericConverter; import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.core.io.Resource; import org.springframework.util.CollectionUtils; /** @@ -154,8 +155,8 @@ private static class ResolvableTypeDescriptor extends TypeDescriptor { private static class TypeConverterConversionService extends GenericConversionService { TypeConverterConversionService(Consumer initializer) { - addConverter(new TypeConverterConverter(initializer)); ApplicationConversionService.addDelimitedStringConverters(this); + addConverter(new TypeConverterConverter(initializer)); } @Override @@ -196,16 +197,23 @@ private static class TypeConverterConverter implements ConditionalGenericConvert @Override public Set getConvertibleTypes() { - return Collections.singleton(new ConvertiblePair(String.class, Object.class)); + return Set.of(new ConvertiblePair(String.class, Object.class), + new ConvertiblePair(String.class, Object[].class), + new ConvertiblePair(String.class, Collection.class)); } @Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { Class type = targetType.getType(); - if (type == null || type == Object.class || Collection.class.isAssignableFrom(type) - || Map.class.isAssignableFrom(type)) { + if (type == null || type == Object.class || Map.class.isAssignableFrom(type)) { return false; } + if (Collection.class.isAssignableFrom(type)) { + TypeDescriptor elementType = targetType.getElementTypeDescriptor(); + if (elementType == null || (!Resource.class.isAssignableFrom(elementType.getType()))) { + return false; + } + } PropertyEditor editor = this.matchesOnlyTypeConverter.getDefaultEditor(type); if (editor == null) { editor = this.matchesOnlyTypeConverter.findCustomEditor(type, null); @@ -218,7 +226,7 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { @Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { - return createTypeConverter().convertIfNecessary(source, targetType.getType()); + return createTypeConverter().convertIfNecessary(source, targetType.getType(), targetType); } private SimpleTypeConverter createTypeConverter() { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java index 3ae3dd2c2a7b..9f7c97f7a317 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java @@ -23,6 +23,7 @@ import java.time.Period; import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; @@ -1173,6 +1174,22 @@ void loadWhenPotentiallyConstructorBoundPropertiesAreImportedUsesJavaBeanBinding assertThat(properties.getProp()).isEqualTo("alpha"); } + @Test + void loadWhenBindingClasspathPatternToResourceArrayShouldBindMultipleValues() { + load(ResourceArrayPropertiesConfiguration.class, + "test.resources=classpath*:org/springframework/boot/context/properties/*.class"); + ResourceArrayProperties properties = this.context.getBean(ResourceArrayProperties.class); + assertThat(properties.getResources()).hasSizeGreaterThan(1); + } + + @Test + void loadWhenBindingClasspathPatternToResourceCollectionShouldBindMultipleValues() { + load(ResourceCollectionPropertiesConfiguration.class, + "test.resources=classpath*:org/springframework/boot/context/properties/*.class"); + ResourceCollectionProperties properties = this.context.getBean(ResourceCollectionProperties.class); + assertThat(properties.getResources()).hasSizeGreaterThan(1); + } + private AnnotationConfigApplicationContext load(Class configuration, String... inlinedProperties) { return load(new Class[] { configuration }, inlinedProperties); } @@ -3058,4 +3075,44 @@ void setProp(String prop) { } + @EnableConfigurationProperties(ResourceArrayProperties.class) + static class ResourceArrayPropertiesConfiguration { + + } + + @ConfigurationProperties("test") + static class ResourceArrayProperties { + + private Resource[] resources; + + Resource[] getResources() { + return this.resources; + } + + void setResources(Resource[] resources) { + this.resources = resources; + } + + } + + @EnableConfigurationProperties(ResourceCollectionProperties.class) + static class ResourceCollectionPropertiesConfiguration { + + } + + @ConfigurationProperties("test") + static class ResourceCollectionProperties { + + private Collection resources; + + Collection getResources() { + return this.resources; + } + + void setResources(Collection resources) { + this.resources = resources; + } + + } + } From 204cfce04c433048918eacdc78b0f1e407afe83b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Oct 2023 12:27:44 +0100 Subject: [PATCH 0624/1215] Upgrade to Dropwizard Metrics 4.2.21 Closes gh-37899 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 0da0a93885f1..5176bb109667 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -242,7 +242,7 @@ bom { ] } } - library("Dropwizard Metrics", "4.2.20") { + library("Dropwizard Metrics", "4.2.21") { group("io.dropwizard.metrics") { imports = [ "metrics-bom" From f30c9846638661180e0896d6e889c017a01e3d87 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Oct 2023 12:27:49 +0100 Subject: [PATCH 0625/1215] Upgrade to Jackson Bom 2.15.3 Closes gh-37900 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a4d4b139bd51..2a284069bd57 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 assertjVersion=3.24.2 commonsCodecVersion=1.16.0 hamcrestVersion=2.2 -jacksonVersion=2.15.2 +jacksonVersion=2.15.3 junitJupiterVersion=5.10.0 kotlinVersion=1.9.10 mavenVersion=3.9.4 From 5970f29fd58197e4d53d1e498ffc9fdb3059a9a7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Oct 2023 12:27:50 +0100 Subject: [PATCH 0626/1215] Upgrade to Spring Data Bom 2023.1.0-RC1 Closes gh-37709 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5176bb109667..728938d7ae4c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1517,7 +1517,7 @@ bom { ] } } - library("Spring Data Bom", "2023.1.0-SNAPSHOT") { + library("Spring Data Bom", "2023.1.0-RC1") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From 92cd85002de36199e6039778889460f64bcc67dd Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Oct 2023 13:58:18 +0100 Subject: [PATCH 0627/1215] Upgrade to Spring Retry 2.0.4 Closes gh-37714 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 728938d7ae4c..e4ab5ec66a1a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1598,7 +1598,7 @@ bom { ] } } - library("Spring Retry", "2.0.4-SNAPSHOT") { + library("Spring Retry", "2.0.4") { considerSnapshots() group("org.springframework.retry") { modules = [ From 6f5688ad3e10cfc685f4d5f63714e3cb2f575a14 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Oct 2023 13:58:23 +0100 Subject: [PATCH 0628/1215] Upgrade to Tomcat 10.1.15 Closes gh-37903 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2a284069bd57..a520f2c15434 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,6 +13,6 @@ kotlinVersion=1.9.10 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.27 springFrameworkVersion=6.1.0-RC1 -tomcatVersion=10.1.13 +tomcatVersion=10.1.15 kotlin.stdlib.default.dependency=false From 19fd88b25b651441f9bb09d1c846d79aff0e3552 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Oct 2023 14:07:39 +0100 Subject: [PATCH 0629/1215] Implement SSL hot reload for Netty and Tomcat Closes gh-37808 --- .../boot/autoconfigure/ssl/FileWatcher.java | 229 ++++++++++++++++++ .../ssl/SslAutoConfiguration.java | 21 +- .../ssl/SslBundleProperties.java | 15 +- .../boot/autoconfigure/ssl/SslProperties.java | 41 ++++ .../ssl/SslPropertiesBundleRegistrar.java | 85 ++++++- .../autoconfigure/ssl/FileWatcherTests.java | 200 +++++++++++++++ .../SslPropertiesBundleRegistrarTests.java | 172 +++++++++++++ .../src/docs/asciidoc/features/ssl.adoc | 30 +++ .../netty/NettyRSocketServerFactory.java | 5 +- .../boot/ssl/DefaultSslBundleRegistry.java | 70 +++++- .../boot/ssl/SslBundleRegistry.java | 10 + .../springframework/boot/ssl/SslBundles.java | 16 +- .../netty/NettyReactiveWebServerFactory.java | 9 +- .../web/embedded/netty/NettyWebServer.java | 6 +- .../embedded/netty/SslServerCustomizer.java | 66 ++++- .../tomcat/SslConnectorCustomizer.java | 65 ++--- .../TomcatReactiveWebServerFactory.java | 12 +- .../tomcat/TomcatServletWebServerFactory.java | 12 +- .../AbstractConfigurableWebServerFactory.java | 9 + .../ssl/DefaultSslBundleRegistryTests.java | 48 +++- .../boot/ssl/pem/PemSslStoreBundleTests.java | 66 +++++ .../NettyReactiveWebServerFactoryTests.java | 38 ++- .../tomcat/SslConnectorCustomizerTests.java | 52 ++-- .../TomcatServletWebServerFactoryTests.java | 55 +++++ .../AbstractServletWebServerFactoryTests.java | 9 +- .../boot/web/embedded/netty/1.crt | 9 + .../boot/web/embedded/netty/1.key | 3 + .../boot/web/embedded/netty/2.crt | 9 + .../boot/web/embedded/netty/2.key | 3 + .../boot/web/embedded/tomcat/1.crt | 9 + .../boot/web/embedded/tomcat/1.key | 3 + .../boot/web/embedded/tomcat/2.crt | 9 + .../boot/web/embedded/tomcat/2.key | 3 + 33 files changed, 1285 insertions(+), 104 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/FileWatcher.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/FileWatcherTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrarTests.java create mode 100644 spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/netty/1.crt create mode 100644 spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/netty/1.key create mode 100644 spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/netty/2.crt create mode 100644 spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/netty/2.key create mode 100644 spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/tomcat/1.crt create mode 100644 spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/tomcat/1.key create mode 100644 spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/tomcat/2.crt create mode 100644 spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/tomcat/2.key diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/FileWatcher.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/FileWatcher.java new file mode 100644 index 000000000000..eecad97b3b4e --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/FileWatcher.java @@ -0,0 +1,229 @@ +/* + * Copyright 2012-2023 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.ssl; + +import java.io.Closeable; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.ClosedWatchServiceException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.time.Duration; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.log.LogMessage; +import org.springframework.util.Assert; + +/** + * Watches files and directories and triggers a callback on change. + * + * @author Moritz Halbritter + * @author Phillip Webb + */ +class FileWatcher implements Closeable { + + private static final Log logger = LogFactory.getLog(FileWatcher.class); + + private final Duration quietPeriod; + + private final Object lock = new Object(); + + private WatcherThread thread; + + /** + * Create a new {@link FileWatcher} instance. + * @param quietPeriod the duration that no file changes should occur before triggering + * actions + */ + FileWatcher(Duration quietPeriod) { + Assert.notNull(quietPeriod, "QuietPeriod must not be null"); + this.quietPeriod = quietPeriod; + } + + /** + * Watch the given files or directories for changes. + * @param paths the files or directories to watch + * @param action the action to take when changes are detected + */ + void watch(Set paths, Runnable action) { + Assert.notNull(paths, "Paths must not be null"); + Assert.notNull(action, "Action must not be null"); + if (paths.isEmpty()) { + return; + } + synchronized (this.lock) { + try { + if (this.thread == null) { + this.thread = new WatcherThread(); + this.thread.start(); + } + this.thread.register(new Registration(paths, action)); + } + catch (IOException ex) { + throw new UncheckedIOException("Failed to register paths for watching: " + paths, ex); + } + } + } + + @Override + public void close() throws IOException { + synchronized (this.lock) { + if (this.thread != null) { + this.thread.close(); + this.thread.interrupt(); + try { + this.thread.join(); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + this.thread = null; + } + } + } + + /** + * The watcher thread used to check for changes. + */ + private class WatcherThread extends Thread implements Closeable { + + private final WatchService watchService = FileSystems.getDefault().newWatchService(); + + private final Map> registrations = new ConcurrentHashMap<>(); + + private volatile boolean running = true; + + WatcherThread() throws IOException { + setName("ssl-bundle-watcher"); + setDaemon(true); + setUncaughtExceptionHandler(this::onThreadException); + } + + private void onThreadException(Thread thread, Throwable throwable) { + logger.error("Uncaught exception in file watcher thread", throwable); + } + + void register(Registration registration) throws IOException { + for (Path path : registration.paths()) { + if (!Files.isRegularFile(path) && !Files.isDirectory(path)) { + throw new IOException("'%s' is neither a file nor a directory".formatted(path)); + } + Path directory = Files.isDirectory(path) ? path : path.getParent(); + WatchKey watchKey = register(directory); + this.registrations.computeIfAbsent(watchKey, (key) -> new CopyOnWriteArrayList<>()).add(registration); + } + } + + private WatchKey register(Path directory) throws IOException { + logger.debug(LogMessage.format("Registering '%s'", directory)); + return directory.register(this.watchService, StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE); + } + + @Override + public void run() { + logger.debug("Watch thread started"); + Set actions = new HashSet<>(); + while (this.running) { + try { + long timeout = FileWatcher.this.quietPeriod.toMillis(); + WatchKey key = this.watchService.poll(timeout, TimeUnit.MILLISECONDS); + if (key == null) { + actions.forEach(this::runSafely); + actions.clear(); + } + else { + accumulate(key, actions); + key.reset(); + } + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + catch (ClosedWatchServiceException ex) { + logger.debug("File watcher has been closed"); + this.running = false; + } + } + logger.debug("Watch thread stopped"); + } + + private void runSafely(Runnable action) { + try { + action.run(); + } + catch (Throwable ex) { + logger.error("Unexpected SSL reload error", ex); + } + } + + private void accumulate(WatchKey key, Set actions) { + List registrations = this.registrations.get(key); + Path directory = (Path) key.watchable(); + for (WatchEvent event : key.pollEvents()) { + Path file = directory.resolve((Path) event.context()); + for (Registration registration : registrations) { + if (registration.manages(file)) { + actions.add(registration.action()); + } + } + } + } + + @Override + public void close() throws IOException { + this.running = false; + this.watchService.close(); + } + + } + + /** + * An individual watch registration. + */ + private record Registration(Set paths, Runnable action) { + + Registration { + paths = paths.stream().map(Path::toAbsolutePath).collect(Collectors.toSet()); + } + + boolean manages(Path file) { + Path absolutePath = file.toAbsolutePath(); + return this.paths.contains(absolutePath) || isInDirectories(absolutePath); + } + + private boolean isInDirectories(Path file) { + return this.paths.stream().filter(Files::isDirectory).anyMatch(file::startsWith); + } + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java index 12b856c8a01d..1348f16b37b8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslAutoConfiguration.java @@ -16,8 +16,7 @@ package org.springframework.boot.autoconfigure.ssl; -import java.util.List; - +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -37,19 +36,27 @@ @EnableConfigurationProperties(SslProperties.class) public class SslAutoConfiguration { - SslAutoConfiguration() { + private final SslProperties sslProperties; + + SslAutoConfiguration(SslProperties sslProperties) { + this.sslProperties = sslProperties; + } + + @Bean + FileWatcher fileWatcher() { + return new FileWatcher(this.sslProperties.getBundle().getWatch().getFile().getQuietPeriod()); } @Bean - public SslPropertiesBundleRegistrar sslPropertiesSslBundleRegistrar(SslProperties sslProperties) { - return new SslPropertiesBundleRegistrar(sslProperties); + SslPropertiesBundleRegistrar sslPropertiesSslBundleRegistrar(FileWatcher fileWatcher) { + return new SslPropertiesBundleRegistrar(this.sslProperties, fileWatcher); } @Bean @ConditionalOnMissingBean({ SslBundleRegistry.class, SslBundles.class }) - public DefaultSslBundleRegistry sslBundleRegistry(List sslBundleRegistrars) { + DefaultSslBundleRegistry sslBundleRegistry(ObjectProvider sslBundleRegistrars) { DefaultSslBundleRegistry registry = new DefaultSslBundleRegistry(); - sslBundleRegistrars.forEach((registrar) -> registrar.registerBundles(registry)); + sslBundleRegistrars.orderedStream().forEach((registrar) -> registrar.registerBundles(registry)); return registry; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslBundleProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslBundleProperties.java index e8b9fd1a4cba..b01201dba07e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslBundleProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslBundleProperties.java @@ -36,7 +36,7 @@ public abstract class SslBundleProperties { private final Key key = new Key(); /** - * Options for the SLL connection. + * Options for the SSL connection. */ private final Options options = new Options(); @@ -45,6 +45,11 @@ public abstract class SslBundleProperties { */ private String protocol = SslBundle.DEFAULT_PROTOCOL; + /** + * Whether to reload the SSL bundle. + */ + private boolean reloadOnUpdate; + public Key getKey() { return this.key; } @@ -61,6 +66,14 @@ public void setProtocol(String protocol) { this.protocol = protocol; } + public boolean isReloadOnUpdate() { + return this.reloadOnUpdate; + } + + public void setReloadOnUpdate(boolean reloadOnUpdate) { + this.reloadOnUpdate = reloadOnUpdate; + } + public static class Options { /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslProperties.java index 49aced749021..a755a871b8d7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslProperties.java @@ -16,6 +16,7 @@ package org.springframework.boot.autoconfigure.ssl; +import java.time.Duration; import java.util.LinkedHashMap; import java.util.Map; @@ -25,6 +26,7 @@ * Properties for centralized SSL trust material configuration. * * @author Scott Frederick + * @author Moritz Halbritter * @since 3.1.0 */ @ConfigurationProperties(prefix = "spring.ssl") @@ -54,6 +56,11 @@ public static class Bundles { */ private final Map jks = new LinkedHashMap<>(); + /** + * Trust material watching. + */ + private final Watch watch = new Watch(); + public Map getPem() { return this.pem; } @@ -62,6 +69,40 @@ public Map getJks() { return this.jks; } + public Watch getWatch() { + return this.watch; + } + + public static class Watch { + + /** + * File watching. + */ + private final File file = new File(); + + public File getFile() { + return this.file; + } + + public static class File { + + /** + * Quiet period, after which changes are detected. + */ + private Duration quietPeriod = Duration.ofSeconds(10); + + public Duration getQuietPeriod() { + return this.quietPeriod; + } + + public void setQuietPeriod(Duration quietPeriod) { + this.quietPeriod = quietPeriod; + } + + } + + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java index 89a3e7c1265c..531fb8d574f5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java @@ -16,11 +16,22 @@ package org.springframework.boot.autoconfigure.ssl; +import java.io.FileNotFoundException; +import java.io.UncheckedIOException; +import java.net.URL; +import java.nio.file.Path; +import java.util.LinkedHashSet; import java.util.Map; +import java.util.Set; import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundleRegistry; +import org.springframework.util.Assert; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; /** * A {@link SslBundleRegistrar} that registers SSL bundles based @@ -28,25 +39,87 @@ * * @author Scott Frederick * @author Phillip Webb + * @author Moritz Halbritter */ class SslPropertiesBundleRegistrar implements SslBundleRegistrar { + private static final Pattern PEM_CONTENT = Pattern.compile("-+BEGIN\\s+[^-]*-+", Pattern.CASE_INSENSITIVE); + private final SslProperties.Bundles properties; - SslPropertiesBundleRegistrar(SslProperties properties) { + private final FileWatcher fileWatcher; + + SslPropertiesBundleRegistrar(SslProperties properties, FileWatcher fileWatcher) { this.properties = properties.getBundle(); + this.fileWatcher = fileWatcher; } @Override public void registerBundles(SslBundleRegistry registry) { - registerBundles(registry, this.properties.getPem(), PropertiesSslBundle::get); - registerBundles(registry, this.properties.getJks(), PropertiesSslBundle::get); + registerBundles(registry, this.properties.getPem(), PropertiesSslBundle::get, this::getLocations); + registerBundles(registry, this.properties.getJks(), PropertiesSslBundle::get, this::getLocations); } private

    void registerBundles(SslBundleRegistry registry, Map properties, - Function bundleFactory) { - properties.forEach((bundleName, bundleProperties) -> registry.registerBundle(bundleName, - bundleFactory.apply(bundleProperties))); + Function bundleFactory, Function> locationsSupplier) { + properties.forEach((bundleName, bundleProperties) -> { + SslBundle bundle = bundleFactory.apply(bundleProperties); + registry.registerBundle(bundleName, bundle); + if (bundleProperties.isReloadOnUpdate()) { + Set paths = locationsSupplier.apply(bundleProperties) + .stream() + .filter(Location::hasValue) + .map((location) -> toPath(bundleName, location)) + .collect(Collectors.toSet()); + this.fileWatcher.watch(paths, + () -> registry.updateBundle(bundleName, bundleFactory.apply(bundleProperties))); + } + }); + } + + private Set getLocations(JksSslBundleProperties properties) { + JksSslBundleProperties.Store keystore = properties.getKeystore(); + JksSslBundleProperties.Store truststore = properties.getTruststore(); + Set locations = new LinkedHashSet<>(); + locations.add(new Location("keystore.location", keystore.getLocation())); + locations.add(new Location("truststore.location", truststore.getLocation())); + return locations; + } + + private Set getLocations(PemSslBundleProperties properties) { + PemSslBundleProperties.Store keystore = properties.getKeystore(); + PemSslBundleProperties.Store truststore = properties.getTruststore(); + Set locations = new LinkedHashSet<>(); + locations.add(new Location("keystore.private-key", keystore.getPrivateKey())); + locations.add(new Location("keystore.certificate", keystore.getCertificate())); + locations.add(new Location("truststore.private-key", truststore.getPrivateKey())); + locations.add(new Location("truststore.certificate", truststore.getCertificate())); + return locations; + } + + private Path toPath(String bundleName, Location watchableLocation) { + String value = watchableLocation.value(); + String field = watchableLocation.field(); + Assert.state(!PEM_CONTENT.matcher(value).find(), + () -> "SSL bundle '%s' '%s' is not a URL and can't be watched".formatted(bundleName, field)); + try { + URL url = ResourceUtils.getURL(value); + Assert.state("file".equalsIgnoreCase(url.getProtocol()), + () -> "SSL bundle '%s' '%s' URL '%s' doesn't point to a file".formatted(bundleName, field, url)); + return Path.of(url.getFile()).toAbsolutePath(); + } + catch (FileNotFoundException ex) { + throw new UncheckedIOException( + "SSL bundle '%s' '%s' location '%s' cannot be watched".formatted(bundleName, field, value), ex); + } + } + + private record Location(String field, String value) { + + boolean hasValue() { + return StringUtils.hasText(this.value); + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/FileWatcherTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/FileWatcherTests.java new file mode 100644 index 000000000000..e680479d0e45 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/FileWatcherTests.java @@ -0,0 +1,200 @@ +/* + * Copyright 2012-2023 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.ssl; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.fail; + +/** + * Tests for {@link FileWatcher}. + * + * @author Moritz Halbritter + */ +class FileWatcherTests { + + private FileWatcher fileWatcher; + + @BeforeEach + void setUp() { + this.fileWatcher = new FileWatcher(Duration.ofMillis(10)); + } + + @AfterEach + void tearDown() throws IOException { + this.fileWatcher.close(); + } + + @Test + void shouldTriggerOnFileCreation(@TempDir Path tempDir) throws Exception { + Path newFile = tempDir.resolve("new-file.txt"); + WaitingCallback callback = new WaitingCallback(); + this.fileWatcher.watch(Set.of(tempDir), callback); + Files.createFile(newFile); + callback.expectChanges(); + } + + @Test + void shouldTriggerOnFileDeletion(@TempDir Path tempDir) throws Exception { + Path deletedFile = tempDir.resolve("deleted-file.txt"); + Files.createFile(deletedFile); + WaitingCallback callback = new WaitingCallback(); + this.fileWatcher.watch(Set.of(tempDir), callback); + Files.delete(deletedFile); + callback.expectChanges(); + } + + @Test + void shouldTriggerOnFileModification(@TempDir Path tempDir) throws Exception { + Path deletedFile = tempDir.resolve("modified-file.txt"); + Files.createFile(deletedFile); + WaitingCallback callback = new WaitingCallback(); + this.fileWatcher.watch(Set.of(tempDir), callback); + Files.writeString(deletedFile, "Some content"); + callback.expectChanges(); + } + + @Test + void shouldWatchFile(@TempDir Path tempDir) throws Exception { + Path watchedFile = tempDir.resolve("watched.txt"); + Files.createFile(watchedFile); + WaitingCallback callback = new WaitingCallback(); + this.fileWatcher.watch(Set.of(watchedFile), callback); + Files.writeString(watchedFile, "Some content"); + callback.expectChanges(); + } + + @Test + void shouldIgnoreNotWatchedFiles(@TempDir Path tempDir) throws Exception { + Path watchedFile = tempDir.resolve("watched.txt"); + Path notWatchedFile = tempDir.resolve("not-watched.txt"); + Files.createFile(watchedFile); + Files.createFile(notWatchedFile); + WaitingCallback callback = new WaitingCallback(); + this.fileWatcher.watch(Set.of(watchedFile), callback); + Files.writeString(notWatchedFile, "Some content"); + callback.expectNoChanges(); + } + + @Test + void shouldFailIfDirectoryOrFileDoesNotExist(@TempDir Path tempDir) { + Path directory = tempDir.resolve("dir1"); + assertThatExceptionOfType(UncheckedIOException.class) + .isThrownBy(() -> this.fileWatcher.watch(Set.of(directory), new WaitingCallback())) + .withMessageMatching("Failed to register paths for watching: \\[.+/dir1]"); + } + + @Test + void shouldNotFailIfDirectoryIsRegisteredMultipleTimes(@TempDir Path tempDir) { + WaitingCallback callback = new WaitingCallback(); + assertThatCode(() -> { + this.fileWatcher.watch(Set.of(tempDir), callback); + this.fileWatcher.watch(Set.of(tempDir), callback); + }).doesNotThrowAnyException(); + } + + @Test + void shouldNotFailIfStoppedMultipleTimes(@TempDir Path tempDir) { + WaitingCallback callback = new WaitingCallback(); + this.fileWatcher.watch(Set.of(tempDir), callback); + assertThatCode(() -> { + this.fileWatcher.close(); + this.fileWatcher.close(); + }).doesNotThrowAnyException(); + } + + @Test + void testRelativeFiles() throws Exception { + Path watchedFile = Path.of(UUID.randomUUID() + ".txt"); + Files.createFile(watchedFile); + try { + WaitingCallback callback = new WaitingCallback(); + this.fileWatcher.watch(Set.of(watchedFile), callback); + Files.delete(watchedFile); + callback.expectChanges(); + } + finally { + Files.deleteIfExists(watchedFile); + } + } + + @Test + void testRelativeDirectories() throws Exception { + Path watchedDirectory = Path.of(UUID.randomUUID() + "/"); + Path file = watchedDirectory.resolve("file.txt"); + Files.createDirectory(watchedDirectory); + try { + WaitingCallback callback = new WaitingCallback(); + this.fileWatcher.watch(Set.of(watchedDirectory), callback); + Files.createFile(file); + callback.expectChanges(); + } + finally { + Files.deleteIfExists(file); + Files.deleteIfExists(watchedDirectory); + } + } + + private static class WaitingCallback implements Runnable { + + private final CountDownLatch latch = new CountDownLatch(1); + + volatile boolean changed = false; + + @Override + public void run() { + this.changed = true; + this.latch.countDown(); + } + + void expectChanges() throws InterruptedException { + waitForChanges(true); + assertThat(this.changed).as("changed").isTrue(); + } + + void expectNoChanges() throws InterruptedException { + waitForChanges(false); + assertThat(this.changed).as("changed").isFalse(); + } + + void waitForChanges(boolean fail) throws InterruptedException { + if (!this.latch.await(5, TimeUnit.SECONDS)) { + if (fail) { + fail("Timeout while waiting for changes"); + } + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrarTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrarTests.java new file mode 100644 index 000000000000..2410b8bbaaef --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrarTests.java @@ -0,0 +1,172 @@ +/* + * Copyright 2012-2023 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.ssl; + +import java.nio.file.Path; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.boot.ssl.SslBundleRegistry; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.assertArg; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; + +/** + * Tests for {@link SslPropertiesBundleRegistrar}. + * + * @author Moritz Halbritter + */ +class SslPropertiesBundleRegistrarTests { + + private SslPropertiesBundleRegistrar registrar; + + private FileWatcher fileWatcher; + + private SslProperties properties; + + private SslBundleRegistry registry; + + @BeforeEach + void setUp() { + this.properties = new SslProperties(); + this.fileWatcher = Mockito.mock(FileWatcher.class); + this.registrar = new SslPropertiesBundleRegistrar(this.properties, this.fileWatcher); + this.registry = Mockito.mock(SslBundleRegistry.class); + } + + @Test + void shouldWatchJksBundles() { + JksSslBundleProperties jks = new JksSslBundleProperties(); + jks.setReloadOnUpdate(true); + jks.getKeystore().setLocation("classpath:test.jks"); + jks.getKeystore().setPassword("secret"); + jks.getTruststore().setLocation("classpath:test.jks"); + jks.getTruststore().setPassword("secret"); + this.properties.getBundle().getJks().put("bundle1", jks); + this.registrar.registerBundles(this.registry); + then(this.registry).should(times(1)).registerBundle(eq("bundle1"), any()); + then(this.fileWatcher).should().watch(assertArg((set) -> pathEndingWith(set, "test.jks")), any()); + } + + @Test + void shouldWatchPemBundles() { + PemSslBundleProperties pem = new PemSslBundleProperties(); + pem.setReloadOnUpdate(true); + pem.getKeystore().setCertificate("classpath:org/springframework/boot/autoconfigure/ssl/rsa-cert.pem"); + pem.getKeystore().setPrivateKey("classpath:org/springframework/boot/autoconfigure/ssl/rsa-key.pem"); + pem.getTruststore().setCertificate("classpath:org/springframework/boot/autoconfigure/ssl/ed25519-cert.pem"); + pem.getTruststore().setPrivateKey("classpath:org/springframework/boot/autoconfigure/ssl/ed25519-key.pem"); + this.properties.getBundle().getPem().put("bundle1", pem); + this.registrar.registerBundles(this.registry); + then(this.registry).should(times(1)).registerBundle(eq("bundle1"), any()); + then(this.fileWatcher).should() + .watch(assertArg((set) -> pathEndingWith(set, "rsa-cert.pem", "rsa-key.pem")), any()); + } + + @Test + void shouldFailIfPemKeystoreCertificateIsEmbedded() { + PemSslBundleProperties pem = new PemSslBundleProperties(); + pem.setReloadOnUpdate(true); + pem.getKeystore().setCertificate(""" + -----BEGIN CERTIFICATE----- + MIICCzCCAb2gAwIBAgIUZbDi7G5czH+Yi0k2EMWxdf00XagwBQYDK2VwMHsxCzAJ + BgNVBAYTAlhYMRIwEAYDVQQIDAlTdGF0ZU5hbWUxETAPBgNVBAcMCENpdHlOYW1l + MRQwEgYDVQQKDAtDb21wYW55TmFtZTEbMBkGA1UECwwSQ29tcGFueVNlY3Rpb25O + YW1lMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMjMwOTExMTIxNDMwWhcNMzMwOTA4 + MTIxNDMwWjB7MQswCQYDVQQGEwJYWDESMBAGA1UECAwJU3RhdGVOYW1lMREwDwYD + VQQHDAhDaXR5TmFtZTEUMBIGA1UECgwLQ29tcGFueU5hbWUxGzAZBgNVBAsMEkNv + bXBhbnlTZWN0aW9uTmFtZTESMBAGA1UEAwwJbG9jYWxob3N0MCowBQYDK2VwAyEA + Q/DDA4BSgZ+Hx0DUxtIRjVjN+OcxXVURwAWc3Gt9GUyjUzBRMB0GA1UdDgQWBBSv + EdpoaBMBoxgO96GFbf03k07DSTAfBgNVHSMEGDAWgBSvEdpoaBMBoxgO96GFbf03 + k07DSTAPBgNVHRMBAf8EBTADAQH/MAUGAytlcANBAHMXDkGd57d4F4cRk/8UjhxD + 7OtRBZfdfznSvlhJIMNfH5q0zbC2eO3hWCB3Hrn/vIeswGP8Ov4AJ6eXeX44BQM= + -----END CERTIFICATE----- + """.strip()); + this.properties.getBundle().getPem().put("bundle1", pem); + assertThatIllegalStateException().isThrownBy(() -> this.registrar.registerBundles(this.registry)) + .withMessage("SSL bundle 'bundle1' 'keystore.certificate' is not a URL and can't be watched"); + } + + @Test + void shouldFailIfPemKeystorePrivateKeyIsEmbedded() { + PemSslBundleProperties pem = new PemSslBundleProperties(); + pem.setReloadOnUpdate(true); + pem.getKeystore().setCertificate("classpath:org/springframework/boot/autoconfigure/ssl/ed25519-cert.pem"); + pem.getKeystore().setPrivateKey(""" + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIC29RnMVTcyqXEAIO1b/6p7RdbM6TiqvnztVQ4IxYxUh + -----END PRIVATE KEY----- + """.strip()); + this.properties.getBundle().getPem().put("bundle1", pem); + assertThatIllegalStateException().isThrownBy(() -> this.registrar.registerBundles(this.registry)) + .withMessage("SSL bundle 'bundle1' 'keystore.private-key' is not a URL and can't be watched"); + } + + @Test + void shouldFailIfPemTruststoreCertificateIsEmbedded() { + PemSslBundleProperties pem = new PemSslBundleProperties(); + pem.setReloadOnUpdate(true); + pem.getTruststore().setCertificate(""" + -----BEGIN CERTIFICATE----- + MIICCzCCAb2gAwIBAgIUZbDi7G5czH+Yi0k2EMWxdf00XagwBQYDK2VwMHsxCzAJ + BgNVBAYTAlhYMRIwEAYDVQQIDAlTdGF0ZU5hbWUxETAPBgNVBAcMCENpdHlOYW1l + MRQwEgYDVQQKDAtDb21wYW55TmFtZTEbMBkGA1UECwwSQ29tcGFueVNlY3Rpb25O + YW1lMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMjMwOTExMTIxNDMwWhcNMzMwOTA4 + MTIxNDMwWjB7MQswCQYDVQQGEwJYWDESMBAGA1UECAwJU3RhdGVOYW1lMREwDwYD + VQQHDAhDaXR5TmFtZTEUMBIGA1UECgwLQ29tcGFueU5hbWUxGzAZBgNVBAsMEkNv + bXBhbnlTZWN0aW9uTmFtZTESMBAGA1UEAwwJbG9jYWxob3N0MCowBQYDK2VwAyEA + Q/DDA4BSgZ+Hx0DUxtIRjVjN+OcxXVURwAWc3Gt9GUyjUzBRMB0GA1UdDgQWBBSv + EdpoaBMBoxgO96GFbf03k07DSTAfBgNVHSMEGDAWgBSvEdpoaBMBoxgO96GFbf03 + k07DSTAPBgNVHRMBAf8EBTADAQH/MAUGAytlcANBAHMXDkGd57d4F4cRk/8UjhxD + 7OtRBZfdfznSvlhJIMNfH5q0zbC2eO3hWCB3Hrn/vIeswGP8Ov4AJ6eXeX44BQM= + -----END CERTIFICATE----- + """.strip()); + this.properties.getBundle().getPem().put("bundle1", pem); + assertThatIllegalStateException().isThrownBy(() -> this.registrar.registerBundles(this.registry)) + .withMessage("SSL bundle 'bundle1' 'truststore.certificate' is not a URL and can't be watched"); + } + + @Test + void shouldFailIfPemTruststorePrivateKeyIsEmbedded() { + PemSslBundleProperties pem = new PemSslBundleProperties(); + pem.setReloadOnUpdate(true); + pem.getTruststore().setCertificate("classpath:org/springframework/boot/autoconfigure/ssl/ed25519-cert.pem"); + pem.getTruststore().setPrivateKey(""" + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIC29RnMVTcyqXEAIO1b/6p7RdbM6TiqvnztVQ4IxYxUh + -----END PRIVATE KEY----- + """.strip()); + this.properties.getBundle().getPem().put("bundle1", pem); + assertThatIllegalStateException().isThrownBy(() -> this.registrar.registerBundles(this.registry)) + .withMessage("SSL bundle 'bundle1' 'truststore.private-key' is not a URL and can't be watched"); + } + + private void pathEndingWith(Set paths, String... suffixes) { + for (String suffix : suffixes) { + assertThat(paths).anyMatch((path) -> path.getFileName().toString().endsWith(suffix)); + } + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/ssl.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/ssl.adoc index 759755c25aa7..23f7ee3635e0 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/ssl.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/ssl.adoc @@ -104,3 +104,33 @@ In addition, the `SslBundle` provides details about the key being used, the prot The following example shows retrieving an `SslBundle` and using it to create an `SSLContext`: include::code:MyComponent[] + +[[features.ssl.reloading]] +=== Reloading SSL bundles + +SSL bundles can be reloaded when the key material changes. +The component consuming the bundle has to be compatible with reloadable SSL bundles. +Currently the following components are compatible: + +* Tomcat web server +* Netty web server + +To enable reloading, you need to opt-in via a configuration property as shown in this example: + +[source,yaml,indent=0,subs="verbatim",configblocks] +---- + spring: + ssl: + bundle: + pem: + mybundle: + reload-on-update: true + keystore: + certificate: "file:/some/directory/application.crt" + private-key: "file:/some/directory/application.key" +---- + +A file watcher is then watching the files and if they change, the SSL bundle will be reloaded. +This in turn triggers a reload in the consuming component, e.g. Tomcat rotates the certificates in the SSL enabled connectors. + +You can configure the quiet period (to make sure that there are no more changes) of the file watcher with the configprop:spring.ssl.bundle.watch.file.quiet-period[] property. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactory.java index 7547b8d68e6f..e291716cac71 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactory.java @@ -219,12 +219,15 @@ private InetSocketAddress getListenAddress() { private static final class TcpSslServerCustomizer extends org.springframework.boot.web.embedded.netty.SslServerCustomizer { + private final SslBundle sslBundle; + private TcpSslServerCustomizer(Ssl.ClientAuth clientAuth, SslBundle sslBundle) { super(null, clientAuth, sslBundle); + this.sslBundle = sslBundle; } private TcpServer apply(TcpServer server) { - AbstractProtocolSslContextSpec sslContextSpec = createSslContextSpec(); + AbstractProtocolSslContextSpec sslContextSpec = createSslContextSpec(this.sslBundle); return server.secure((spec) -> spec.sslContext(sslContextSpec)); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/DefaultSslBundleRegistry.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/DefaultSslBundleRegistry.java index fa79265755c2..8c999e5ccf4f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/DefaultSslBundleRegistry.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/DefaultSslBundleRegistry.java @@ -16,20 +16,31 @@ package org.springframework.boot.ssl; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.log.LogMessage; import org.springframework.util.Assert; /** * Default {@link SslBundleRegistry} implementation. * * @author Scott Frederick + * @author Moritz Halbritter + * @author Phillip Webb * @since 3.1.0 */ public class DefaultSslBundleRegistry implements SslBundleRegistry, SslBundles { - private final Map bundles = new ConcurrentHashMap<>(); + private static final Log logger = LogFactory.getLog(DefaultSslBundleRegistry.class); + + private final Map registeredBundles = new ConcurrentHashMap<>(); public DefaultSslBundleRegistry() { } @@ -42,18 +53,67 @@ public DefaultSslBundleRegistry(String name, SslBundle bundle) { public void registerBundle(String name, SslBundle bundle) { Assert.notNull(name, "Name must not be null"); Assert.notNull(bundle, "Bundle must not be null"); - SslBundle previous = this.bundles.putIfAbsent(name, bundle); + RegisteredSslBundle previous = this.registeredBundles.putIfAbsent(name, new RegisteredSslBundle(name, bundle)); Assert.state(previous == null, () -> "Cannot replace existing SSL bundle '%s'".formatted(name)); } + @Override + public void updateBundle(String name, SslBundle updatedBundle) { + getRegistered(name).update(updatedBundle); + } + @Override public SslBundle getBundle(String name) { + return getRegistered(name).getBundle(); + } + + @Override + public void addBundleUpdateHandler(String name, Consumer updateHandler) throws NoSuchSslBundleException { + getRegistered(name).addUpdateHandler(updateHandler); + } + + private RegisteredSslBundle getRegistered(String name) throws NoSuchSslBundleException { Assert.notNull(name, "Name must not be null"); - SslBundle bundle = this.bundles.get(name); - if (bundle == null) { + RegisteredSslBundle registered = this.registeredBundles.get(name); + if (registered == null) { throw new NoSuchSslBundleException(name, "SSL bundle name '%s' cannot be found".formatted(name)); } - return bundle; + return registered; + } + + private static class RegisteredSslBundle { + + private final String name; + + private final List> updateHandlers = new CopyOnWriteArrayList<>(); + + private volatile SslBundle bundle; + + RegisteredSslBundle(String name, SslBundle bundle) { + this.name = name; + this.bundle = bundle; + } + + void update(SslBundle updatedBundle) { + Assert.notNull(updatedBundle, "UpdatedBundle must not be null"); + this.bundle = updatedBundle; + if (this.updateHandlers.isEmpty()) { + logger.warn(LogMessage.format( + "SSL bundle '%s' has been updated but may be in use by a technology that doesn't support SSL reloading", + this.name)); + } + this.updateHandlers.forEach((handler) -> handler.accept(updatedBundle)); + } + + SslBundle getBundle() { + return this.bundle; + } + + void addUpdateHandler(Consumer updateHandler) { + Assert.notNull(updateHandler, "UpdateHandler must not be null"); + this.updateHandlers.add(updateHandler); + } + } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundleRegistry.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundleRegistry.java index 990a481066be..e1c0a4c64179 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundleRegistry.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundleRegistry.java @@ -20,6 +20,7 @@ * Interface that can be used to register an {@link SslBundle} for a given name. * * @author Scott Frederick + * @author Moritz Halbritter * @since 3.1.0 */ public interface SslBundleRegistry { @@ -31,4 +32,13 @@ public interface SslBundleRegistry { */ void registerBundle(String name, SslBundle bundle); + /** + * Updates an {@link SslBundle}. + * @param name the bundle name + * @param updatedBundle the updated bundle + * @throws NoSuchSslBundleException if the bundle cannot be found + * @since 3.2.0 + */ + void updateBundle(String name, SslBundle updatedBundle) throws NoSuchSslBundleException; + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundles.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundles.java index ed8a0ea9cda4..21afc4346a61 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundles.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/SslBundles.java @@ -16,20 +16,32 @@ package org.springframework.boot.ssl; +import java.util.function.Consumer; + /** * A managed set of {@link SslBundle} instances that can be retrieved by name. * * @author Scott Frederick + * @author Moritz Halbritter * @since 3.1.0 */ public interface SslBundles { /** * Return an {@link SslBundle} with the provided name. - * @param bundleName the bundle name + * @param name the bundle name * @return the bundle * @throws NoSuchSslBundleException if a bundle with the provided name does not exist */ - SslBundle getBundle(String bundleName) throws NoSuchSslBundleException; + SslBundle getBundle(String name) throws NoSuchSslBundleException; + + /** + * Add a handler that will be called each time the named bundle is updated. + * @param name the bundle name + * @param updateHandler the handler that should be called + * @throws NoSuchSslBundleException if a bundle with the provided name does not exist + * @since 3.2.0 + */ + void addBundleUpdateHandler(String name, Consumer updateHandler) throws NoSuchSslBundleException; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java index 8f7fa831a8cd..ee8c014ec3d9 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java @@ -37,11 +37,13 @@ import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * {@link ReactiveWebServerFactory} that can be used to create {@link NettyWebServer}s. * * @author Brian Clozel + * @author Moritz Halbritter * @since 2.0.0 */ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFactory { @@ -170,7 +172,12 @@ private HttpServer createHttpServer() { } private HttpServer customizeSslConfiguration(HttpServer httpServer) { - return new SslServerCustomizer(getHttp2(), getSsl().getClientAuth(), getSslBundle()).apply(httpServer); + SslServerCustomizer customizer = new SslServerCustomizer(getHttp2(), getSsl().getClientAuth(), getSslBundle()); + String bundleName = getSsl().getBundle(); + if (StringUtils.hasText(bundleName)) { + getSslBundles().addBundleUpdateHandler(bundleName, customizer::updateSslBundle); + } + return customizer.apply(httpServer); } private HttpProtocol[] listProtocols() { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java index 543ffdbd9fa1..a9e6e6c2c953 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java @@ -106,7 +106,6 @@ public NettyWebServer(HttpServer httpServer, ReactorHttpHandlerAdapter handlerAd * @param resourceFactory the factory for the server's {@link LoopResources loop * resources}, may be {@code null} * @since 3.2.0 - * {@link #NettyWebServer(HttpServer, ReactorHttpHandlerAdapter, Duration, Shutdown, ReactorResourceFactory)} */ public NettyWebServer(HttpServer httpServer, ReactorHttpHandlerAdapter handlerAdapter, Duration lifecycleTimeout, Shutdown shutdown, ReactorResourceFactory resourceFactory) { @@ -149,7 +148,7 @@ private String getStartedOnMessage(DisposableServer server) { StringBuilder message = new StringBuilder(); tryAppend(message, "port %s", server::port); tryAppend(message, "path %s", server::path); - return (message.length() > 0) ? "Netty started on " + message : "Netty started"; + return (!message.isEmpty()) ? "Netty started on " + message : "Netty started"; } protected String getStartedLogMessage() { @@ -159,10 +158,11 @@ protected String getStartedLogMessage() { private void tryAppend(StringBuilder message, String format, Supplier supplier) { try { Object value = supplier.get(); - message.append((message.length() != 0) ? " " : ""); + message.append((!message.isEmpty()) ? " " : ""); message.append(String.format(format, value)); } catch (UnsupportedOperationException ex) { + // Ignore } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/SslServerCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/SslServerCustomizer.java index 5480c4d0c876..204868e06088 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/SslServerCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/SslServerCustomizer.java @@ -17,10 +17,14 @@ package org.springframework.boot.web.embedded.netty; import io.netty.handler.ssl.ClientAuth; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import reactor.netty.http.Http11SslContextSpec; import reactor.netty.http.Http2SslContextSpec; import reactor.netty.http.server.HttpServer; import reactor.netty.tcp.AbstractProtocolSslContextSpec; +import reactor.netty.tcp.SslProvider; +import reactor.netty.tcp.SslProvider.SslContextSpec; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslOptions; @@ -36,41 +40,77 @@ * @author Chris Bono * @author Cyril Dangerville * @author Scott Frederick + * @author Moritz Halbritter + * @author Phillip Webb * @since 2.0.0 */ public class SslServerCustomizer implements NettyServerCustomizer { + private static final Log logger = LogFactory.getLog(SslServerCustomizer.class); + private final Http2 http2; - private final Ssl.ClientAuth clientAuth; + private final ClientAuth clientAuth; + + private volatile SslProvider sslProvider; - private final SslBundle sslBundle; + private volatile SslBundle sslBundle; public SslServerCustomizer(Http2 http2, Ssl.ClientAuth clientAuth, SslBundle sslBundle) { this.http2 = http2; - this.clientAuth = clientAuth; + this.clientAuth = Ssl.ClientAuth.map(clientAuth, ClientAuth.NONE, ClientAuth.OPTIONAL, ClientAuth.REQUIRE); this.sslBundle = sslBundle; + this.sslProvider = createSslProvider(sslBundle); } @Override public HttpServer apply(HttpServer server) { - AbstractProtocolSslContextSpec sslContextSpec = createSslContextSpec(); - return server.secure((spec) -> spec.sslContext(sslContextSpec)); + return server.secure(this::applySecurity); + } + + private void applySecurity(SslContextSpec spec) { + spec.sslContext(this.sslProvider.getSslContext()) + .setSniAsyncMappings((domainName, promise) -> promise.setSuccess(this.sslProvider)); } + void updateSslBundle(SslBundle sslBundle) { + logger.debug("SSL Bundle has been updated, reloading SSL configuration"); + this.sslBundle = sslBundle; + this.sslProvider = createSslProvider(sslBundle); + } + + private SslProvider createSslProvider(SslBundle sslBundle) { + return SslProvider.builder().sslContext(createSslContextSpec(sslBundle)).build(); + } + + /** + * Factory method used to create an {@link AbstractProtocolSslContextSpec}. + * @return the {@link AbstractProtocolSslContextSpec} to use + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of + * {@link #createSslContextSpec(SslBundle)} + */ + @Deprecated(since = "3.2", forRemoval = true) protected AbstractProtocolSslContextSpec createSslContextSpec() { + return createSslContextSpec(this.sslBundle); + } + + /** + * Create an {@link AbstractProtocolSslContextSpec} for a given {@link SslBundle}. + * @param sslBundle the {@link SslBundle} to use + * @return an {@link AbstractProtocolSslContextSpec} instance + * @since 3.2.0 + */ + protected final AbstractProtocolSslContextSpec createSslContextSpec(SslBundle sslBundle) { AbstractProtocolSslContextSpec sslContextSpec = (this.http2 != null && this.http2.isEnabled()) - ? Http2SslContextSpec.forServer(this.sslBundle.getManagers().getKeyManagerFactory()) - : Http11SslContextSpec.forServer(this.sslBundle.getManagers().getKeyManagerFactory()); - sslContextSpec.configure((builder) -> { - builder.trustManager(this.sslBundle.getManagers().getTrustManagerFactory()); - SslOptions options = this.sslBundle.getOptions(); + ? Http2SslContextSpec.forServer(sslBundle.getManagers().getKeyManagerFactory()) + : Http11SslContextSpec.forServer(sslBundle.getManagers().getKeyManagerFactory()); + return sslContextSpec.configure((builder) -> { + builder.trustManager(sslBundle.getManagers().getTrustManagerFactory()); + SslOptions options = sslBundle.getOptions(); builder.protocols(options.getEnabledProtocols()); builder.ciphers(SslOptions.asSet(options.getCiphers())); - builder.clientAuth(org.springframework.boot.web.server.Ssl.ClientAuth.map(this.clientAuth, ClientAuth.NONE, - ClientAuth.OPTIONAL, ClientAuth.REQUIRE)); + builder.clientAuth(this.clientAuth); }); - return sslContextSpec; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java index 516c61db0084..75601111c1df 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java @@ -17,6 +17,7 @@ package org.springframework.boot.web.embedded.tomcat; import org.apache.catalina.connector.Connector; +import org.apache.commons.logging.Log; import org.apache.coyote.ProtocolHandler; import org.apache.coyote.http11.AbstractHttp11JsseProtocol; import org.apache.coyote.http11.Http11NioProtocol; @@ -33,48 +34,62 @@ import org.springframework.util.StringUtils; /** - * {@link TomcatConnectorCustomizer} that configures SSL support on the given connector. + * Utility that configures SSL support on the given connector. * * @author Brian Clozel * @author Andy Wilkinson * @author Scott Frederick * @author Cyril Dangerville + * @author Moritz Halbritter */ -class SslConnectorCustomizer implements TomcatConnectorCustomizer { +class SslConnectorCustomizer { + + private final Log logger; private final ClientAuth clientAuth; - private final SslBundle sslBundle; + private final Connector connector; - SslConnectorCustomizer(ClientAuth clientAuth, SslBundle sslBundle) { + SslConnectorCustomizer(Log logger, Connector connector, ClientAuth clientAuth) { + this.logger = logger; this.clientAuth = clientAuth; - this.sslBundle = sslBundle; + this.connector = connector; + } + + void update(SslBundle updatedSslBundle) { + this.logger.debug("SSL Bundle has been updated, reloading SSL configuration"); + customize(updatedSslBundle); } - @Override - public void customize(Connector connector) { - ProtocolHandler handler = connector.getProtocolHandler(); + void customize(SslBundle sslBundle) { + ProtocolHandler handler = this.connector.getProtocolHandler(); Assert.state(handler instanceof AbstractHttp11JsseProtocol, "To use SSL, the connector's protocol handler must be an AbstractHttp11JsseProtocol subclass"); - configureSsl((AbstractHttp11JsseProtocol) handler); - connector.setScheme("https"); - connector.setSecure(true); + configureSsl(sslBundle, (AbstractHttp11JsseProtocol) handler); + this.connector.setScheme("https"); + this.connector.setSecure(true); } /** * Configure Tomcat's {@link AbstractHttp11JsseProtocol} for SSL. + * @param sslBundle the SSL bundle * @param protocol the protocol */ - void configureSsl(AbstractHttp11JsseProtocol protocol) { - SslBundleKey key = this.sslBundle.getKey(); - SslStoreBundle stores = this.sslBundle.getStores(); - SslOptions options = this.sslBundle.getOptions(); + private void configureSsl(SslBundle sslBundle, AbstractHttp11JsseProtocol protocol) { protocol.setSSLEnabled(true); SSLHostConfig sslHostConfig = new SSLHostConfig(); sslHostConfig.setHostName(protocol.getDefaultSSLHostConfigName()); - sslHostConfig.setSslProtocol(this.sslBundle.getProtocol()); - protocol.addSslHostConfig(sslHostConfig); configureSslClientAuth(sslHostConfig); + applySslBundle(sslBundle, protocol, sslHostConfig); + protocol.addSslHostConfig(sslHostConfig, true); + } + + private void applySslBundle(SslBundle sslBundle, AbstractHttp11JsseProtocol protocol, + SSLHostConfig sslHostConfig) { + SslBundleKey key = sslBundle.getKey(); + SslStoreBundle stores = sslBundle.getStores(); + SslOptions options = sslBundle.getOptions(); + sslHostConfig.setSslProtocol(sslBundle.getProtocol()); SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, Type.UNDEFINED); String keystorePassword = (stores.getKeyStorePassword() != null) ? stores.getKeyStorePassword() : ""; certificate.setCertificateKeystorePassword(keystorePassword); @@ -89,17 +104,14 @@ void configureSsl(AbstractHttp11JsseProtocol protocol) { String ciphers = StringUtils.arrayToCommaDelimitedString(options.getCiphers()); sslHostConfig.setCiphers(ciphers); } - configureEnabledProtocols(protocol); - configureSslStoreProvider(protocol, sslHostConfig, certificate); + configureSslStoreProvider(protocol, sslHostConfig, certificate, stores); + configureEnabledProtocols(sslHostConfig, options); } - private void configureEnabledProtocols(AbstractHttp11JsseProtocol protocol) { - SslOptions options = this.sslBundle.getOptions(); + private void configureEnabledProtocols(SSLHostConfig sslHostConfig, SslOptions options) { if (options.getEnabledProtocols() != null) { String enabledProtocols = StringUtils.arrayToDelimitedString(options.getEnabledProtocols(), "+"); - for (SSLHostConfig sslHostConfig : protocol.findSslHostConfigs()) { - sslHostConfig.setProtocols(enabledProtocols); - } + sslHostConfig.setProtocols(enabledProtocols); } } @@ -107,12 +119,11 @@ private void configureSslClientAuth(SSLHostConfig config) { config.setCertificateVerification(ClientAuth.map(this.clientAuth, "none", "optional", "required")); } - protected void configureSslStoreProvider(AbstractHttp11JsseProtocol protocol, SSLHostConfig sslHostConfig, - SSLHostConfigCertificate certificate) { + private void configureSslStoreProvider(AbstractHttp11JsseProtocol protocol, SSLHostConfig sslHostConfig, + SSLHostConfigCertificate certificate, SslStoreBundle stores) { Assert.isInstanceOf(Http11NioProtocol.class, protocol, "SslStoreProvider can only be used with Http11NioProtocol"); try { - SslStoreBundle stores = this.sslBundle.getStores(); if (stores.getKeyStore() != null) { certificate.setCertificateKeystore(stores.getKeyStore()); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java index ab31ecce7398..97d903d398c6 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java @@ -35,6 +35,8 @@ import org.apache.catalina.core.AprLifecycleListener; import org.apache.catalina.loader.WebappLoader; import org.apache.catalina.startup.Tomcat; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.apache.coyote.AbstractProtocol; import org.apache.coyote.ProtocolHandler; import org.apache.coyote.http2.Http2Protocol; @@ -57,11 +59,14 @@ * * @author Brian Clozel * @author HaiTao Zhang + * @author Moritz Halbritter * @since 2.0.0 */ public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFactory implements ConfigurableTomcatWebServerFactory { + private static final Log logger = LogFactory.getLog(TomcatReactiveWebServerFactory.class); + private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; /** @@ -224,7 +229,12 @@ private void customizeProtocol(AbstractProtocol protocol) { } private void customizeSsl(Connector connector) { - new SslConnectorCustomizer(getSsl().getClientAuth(), getSslBundle()).customize(connector); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(logger, connector, getSsl().getClientAuth()); + customizer.customize(getSslBundle()); + String sslBundleName = getSsl().getBundle(); + if (StringUtils.hasText(sslBundleName)) { + getSslBundles().addBundleUpdateHandler(sslBundleName, customizer::update); + } } @Override diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java index 7f05d87a1159..31144f56a3a3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java @@ -62,6 +62,8 @@ import org.apache.catalina.webresources.AbstractResourceSet; import org.apache.catalina.webresources.EmptyResource; import org.apache.catalina.webresources.StandardRoot; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.apache.coyote.AbstractProtocol; import org.apache.coyote.ProtocolHandler; import org.apache.coyote.http2.Http2Protocol; @@ -103,6 +105,7 @@ * @author Eddú Meléndez * @author Christoffer Sawicki * @author Dawid Antecki + * @author Moritz Halbritter * @since 2.0.0 * @see #setPort(int) * @see #setContextLifecycleListeners(Collection) @@ -111,6 +114,8 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFactory implements ConfigurableTomcatWebServerFactory, ResourceLoaderAware { + private static final Log logger = LogFactory.getLog(TomcatServletWebServerFactory.class); + private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; private static final Set> NO_CLASSES = Collections.emptySet(); @@ -366,7 +371,12 @@ private void invokeProtocolHandlerCustomizers(ProtocolHandler protocolHandler) { } private void customizeSsl(Connector connector) { - new SslConnectorCustomizer(getSsl().getClientAuth(), getSslBundle()).customize(connector); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(logger, connector, getSsl().getClientAuth()); + customizer.customize(getSslBundle()); + String sslBundleName = getSsl().getBundle(); + if (StringUtils.hasText(sslBundleName)) { + getSslBundles().addBundleUpdateHandler(sslBundleName, customizer::update); + } } /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java index 4a23afd75332..21fa14527ae6 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java @@ -146,6 +146,15 @@ public void setSslStoreProvider(SslStoreProvider sslStoreProvider) { this.sslStoreProvider = sslStoreProvider; } + /** + * Return the configured {@link SslBundles}. + * @return the {@link SslBundles} or {@code null} + * @since 3.2.0 + */ + public SslBundles getSslBundles() { + return this.sslBundles; + } + @Override public void setSslBundles(SslBundles sslBundles) { this.sslBundles = sslBundles; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/DefaultSslBundleRegistryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/DefaultSslBundleRegistryTests.java index d8cf034eef5f..fdd2c8824596 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/DefaultSslBundleRegistryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/DefaultSslBundleRegistryTests.java @@ -16,26 +16,43 @@ package org.springframework.boot.ssl; +import java.util.concurrent.atomic.AtomicReference; + +import org.awaitility.Awaitility; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.testsupport.system.CapturedOutput; +import org.springframework.boot.testsupport.system.OutputCaptureExtension; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; /** * Tests for {@link DefaultSslBundleRegistry}. * * @author Phillip Webb + * @author Moritz Halbritter */ +@ExtendWith(OutputCaptureExtension.class) class DefaultSslBundleRegistryTests { - private SslBundle bundle1 = mock(SslBundle.class); + private final SslBundle bundle1 = mock(SslBundle.class); - private SslBundle bundle2 = mock(SslBundle.class); + private final SslBundle bundle2 = mock(SslBundle.class); - private DefaultSslBundleRegistry registry = new DefaultSslBundleRegistry(); + private DefaultSslBundleRegistry registry; + + @BeforeEach + void setUp() { + this.registry = new DefaultSslBundleRegistry(); + } @Test void createWithNameAndBundleRegistersBundle() { @@ -89,4 +106,29 @@ void getBundleReturnsBundle() { assertThat(this.registry.getBundle("test2")).isSameAs(this.bundle2); } + @Test + void updateBundleShouldNotifyUpdateHandlers() { + AtomicReference updatedBundle = new AtomicReference<>(); + this.registry.registerBundle("test1", this.bundle1); + this.registry.addBundleUpdateHandler("test1", updatedBundle::set); + this.registry.updateBundle("test1", this.bundle2); + Awaitility.await().untilAtomic(updatedBundle, Matchers.equalTo(this.bundle2)); + } + + @Test + void shouldFailIfUpdatingNonRegisteredBundle() { + assertThatThrownBy(() -> this.registry.updateBundle("dummy", this.bundle1)) + .isInstanceOf(NoSuchSslBundleException.class) + .hasMessageContaining("'dummy'"); + } + + @Test + void shouldLogIfUpdatingBundleWithoutListeners(CapturedOutput output) { + this.registry.registerBundle("test1", this.bundle1); + this.registry.getBundle("test1"); + this.registry.updateBundle("test1", this.bundle2); + assertThat(output).contains( + "SSL bundle 'test1' has been updated but may be in use by a technology that doesn't support SSL reloading"); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java index cdb30bbc50e7..84c0f408cd6f 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java @@ -35,6 +35,62 @@ */ class PemSslStoreBundleTests { + private static final String CERTIFICATE = """ + -----BEGIN CERTIFICATE----- + MIIDqzCCApOgAwIBAgIIFMqbpqvipw0wDQYJKoZIhvcNAQELBQAwbDELMAkGA1UE + BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBhbG8gQWx0bzEP + MA0GA1UEChMGVk13YXJlMQ8wDQYDVQQLEwZTcHJpbmcxEjAQBgNVBAMTCWxvY2Fs + aG9zdDAgFw0yMzA1MDUxMTI2NThaGA8yMTIzMDQxMTExMjY1OFowbDELMAkGA1UE + BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBhbG8gQWx0bzEP + MA0GA1UEChMGVk13YXJlMQ8wDQYDVQQLEwZTcHJpbmcxEjAQBgNVBAMTCWxvY2Fs + aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPwHWxoE3xjRmNdD + +m+e/aFlr5wEGQUdWSDD613OB1w7kqO/audEp3c6HxDB3GPcEL0amJwXgY6CQMYu + sythuZX/EZSc2HdilTBu/5T+mbdWe5JkKThpiA0RYeucQfKuB7zv4ypioa4wiR4D + nPsZXjg95OF8pCzYEssv8wT49v+M3ohWUgfF0FPlMFCSo0YVTuzB1mhDlWKq/jhQ + 11WpTmk/dQX+l6ts6bYIcJt4uItG+a68a4FutuSjZdTAE0f5SOYRBpGH96mjLwEP + fW8ZjzvKb9g4R2kiuoPxvCDs1Y/8V2yvKqLyn5Tx9x/DjFmOi0DRK/TgELvNceCb + UDJmhXMCAwEAAaNPME0wHQYDVR0OBBYEFMBIGU1nwix5RS3O5hGLLoMdR1+NMCwG + A1UdEQQlMCOCCWxvY2FsaG9zdIcQAAAAAAAAAAAAAAAAAAAAAYcEfwAAATANBgkq + hkiG9w0BAQsFAAOCAQEAhepfJgTFvqSccsT97XdAZfvB0noQx5NSynRV8NWmeOld + hHP6Fzj6xCxHSYvlUfmX8fVP9EOAuChgcbbuTIVJBu60rnDT21oOOnp8FvNonCV6 + gJ89sCL7wZ77dw2RKIeUFjXXEV3QJhx2wCOVmLxnJspDoKFIEVjfLyiPXKxqe/6b + dG8zzWDZ6z+M2JNCtVoOGpljpHqMPCmbDktncv6H3dDTZ83bmLj1nbpOU587gAJ8 + fl1PiUDyPRIl2cnOJd+wCHKsyym/FL7yzk0OSEZ81I92LpGd/0b2Ld3m/bpe+C4Z + ILzLXTnC6AhrLcDc9QN/EO+BiCL52n7EplNLtSn1LQ== + -----END CERTIFICATE----- + """.strip(); + + private static final String PRIVATE_KEY = """ + -----BEGIN PRIVATE KEY----- + MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD8B1saBN8Y0ZjX + Q/pvnv2hZa+cBBkFHVkgw+tdzgdcO5Kjv2rnRKd3Oh8Qwdxj3BC9GpicF4GOgkDG + LrMrYbmV/xGUnNh3YpUwbv+U/pm3VnuSZCk4aYgNEWHrnEHyrge87+MqYqGuMIke + A5z7GV44PeThfKQs2BLLL/ME+Pb/jN6IVlIHxdBT5TBQkqNGFU7swdZoQ5Viqv44 + UNdVqU5pP3UF/perbOm2CHCbeLiLRvmuvGuBbrbko2XUwBNH+UjmEQaRh/epoy8B + D31vGY87ym/YOEdpIrqD8bwg7NWP/Fdsryqi8p+U8fcfw4xZjotA0Sv04BC7zXHg + m1AyZoVzAgMBAAECggEAfEqiZqANaF+BqXQIb4Dw42ZTJzWsIyYYnPySOGZRoe5t + QJ03uwtULYv34xtANe1DQgd6SMyc46ugBzzjtprQ3ET5Jhn99U6kdcjf+dpf85dO + hOEppP0CkDNI39nleinSfh6uIOqYgt/D143/nqQhn8oCdSOzkbwT9KnWh1bC9T7I + vFjGfElvt1/xl88qYgrWgYLgXaencNGgiv/4/M0FNhiHEGsVC7SCu6kapC/WIQpE + 5IdV+HR+tiLoGZhXlhqorY7QC4xKC4wwafVSiFxqDOQAuK+SMD4TCEv0Aop+c+SE + YBigVTmgVeJkjK7IkTEhKkAEFmRF5/5w+bZD9FhTNQKBgQD+4fNG1ChSU8RdizZT + 5dPlDyAxpETSCEXFFVGtPPh2j93HDWn7XugNyjn5FylTH507QlabC+5wZqltdIjK + GRB5MIinQ9/nR2fuwGc9s+0BiSEwNOUB1MWm7wWL/JUIiKq6sTi6sJIfsYg79zco + qxl5WE94aoINx9Utq1cdWhwJTQKBgQD9IjPksd4Jprz8zMrGLzR8k1gqHyhv24qY + EJ7jiHKKAP6xllTUYwh1IBSL6w2j5lfZPpIkb4Jlk2KUoX6fN81pWkBC/fTBUSIB + EHM9bL51+yKEYUbGIy/gANuRbHXsWg3sjUsFTNPN4hGTFk3w2xChCyl/f5us8Lo8 + Z633SNdpvwKBgQCGyDU9XzNzVZihXtx7wS0sE7OSjKtX5cf/UCbA1V0OVUWR3SYO + J0HPCQFfF0BjFHSwwYPKuaR9C8zMdLNhK5/qdh/NU7czNi9fsZ7moh7SkRFbzJzN + OxbKD9t/CzJEMQEXeF/nWTfsSpUgILqqZtAxuuFLbAcaAnJYlCKdAumQgQKBgQCK + mqjJh68pn7gJwGUjoYNe1xtGbSsqHI9F9ovZ0MPO1v6e5M7sQJHH+Fnnxzv/y8e8 + d6tz8e73iX1IHymDKv35uuZHCGF1XOR+qrA/KQUc+vcKf21OXsP/JtkTRs1HLoRD + S5aRf2DWcfvniyYARSNU2xTM8GWgi2ueWbMDHUp+ZwKBgA/swC+K+Jg5DEWm6Sau + e6y+eC6S+SoXEKkI3wf7m9aKoZo0y+jh8Gas6gratlc181pSM8O3vZG0n19b493I + apCFomMLE56zEzvyzfpsNhFhk5MBMCn0LPyzX6MiynRlGyWIj0c99fbHI3pOMufP + WgmVLTZ8uDcSW1MbdUCwFSk5 + -----END PRIVATE KEY----- + """.strip(); + private static final char[] EMPTY_KEY_PASSWORD = new char[] {}; @Test @@ -99,6 +155,16 @@ void whenHasKeyStoreDetailsAndTrustStoreDetails() { assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("ssl")); } + @Test + void whenHasEmbeddedKeyStoreDetailsAndTrustStoreDetails() { + PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate(CERTIFICATE).withPrivateKey(PRIVATE_KEY); + PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate(CERTIFICATE) + .withPrivateKey(PRIVATE_KEY); + PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails); + assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("ssl")); + assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("ssl")); + } + @Test void whenHasKeyStoreDetailsAndTrustStoreDetailsAndAlias() { PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem") diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java index 3694ee942d8f..d67008187107 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactoryTests.java @@ -34,6 +34,11 @@ import reactor.netty.http.server.HttpServer; import reactor.test.StepVerifier; +import org.springframework.boot.ssl.DefaultSslBundleRegistry; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.ssl.pem.PemSslStoreBundle; +import org.springframework.boot.ssl.pem.PemSslStoreDetails; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory; import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactoryTests; import org.springframework.boot.web.server.PortInUseException; @@ -59,6 +64,7 @@ * * @author Brian Clozel * @author Chris Bono + * @author Moritz Halbritter */ class NettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactoryTests { @@ -132,6 +138,16 @@ void whenSslIsConfiguredWithAValidAliasARequestSucceeds() { StepVerifier.create(result).expectNext("Hello World").expectComplete().verify(Duration.ofSeconds(30)); } + @Test + void whenSslBundleIsUpdatedThenSslIsReloaded() { + DefaultSslBundleRegistry bundles = new DefaultSslBundleRegistry("bundle1", createSslBundle("1.key", "1.crt")); + Mono result = testSslWithBundle(bundles, "bundle1"); + StepVerifier.create(result).expectNext("Hello World").expectComplete().verify(Duration.ofSeconds(30)); + bundles.updateBundle("bundle1", createSslBundle("2.key", "2.crt")); + Mono result2 = executeSslRequest(); + StepVerifier.create(result2).expectNext("Hello World").expectComplete().verify(Duration.ofSeconds(30)); + } + @Test void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() { NettyReactiveWebServerFactory factory = getFactory(); @@ -161,7 +177,7 @@ void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() { protected void startedLogMessageWithMultiplePorts() { } - protected Mono testSslWithAlias(String alias) { + private Mono testSslWithAlias(String alias) { String keyStore = "classpath:test.jks"; String keyPassword = "password"; NettyReactiveWebServerFactory factory = getFactory(); @@ -172,6 +188,19 @@ protected Mono testSslWithAlias(String alias) { factory.setSsl(ssl); this.webServer = factory.getWebServer(new EchoHandler()); this.webServer.start(); + return executeSslRequest(); + } + + private Mono testSslWithBundle(SslBundles sslBundles, String bundle) { + NettyReactiveWebServerFactory factory = getFactory(); + factory.setSslBundles(sslBundles); + factory.setSsl(Ssl.forBundle(bundle)); + this.webServer = factory.getWebServer(new EchoHandler()); + this.webServer.start(); + return executeSslRequest(); + } + + private Mono executeSslRequest() { ReactorClientHttpConnector connector = buildTrustAllSslConnector(); WebClient client = WebClient.builder() .baseUrl("https://localhost:" + this.webServer.getPort()) @@ -200,6 +229,13 @@ protected void addConnector(int port, AbstractReactiveWebServerFactory factory) throw new UnsupportedOperationException("Reactor Netty does not support multiple ports"); } + private static SslBundle createSslBundle(String key, String certificate) { + return SslBundle.of(new PemSslStoreBundle( + new PemSslStoreDetails(null, "classpath:org/springframework/boot/web/embedded/netty/" + certificate, + "classpath:org/springframework/boot/web/embedded/netty/" + key), + null)); + } + static class NoPortNettyReactiveWebServerFactory extends NettyReactiveWebServerFactory { NoPortNettyReactiveWebServerFactory(int port) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java index 53b05891a7fd..a845ffb1e7e4 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java @@ -27,6 +27,8 @@ import org.apache.catalina.LifecycleState; import org.apache.catalina.connector.Connector; import org.apache.catalina.startup.Tomcat; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.apache.tomcat.util.net.SSLHostConfig; import org.apache.tomcat.util.net.SSLHostConfigCertificate; import org.junit.jupiter.api.AfterEach; @@ -65,6 +67,8 @@ @MockPkcs11Security class SslConnectorCustomizerTests { + private final Log logger = LogFactory.getLog(SslConnectorCustomizerTests.class); + private Tomcat tomcat; @BeforeEach @@ -87,10 +91,9 @@ void sslCiphersConfiguration() throws Exception { ssl.setKeyStore("classpath:test.jks"); ssl.setKeyStorePassword("secret"); ssl.setCiphers(new String[] { "ALPHA", "BRAVO", "CHARLIE" }); - SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(), - WebServerSslBundle.get(ssl)); Connector connector = this.tomcat.getConnector(); - customizer.customize(connector); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth()); + customizer.customize(WebServerSslBundle.get(ssl)); this.tomcat.start(); SSLHostConfig[] sslHostConfigs = connector.getProtocolHandler().findSslHostConfigs(); assertThat(sslHostConfigs[0].getCiphers()).isEqualTo("ALPHA:BRAVO:CHARLIE"); @@ -103,10 +106,9 @@ void sslEnabledMultipleProtocolsConfiguration() throws Exception { ssl.setKeyStore("src/test/resources/test.jks"); ssl.setEnabledProtocols(new String[] { "TLSv1.1", "TLSv1.2" }); ssl.setCiphers(new String[] { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "BRAVO" }); - SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(), - WebServerSslBundle.get(ssl)); Connector connector = this.tomcat.getConnector(); - customizer.customize(connector); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth()); + customizer.customize(WebServerSslBundle.get(ssl)); this.tomcat.start(); SSLHostConfig sslHostConfig = connector.getProtocolHandler().findSslHostConfigs()[0]; assertThat(sslHostConfig.getSslProtocol()).isEqualTo("TLS"); @@ -120,10 +122,9 @@ void sslEnabledProtocolsConfiguration() throws Exception { ssl.setKeyStore("src/test/resources/test.jks"); ssl.setEnabledProtocols(new String[] { "TLSv1.2" }); ssl.setCiphers(new String[] { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "BRAVO" }); - SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(), - WebServerSslBundle.get(ssl)); Connector connector = this.tomcat.getConnector(); - customizer.customize(connector); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth()); + customizer.customize(WebServerSslBundle.get(ssl)); this.tomcat.start(); SSLHostConfig sslHostConfig = connector.getProtocolHandler().findSslHostConfigs()[0]; assertThat(sslHostConfig.getSslProtocol()).isEqualTo("TLS"); @@ -139,10 +140,9 @@ void customizeWhenSslStoreProviderProvidesOnlyKeyStoreShouldUseDefaultTruststore SslStoreProvider sslStoreProvider = mock(SslStoreProvider.class); KeyStore keyStore = loadStore(); given(sslStoreProvider.getKeyStore()).willReturn(keyStore); - SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(), - WebServerSslBundle.get(ssl, null, sslStoreProvider)); Connector connector = this.tomcat.getConnector(); - customizer.customize(connector); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth()); + customizer.customize(WebServerSslBundle.get(ssl, null, sslStoreProvider)); this.tomcat.start(); SSLHostConfig sslHostConfig = connector.getProtocolHandler().findSslHostConfigs()[0]; SSLHostConfig sslHostConfigWithDefaults = new SSLHostConfig(); @@ -161,10 +161,9 @@ void customizeWhenSslStoreProviderProvidesOnlyTrustStoreShouldUseDefaultKeystore SslStoreProvider sslStoreProvider = mock(SslStoreProvider.class); KeyStore trustStore = loadStore(); given(sslStoreProvider.getTrustStore()).willReturn(trustStore); - SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(), - WebServerSslBundle.get(ssl, null, sslStoreProvider)); Connector connector = this.tomcat.getConnector(); - customizer.customize(connector); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth()); + customizer.customize(WebServerSslBundle.get(ssl, null, sslStoreProvider)); this.tomcat.start(); SSLHostConfig sslHostConfig = connector.getProtocolHandler().findSslHostConfigs()[0]; assertThat(sslHostConfig.getTruststore()).isEqualTo(trustStore); @@ -180,10 +179,9 @@ void customizeWhenSslStoreProviderPresentShouldIgnorePasswordFromSsl(CapturedOut SslStoreProvider sslStoreProvider = mock(SslStoreProvider.class); given(sslStoreProvider.getTrustStore()).willReturn(loadStore()); given(sslStoreProvider.getKeyStore()).willReturn(loadStore()); - SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(), - WebServerSslBundle.get(ssl, null, sslStoreProvider)); Connector connector = this.tomcat.getConnector(); - customizer.customize(connector); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth()); + customizer.customize(WebServerSslBundle.get(ssl, null, sslStoreProvider)); this.tomcat.start(); assertThat(connector.getState()).isEqualTo(LifecycleState.STARTED); assertThat(output).doesNotContain("Password verification failed"); @@ -192,9 +190,9 @@ void customizeWhenSslStoreProviderPresentShouldIgnorePasswordFromSsl(CapturedOut @Test void customizeWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsException() { assertThatIllegalStateException().isThrownBy(() -> { - SslConnectorCustomizer customizer = new SslConnectorCustomizer(Ssl.ClientAuth.NONE, - WebServerSslBundle.get(new Ssl())); - customizer.customize(this.tomcat.getConnector()); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, this.tomcat.getConnector(), + Ssl.ClientAuth.NONE); + customizer.customize(WebServerSslBundle.get(new Ssl())); }).withMessageContaining("SSL is enabled but no trust material is configured"); } @@ -206,9 +204,9 @@ void customizeWhenSslIsEnabledWithPkcs11AndKeyStoreThrowsException() { ssl.setKeyStore("src/test/resources/test.jks"); ssl.setKeyPassword("password"); assertThatIllegalStateException().isThrownBy(() -> { - SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(), - WebServerSslBundle.get(ssl)); - customizer.customize(this.tomcat.getConnector()); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, this.tomcat.getConnector(), + ssl.getClientAuth()); + customizer.customize(WebServerSslBundle.get(ssl)); }).withMessageContaining("must be empty or null for PKCS11 hardware key stores"); } @@ -218,9 +216,9 @@ void customizeWhenSslIsEnabledWithPkcs11AndKeyStoreProvider() { ssl.setKeyStoreType("PKCS11"); ssl.setKeyStoreProvider(MockPkcs11SecurityProvider.NAME); ssl.setKeyStorePassword("1234"); - SslConnectorCustomizer customizer = new SslConnectorCustomizer(ssl.getClientAuth(), - WebServerSslBundle.get(ssl)); - assertThatNoException().isThrownBy(() -> customizer.customize(this.tomcat.getConnector())); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, this.tomcat.getConnector(), + ssl.getClientAuth()); + assertThatNoException().isThrownBy(() -> customizer.customize(WebServerSslBundle.get(ssl))); } private KeyStore loadStore() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java index e619c9120ccd..5dafd69bc45c 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java @@ -32,6 +32,9 @@ import javax.naming.InitialContext; import javax.naming.NamingException; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; import jakarta.servlet.MultipartConfigElement; import jakarta.servlet.ServletContext; @@ -60,8 +63,11 @@ import org.apache.hc.client5.http.HttpHostConnectException; import org.apache.hc.client5.http.classic.HttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; +import org.apache.hc.client5.http.ssl.TrustSelfSignedStrategy; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.NoHttpResponseException; +import org.apache.hc.core5.ssl.SSLContextBuilder; import org.apache.jasper.servlet.JspServlet; import org.apache.tomcat.JarScanFilter; import org.apache.tomcat.JarScanType; @@ -73,9 +79,11 @@ import org.junit.jupiter.api.Test; import org.mockito.InOrder; +import org.springframework.boot.ssl.DefaultSslBundleRegistry; import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.web.server.PortInUseException; import org.springframework.boot.web.server.Shutdown; +import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.WebServerException; import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory; import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactoryTests; @@ -87,6 +95,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.util.FileSystemUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -107,6 +116,7 @@ * @author Phillip Webb * @author Dave Syer * @author Stephane Nicoll + * @author Moritz Halbritter */ class TomcatServletWebServerFactoryTests extends AbstractServletWebServerFactoryTests { @@ -636,6 +646,30 @@ void whenServerIsShuttingDownARequestOnAnIdleConnectionResultsInConnectionReset( this.webServer.stop(); } + @Test + void shouldUpdateSslWhenReloadingSslBundles() throws Exception { + TomcatServletWebServerFactory factory = getFactory(); + addTestTxtFile(factory); + DefaultSslBundleRegistry bundles = new DefaultSslBundleRegistry("test", + createPemSslBundle("classpath:org/springframework/boot/web/embedded/tomcat/1.crt", + "classpath:org/springframework/boot/web/embedded/tomcat/1.key")); + factory.setSslBundles(bundles); + factory.setSsl(Ssl.forBundle("test")); + this.webServer = factory.getWebServer(); + this.webServer.start(); + RememberingHostnameVerifier verifier = new RememberingHostnameVerifier(); + SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( + new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy()).build(), verifier); + HttpComponentsClientHttpRequestFactory requestFactory = createHttpComponentsRequestFactory(socketFactory); + assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory)).isEqualTo("test"); + assertThat(verifier.getLastPrincipal()).isEqualTo("CN=1"); + requestFactory = createHttpComponentsRequestFactory(socketFactory); + bundles.updateBundle("test", createPemSslBundle("classpath:org/springframework/boot/web/embedded/tomcat/2.crt", + "classpath:org/springframework/boot/web/embedded/tomcat/2.key")); + assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory)).isEqualTo("test"); + assertThat(verifier.getLastPrincipal()).isEqualTo("CN=2"); + } + @Override protected JspServlet getJspServlet() throws ServletException { Tomcat tomcat = ((TomcatWebServer) this.webServer).getTomcat(); @@ -694,4 +728,25 @@ protected String startedLogMessage() { return ((TomcatWebServer) this.webServer).getStartedLogMessage(); } + private static class RememberingHostnameVerifier implements HostnameVerifier { + + private volatile String lastPrincipal; + + @Override + public boolean verify(String hostname, SSLSession session) { + try { + this.lastPrincipal = session.getPeerPrincipal().getName(); + } + catch (SSLPeerUnverifiedException ex) { + throw new RuntimeException(ex); + } + return true; + } + + String getLastPrincipal() { + return this.lastPrincipal; + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java index 3e5a79f00bc9..d7236bee107e 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java @@ -789,7 +789,7 @@ private JksSslStoreDetails getJksStoreDetails(String location) { return new JksSslStoreDetails(getStoreType(location), null, location, "secret"); } - private SslBundle createPemSslBundle(String cert, String privateKey) { + protected SslBundle createPemSslBundle(String cert, String privateKey) { PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate(cert).withPrivateKey(privateKey); PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate(cert); SslStoreBundle stores = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails); @@ -807,14 +807,13 @@ protected void testRestrictedSSLProtocolsAndCipherSuites(String[] protocols, Str assertThat(getResponse(getLocalUrl("https", "/hello"), requestFactory)).contains("scheme=https"); } - private HttpComponentsClientHttpRequestFactory createHttpComponentsRequestFactory( + protected HttpComponentsClientHttpRequestFactory createHttpComponentsRequestFactory( SSLConnectionSocketFactory socketFactory) { PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create() .setSSLSocketFactory(socketFactory) .build(); HttpClient httpClient = this.httpClientBuilder.get().setConnectionManager(connectionManager).build(); - HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); - return requestFactory; + return new HttpComponentsClientHttpRequestFactory(httpClient); } private String getStoreType(String keyStore) { @@ -1457,7 +1456,7 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) throws protected abstract Charset getCharset(Locale locale); - private void addTestTxtFile(AbstractServletWebServerFactory factory) throws IOException { + protected void addTestTxtFile(AbstractServletWebServerFactory factory) throws IOException { FileCopyUtils.copy("test", new FileWriter(new File(this.tempDir, "test.txt"))); factory.setDocumentRoot(this.tempDir); factory.setRegisterDefaultServlet(true); diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/netty/1.crt b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/netty/1.crt new file mode 100644 index 000000000000..dd4be7410d6e --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/netty/1.crt @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBLjCB4aADAgECAhQ25wrNnapZEkFc8kgf5NDHXKxnTzAFBgMrZXAwDDEKMAgG +A1UEAwwBMTAgFw0yMzEwMTAwODU1MTJaGA8yMTIzMDkxNjA4NTUxMlowDDEKMAgG +A1UEAwwBMTAqMAUGAytlcAMhAOyxNxHzcNj7xTkcjVLI09sYUGUGIvdV5s0YWXT8 +XAiwo1MwUTAdBgNVHQ4EFgQUmm23oLIu5MgdBb/snZSuE+MrRZ0wHwYDVR0jBBgw +FoAUmm23oLIu5MgdBb/snZSuE+MrRZ0wDwYDVR0TAQH/BAUwAwEB/zAFBgMrZXAD +QQA2KMpIyySC8u4onW2MVW1iK2dJJZbMRaNMLlQuE+ZIHQLwflYW4sH/Pp76pboc +QhqKXcO7xH7f2tD5hE2izcUB +-----END CERTIFICATE----- diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/netty/1.key b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/netty/1.key new file mode 100644 index 000000000000..712fa35133c4 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/netty/1.key @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIJb1A+i5bmilBD9mUbhk1oFVI6FAZQGnhduv7xV6WWEc +-----END PRIVATE KEY----- diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/netty/2.crt b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/netty/2.crt new file mode 100644 index 000000000000..7c13395e0a54 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/netty/2.crt @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBLjCB4aADAgECAhR4TMDk3qg5sKREp16lEHR3bV3M9zAFBgMrZXAwDDEKMAgG +A1UEAwwBMjAgFw0yMzEwMTAwODU1MjBaGA8yMTIzMDkxNjA4NTUyMFowDDEKMAgG +A1UEAwwBMjAqMAUGAytlcAMhADPft6hzyCjHCe5wSprChuuO/CuPIJ2t+l4roS1D +43/wo1MwUTAdBgNVHQ4EFgQUfrRibAWml4Ous4kpnBIggM2xnLcwHwYDVR0jBBgw +FoAUfrRibAWml4Ous4kpnBIggM2xnLcwDwYDVR0TAQH/BAUwAwEB/zAFBgMrZXAD +QQC/MOclal2Cp0B3kmaLbK0M8mapclIOJa78hzBkqPA3URClAF2GmF187wHqi7qV ++xZ+KWv26pLJR46vk8Kc6ZIO +-----END CERTIFICATE----- diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/netty/2.key b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/netty/2.key new file mode 100644 index 000000000000..9917897564bf --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/netty/2.key @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEICxhres2Z2lICm7/isnm+2iNR12GmgG7KK86BNDZDeIF +-----END PRIVATE KEY----- diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/tomcat/1.crt b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/tomcat/1.crt new file mode 100644 index 000000000000..dd4be7410d6e --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/tomcat/1.crt @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBLjCB4aADAgECAhQ25wrNnapZEkFc8kgf5NDHXKxnTzAFBgMrZXAwDDEKMAgG +A1UEAwwBMTAgFw0yMzEwMTAwODU1MTJaGA8yMTIzMDkxNjA4NTUxMlowDDEKMAgG +A1UEAwwBMTAqMAUGAytlcAMhAOyxNxHzcNj7xTkcjVLI09sYUGUGIvdV5s0YWXT8 +XAiwo1MwUTAdBgNVHQ4EFgQUmm23oLIu5MgdBb/snZSuE+MrRZ0wHwYDVR0jBBgw +FoAUmm23oLIu5MgdBb/snZSuE+MrRZ0wDwYDVR0TAQH/BAUwAwEB/zAFBgMrZXAD +QQA2KMpIyySC8u4onW2MVW1iK2dJJZbMRaNMLlQuE+ZIHQLwflYW4sH/Pp76pboc +QhqKXcO7xH7f2tD5hE2izcUB +-----END CERTIFICATE----- diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/tomcat/1.key b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/tomcat/1.key new file mode 100644 index 000000000000..712fa35133c4 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/tomcat/1.key @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIJb1A+i5bmilBD9mUbhk1oFVI6FAZQGnhduv7xV6WWEc +-----END PRIVATE KEY----- diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/tomcat/2.crt b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/tomcat/2.crt new file mode 100644 index 000000000000..7c13395e0a54 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/tomcat/2.crt @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBLjCB4aADAgECAhR4TMDk3qg5sKREp16lEHR3bV3M9zAFBgMrZXAwDDEKMAgG +A1UEAwwBMjAgFw0yMzEwMTAwODU1MjBaGA8yMTIzMDkxNjA4NTUyMFowDDEKMAgG +A1UEAwwBMjAqMAUGAytlcAMhADPft6hzyCjHCe5wSprChuuO/CuPIJ2t+l4roS1D +43/wo1MwUTAdBgNVHQ4EFgQUfrRibAWml4Ous4kpnBIggM2xnLcwHwYDVR0jBBgw +FoAUfrRibAWml4Ous4kpnBIggM2xnLcwDwYDVR0TAQH/BAUwAwEB/zAFBgMrZXAD +QQC/MOclal2Cp0B3kmaLbK0M8mapclIOJa78hzBkqPA3URClAF2GmF187wHqi7qV ++xZ+KWv26pLJR46vk8Kc6ZIO +-----END CERTIFICATE----- diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/tomcat/2.key b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/tomcat/2.key new file mode 100644 index 000000000000..9917897564bf --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/web/embedded/tomcat/2.key @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEICxhres2Z2lICm7/isnm+2iNR12GmgG7KK86BNDZDeIF +-----END PRIVATE KEY----- From 8edb4b97292e59b0b96a8deecd3b058e94b744a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edd=C3=BA=20Mel=C3=A9ndez?= Date: Fri, 13 Oct 2023 21:16:43 -0600 Subject: [PATCH 0630/1215] Add properties for configuring EnumFeature and JsonNodeFeature Both `EnumFeature` and `JsonNodeFeature` implement `DataTypeFeature` which was recently added in Spring Framework. This commits introduces support to allow the configuration via properties. See spring-projects/spring-framework#31380 See gh-37885 --- .../jackson/JacksonAutoConfiguration.java | 2 ++ .../jackson/JacksonProperties.java | 23 +++++++++++++++++- .../JacksonAutoConfigurationTests.java | 24 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java index a6aa97773ed3..68cc6f27e023 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java @@ -213,6 +213,8 @@ public void customize(Jackson2ObjectMapperBuilder builder) { configureFeatures(builder, this.jacksonProperties.getMapper()); configureFeatures(builder, this.jacksonProperties.getParser()); configureFeatures(builder, this.jacksonProperties.getGenerator()); + configureFeatures(builder, this.jacksonProperties.getEnumDatatype()); + configureFeatures(builder, this.jacksonProperties.getJsonNodeDatatype()); configureDateFormat(builder); configurePropertyNamingStrategy(builder); configureModules(builder); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java index 805604e98308..a886d7a92241 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -29,6 +29,8 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.cfg.EnumFeature; +import com.fasterxml.jackson.databind.cfg.JsonNodeFeature; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -38,6 +40,7 @@ * @author Andy Wilkinson * @author Marcel Overdijk * @author Johannes Edmeier + * @author Eddú Meléndez * @since 1.2.0 */ @ConfigurationProperties(prefix = "spring.jackson") @@ -86,6 +89,16 @@ public class JacksonProperties { */ private final Map generator = new EnumMap<>(JsonGenerator.Feature.class); + /** + * Jackson on/off features for enum types. + */ + private final Map enumDatatype = new EnumMap<>(EnumFeature.class); + + /** + * Jackson on/off features for JsonNode types. + */ + private final Map jsonNodeDatatype = new EnumMap<>(JsonNodeFeature.class); + /** * Controls the inclusion of properties during serialization. Configured with one of * the values in Jackson's JsonInclude.Include enumeration. @@ -154,6 +167,14 @@ public Map getGenerator() { return this.generator; } + public Map getEnumDatatype() { + return this.enumDatatype; + } + + public Map getJsonNodeDatatype() { + return this.jsonNodeDatatype; + } + public JsonInclude.Include getDefaultPropertyInclusion() { return this.defaultPropertyInclusion; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java index 3c14adc3ef3f..3528fc8afd6b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java @@ -45,6 +45,8 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.cfg.ConstructorDetector; import com.fasterxml.jackson.databind.cfg.ConstructorDetector.SingleArgConstructor; +import com.fasterxml.jackson.databind.cfg.EnumFeature; +import com.fasterxml.jackson.databind.cfg.JsonNodeFeature; import com.fasterxml.jackson.databind.exc.InvalidFormatException; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.util.StdDateFormat; @@ -88,6 +90,7 @@ * @author Johannes Edmeier * @author Grzegorz Poznachowski * @author Ralf Ueberfuhr + * @author Eddú Meléndez */ class JacksonAutoConfigurationTests { @@ -289,6 +292,27 @@ void defaultObjectMapperBuilder() { }); } + @Test + void enableEnumFeature() { + this.contextRunner.withPropertyValues("spring.jackson.enum-data-type.write_enums_to_lowercase:true") + .run((context) -> { + ObjectMapper mapper = context.getBean(ObjectMapper.class); + assertThat(EnumFeature.WRITE_ENUMS_TO_LOWERCASE.enabledByDefault()).isFalse(); + assertThat(mapper.getSerializationConfig().isEnabled(EnumFeature.WRITE_ENUMS_TO_LOWERCASE)).isTrue(); + }); + } + + @Test + void disableJsonNodeFeature() { + this.contextRunner.withPropertyValues("spring.jackson.json-node-data-type.write_null_properties:false") + .run((context) -> { + ObjectMapper mapper = context.getBean(ObjectMapper.class); + assertThat(JsonNodeFeature.WRITE_NULL_PROPERTIES.enabledByDefault()).isTrue(); + assertThat(mapper.getDeserializationConfig().isEnabled(JsonNodeFeature.WRITE_NULL_PROPERTIES)) + .isFalse(); + }); + } + @Test void moduleBeansAndWellKnownModulesAreRegisteredWithTheObjectMapperBuilder() { this.contextRunner.withUserConfiguration(ModuleConfig.class).run((context) -> { From d796087dfae4f15f1f893f70fd8bb72ceb25098c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Oct 2023 15:12:01 +0100 Subject: [PATCH 0631/1215] Polish "Add properties for configuring EnumFeature and JsonNodeFeature" See gh-37885 --- .../jackson/JacksonAutoConfiguration.java | 4 +- .../jackson/JacksonProperties.java | 46 +++++++++++-------- ...itional-spring-configuration-metadata.json | 4 ++ .../JacksonAutoConfigurationTests.java | 4 +- .../src/docs/asciidoc/howto/spring-mvc.adoc | 10 +++- 5 files changed, 45 insertions(+), 23 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java index 68cc6f27e023..5410786e4385 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java @@ -213,8 +213,8 @@ public void customize(Jackson2ObjectMapperBuilder builder) { configureFeatures(builder, this.jacksonProperties.getMapper()); configureFeatures(builder, this.jacksonProperties.getParser()); configureFeatures(builder, this.jacksonProperties.getGenerator()); - configureFeatures(builder, this.jacksonProperties.getEnumDatatype()); - configureFeatures(builder, this.jacksonProperties.getJsonNodeDatatype()); + configureFeatures(builder, this.jacksonProperties.getDatatype().getEnum()); + configureFeatures(builder, this.jacksonProperties.getDatatype().getJsonNode()); configureDateFormat(builder); configurePropertyNamingStrategy(builder); configureModules(builder); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java index a886d7a92241..fe3a67e028a2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java @@ -89,16 +89,6 @@ public class JacksonProperties { */ private final Map generator = new EnumMap<>(JsonGenerator.Feature.class); - /** - * Jackson on/off features for enum types. - */ - private final Map enumDatatype = new EnumMap<>(EnumFeature.class); - - /** - * Jackson on/off features for JsonNode types. - */ - private final Map jsonNodeDatatype = new EnumMap<>(JsonNodeFeature.class); - /** * Controls the inclusion of properties during serialization. Configured with one of * the values in Jackson's JsonInclude.Include enumeration. @@ -127,6 +117,8 @@ public class JacksonProperties { */ private Locale locale; + private final Datatype datatype = new Datatype(); + public String getDateFormat() { return this.dateFormat; } @@ -167,14 +159,6 @@ public Map getGenerator() { return this.generator; } - public Map getEnumDatatype() { - return this.enumDatatype; - } - - public Map getJsonNodeDatatype() { - return this.jsonNodeDatatype; - } - public JsonInclude.Include getDefaultPropertyInclusion() { return this.defaultPropertyInclusion; } @@ -215,6 +199,10 @@ public void setLocale(Locale locale) { this.locale = locale; } + public Datatype getDatatype() { + return this.datatype; + } + public enum ConstructorDetectorStrategy { /** @@ -240,4 +228,26 @@ public enum ConstructorDetectorStrategy { } + public static class Datatype { + + /** + * Jackson on/off features for enums. + */ + private final Map enumFeatures = new EnumMap<>(EnumFeature.class); + + /** + * Jackson on/off features for JsonNodes. + */ + private final Map jsonNode = new EnumMap<>(JsonNodeFeature.class); + + public Map getEnum() { + return this.enumFeatures; + } + + public Map getJsonNode() { + return this.jsonNode; + } + + } + } 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 5f8c301fd27d..7d21dfd05e2d 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 @@ -1559,6 +1559,10 @@ "name": "spring.jackson.constructor-detector", "defaultValue": "default" }, + { + "name": "spring.jackson.datatype.enum", + "description": "Jackson on/off features for enums." + }, { "name": "spring.jackson.joda-date-time-format", "type": "java.lang.String", diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java index 3528fc8afd6b..d11ba22e93b7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java @@ -294,7 +294,7 @@ void defaultObjectMapperBuilder() { @Test void enableEnumFeature() { - this.contextRunner.withPropertyValues("spring.jackson.enum-data-type.write_enums_to_lowercase:true") + this.contextRunner.withPropertyValues("spring.jackson.datatype.enum.write-enums-to-lowercase=true") .run((context) -> { ObjectMapper mapper = context.getBean(ObjectMapper.class); assertThat(EnumFeature.WRITE_ENUMS_TO_LOWERCASE.enabledByDefault()).isFalse(); @@ -304,7 +304,7 @@ void enableEnumFeature() { @Test void disableJsonNodeFeature() { - this.contextRunner.withPropertyValues("spring.jackson.json-node-data-type.write_null_properties:false") + this.contextRunner.withPropertyValues("spring.jackson.datatype.jsonnode.write-null-properties:false") .run((context) -> { ObjectMapper mapper = context.getBean(ObjectMapper.class); assertThat(JsonNodeFeature.WRITE_NULL_PROPERTIES.enabledByDefault()).isTrue(); diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/spring-mvc.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/spring-mvc.adoc index 038d07338e52..a5a4911773f3 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/spring-mvc.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/spring-mvc.adoc @@ -64,11 +64,19 @@ Spring Boot also has some features to make it easier to customize this behavior. You can configure the `ObjectMapper` and `XmlMapper` instances by using the environment. Jackson provides an extensive suite of on/off features that can be used to configure various aspects of its processing. -These features are described in six enums (in Jackson) that map onto properties in the environment: +These features are described in several enums (in Jackson) that map onto properties in the environment: |=== | Enum | Property | Values +| `com.fasterxml.jackson.databind.cfg.EnumFeature` +| `spring.jackson.datatype.enum.` +| `true`, `false` + +| `com.fasterxml.jackson.databind.cfg.JsonNodeFeature` +| `spring.jackson.datatype.jsonnode.` +| `true`, `false` + | `com.fasterxml.jackson.databind.DeserializationFeature` | `spring.jackson.deserialization.` | `true`, `false` From d6a9295c2641af08a225be76aef50e01f0823ebb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Oct 2023 16:55:31 +0100 Subject: [PATCH 0632/1215] Allow additional build info properties to have provided values Closes gh-37889 --- .../src/docs/asciidoc/integrating-with-actuator.adoc | 2 ++ .../boot/gradle/tasks/buildinfo/BuildInfoProperties.java | 8 +++++++- ...ildInfoDslIntegrationTests-additionalProperties.gradle | 3 ++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/integrating-with-actuator.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/integrating-with-actuator.adoc index 1d016b8a9bdf..4232d13f7f73 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/integrating-with-actuator.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/integrating-with-actuator.adoc @@ -95,3 +95,5 @@ include::../gradle/integrating-with-actuator/build-info-additional.gradle[tags=a ---- include::../gradle/integrating-with-actuator/build-info-additional.gradle.kts[tags=additional] ---- + +An additional property's value can be computed lazily by using a `Provider`. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoProperties.java index 5ace201ad715..e75f1a3352e4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoProperties.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoProperties.java @@ -30,6 +30,7 @@ import org.gradle.api.Project; import org.gradle.api.provider.MapProperty; import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; import org.gradle.api.provider.SetProperty; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Internal; @@ -155,7 +156,12 @@ private T getIfNotExcluded(Property property, String name, Supplier de private Map coerceToStringValues(Map input) { Map output = new HashMap<>(); - input.forEach((key, value) -> output.put(key, (value != null) ? value.toString() : null)); + input.forEach((key, value) -> { + if (value instanceof Provider provider) { + value = provider.getOrNull(); + } + output.put(key, (value != null) ? value.toString() : null); + }); return output; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-additionalProperties.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-additionalProperties.gradle index 0567e3acb71c..d8d2b8d319e6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-additionalProperties.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-additionalProperties.gradle @@ -10,7 +10,8 @@ springBoot { buildInfo { properties { additional = [ - 'a': 'alpha', 'b': 'bravo' + 'a': 'alpha', + 'b': providers.provider({'bravo'}) ] } } From 9f224ff1364928e48c8efd9fe4be3e4ac474ae72 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Oct 2023 18:18:34 +0100 Subject: [PATCH 0633/1215] Narrow the scope of 0e3a196 to Resource[] for array binding See gh-15835 --- .../boot/context/properties/bind/BindConverter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java index d98c31135c7a..d56c03e312d2 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java @@ -198,7 +198,7 @@ private static class TypeConverterConverter implements ConditionalGenericConvert @Override public Set getConvertibleTypes() { return Set.of(new ConvertiblePair(String.class, Object.class), - new ConvertiblePair(String.class, Object[].class), + new ConvertiblePair(String.class, Resource[].class), new ConvertiblePair(String.class, Collection.class)); } From fe752dedef152435e35afb752a67e47b4e64e216 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 16 Oct 2023 15:50:58 -0700 Subject: [PATCH 0634/1215] Polish adoc formatting --- .../spring-boot-docs/src/docs/asciidoc/features/ssl.adoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/ssl.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/ssl.adoc index 23f7ee3635e0..32e0e0efd603 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/ssl.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/ssl.adoc @@ -105,9 +105,10 @@ The following example shows retrieving an `SslBundle` and using it to create an include::code:MyComponent[] + + [[features.ssl.reloading]] === Reloading SSL bundles - SSL bundles can be reloaded when the key material changes. The component consuming the bundle has to be compatible with reloadable SSL bundles. Currently the following components are compatible: From 33c5e1269aea741f743d5c2597c9d5c57f3ad357 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 9 Oct 2023 18:37:10 -0700 Subject: [PATCH 0635/1215] Write signature files to uber jars to for Oracle Java 17 verification Update Gradle and Maven plugins to write an empty `META-INF/BOOT.SF` file whenever there is a nested signed jar. This update allows Oracle Java 17 to correctly verify the nested JARs. The file is required because `JarVerifier` has code roughly equivalent to: if (!jarManifestNameChecked && SharedSecrets .getJavaUtilZipFileAccess().getManifestName(jf, true) == null) { throw new JarException("The JCE Provider " + jarURL.toString() + " is not signed."); } The `SharedSecrets.getJavaUtilZipFileAccess().getManifestName(jf, true)` call ends up in `ZipFile.getManifestName(onlyIfSignatureRelatedFiles)` which is a private method that we cannot override in our `NestedJarFile` subclass. By writing an empty `.SF` file we ensure that the `Manifest` is always returned because there are always "signature related files". Fixes gh-28837 --- .../tasks/bundling/BootArchiveSupport.java | 10 +-- .../boot/gradle/tasks/bundling/BootJar.java | 6 +- .../boot/gradle/tasks/bundling/BootWar.java | 6 +- .../tasks/bundling/BootZipCopyAction.java | 22 ++++++- .../bundling/BootJarIntegrationTests.java | 12 ++++ .../BootJarIntegrationTests-signed.gradle | 17 +++++ .../boot/loader/tools/FileUtils.java | 32 +++++++++- .../boot/loader/tools/Packager.java | 5 ++ .../boot/loader/tools/Repackager.java | 21 ++++++- .../loader/tools/AbstractPackagerTests.java | 4 +- .../boot/loader/tools/FileUtilsTests.java | 28 +++++++++ .../boot/loader/tools/RepackagerTests.java | 15 +++++ .../boot/loader/tools/signed-manifest.mf | 9 +++ .../boot/maven/JarIntegrationTests.java | 8 +++ .../src/intTest/projects/jar-signed/pom.xml | 62 +++++++++++++++++++ .../main/java/org/test/SampleApplication.java | 24 +++++++ .../boot/loader/LoaderIntegrationTests.java | 2 - 17 files changed, 266 insertions(+), 17 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-signed.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/signed-manifest.mf create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-signed/pom.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-signed/src/main/java/org/test/SampleApplication.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java index 782eb730d2ed..330bc1aef1cd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java @@ -123,12 +123,13 @@ private String determineSpringBootVersion() { } CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies, - LoaderImplementation loaderImplementation) { - return createCopyAction(jar, resolvedDependencies, loaderImplementation, null, null); + LoaderImplementation loaderImplementation, boolean supportsSignatureFile) { + return createCopyAction(jar, resolvedDependencies, loaderImplementation, supportsSignatureFile, null, null); } CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies, - LoaderImplementation loaderImplementation, LayerResolver layerResolver, String layerToolsLocation) { + LoaderImplementation loaderImplementation, boolean supportsSignatureFile, LayerResolver layerResolver, + String layerToolsLocation) { File output = jar.getArchiveFile().get().getAsFile(); Manifest manifest = jar.getManifest(); boolean preserveFileTimestamps = jar.isPreserveFileTimestamps(); @@ -143,7 +144,8 @@ CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies, String encoding = jar.getMetadataCharset(); CopyAction action = new BootZipCopyAction(output, manifest, preserveFileTimestamps, dirMode, fileMode, includeDefaultLoader, layerToolsLocation, requiresUnpack, exclusions, launchScript, librarySpec, - compressionResolver, encoding, resolvedDependencies, layerResolver, loaderImplementation); + compressionResolver, encoding, resolvedDependencies, supportsSignatureFile, layerResolver, + loaderImplementation); return jar.isReproducibleFileOrder() ? new ReproducibleOrderingCopyAction(action) : action; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java index c76a95f1d6cc..7ed3f998c54f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java @@ -147,10 +147,10 @@ protected CopyAction createCopyAction() { if (!isLayeredDisabled()) { LayerResolver layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary); String layerToolsLocation = this.layered.getIncludeLayerTools().get() ? LIB_DIRECTORY : null; - return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, layerResolver, - layerToolsLocation); + return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, true, + layerResolver, layerToolsLocation); } - return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation); + return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, true); } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java index d3aa0eab860c..d19f152f84b6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java @@ -121,10 +121,10 @@ protected CopyAction createCopyAction() { if (!isLayeredDisabled()) { LayerResolver layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary); String layerToolsLocation = this.layered.getIncludeLayerTools().get() ? LIB_DIRECTORY : null; - return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, layerResolver, - layerToolsLocation); + return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, false, + layerResolver, layerToolsLocation); } - return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation); + return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, false); } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java index 2d8b53ea73a7..85f509ecf76c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java @@ -111,6 +111,8 @@ class BootZipCopyAction implements CopyAction { private final ResolvedDependencies resolvedDependencies; + private final boolean supportsSignatureFile; + private final LayerResolver layerResolver; private final LoaderImplementation loaderImplementation; @@ -119,7 +121,7 @@ class BootZipCopyAction implements CopyAction { boolean includeDefaultLoader, String layerToolsLocation, Spec requiresUnpack, Spec exclusions, LaunchScriptConfiguration launchScript, Spec librarySpec, Function compressionResolver, String encoding, - ResolvedDependencies resolvedDependencies, LayerResolver layerResolver, + ResolvedDependencies resolvedDependencies, boolean supportsSignatureFile, LayerResolver layerResolver, LoaderImplementation loaderImplementation) { this.output = output; this.manifest = manifest; @@ -135,6 +137,7 @@ class BootZipCopyAction implements CopyAction { this.compressionResolver = compressionResolver; this.encoding = encoding; this.resolvedDependencies = resolvedDependencies; + this.supportsSignatureFile = supportsSignatureFile; this.layerResolver = layerResolver; this.loaderImplementation = loaderImplementation; } @@ -302,6 +305,7 @@ private String getParentDirectory(String name) { void finish() throws IOException { writeLoaderEntriesIfNecessary(null); writeJarToolsIfNecessary(); + writeSignatureFileIfNecessary(); writeClassPathIndexIfNecessary(); writeNativeImageArgFileIfNecessary(); // We must write the layer index last @@ -351,6 +355,22 @@ private void writeJarModeLibrary(String location, JarModeLibrary library) throws } } + private void writeSignatureFileIfNecessary() throws IOException { + if (BootZipCopyAction.this.supportsSignatureFile && hasSignedLibrary()) { + writeEntry("META-INF/BOOT.SF", (out) -> { + }, false); + } + } + + private boolean hasSignedLibrary() throws IOException { + for (FileCopyDetails writtenLibrary : this.writtenLibraries.values()) { + if (FileUtils.isSignedJarFile(writtenLibrary.getFile())) { + return true; + } + } + return false; + } + private void writeClassPathIndexIfNecessary() throws IOException { Attributes manifestAttributes = BootZipCopyAction.this.manifest.getAttributes(); String classPathIndex = (String) manifestAttributes.get("Spring-Boot-Classpath-Index"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java index 146fb595ae60..d83e54ed165c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java @@ -16,12 +16,15 @@ package org.springframework.boot.gradle.tasks.bundling; +import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.Set; import java.util.TreeSet; +import java.util.jar.JarFile; import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.TaskOutcome; import org.junit.jupiter.api.TestTemplate; import org.springframework.boot.gradle.junit.GradleCompatibility; @@ -42,6 +45,15 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { super("bootJar", "BOOT-INF/lib/", "BOOT-INF/classes/", "BOOT-INF/"); } + @TestTemplate + void signed() throws Exception { + assertThat(this.gradleBuild.build("bootJar").task(":bootJar").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + File jar = new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0]; + try (JarFile jarFile = new JarFile(jar)) { + assertThat(jarFile.getEntry("META-INF/BOOT.SF")).isNotNull(); + } + } + @TestTemplate void whenAResolvableCopyOfAnUnresolvableConfigurationIsResolvedThenResolutionSucceeds() { this.gradleBuild.expectDeprecationWarningsWithAtLeastVersion("8.0").build("build"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-signed.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-signed.gradle new file mode 100644 index 000000000000..e879cc96e8a0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-signed.gradle @@ -0,0 +1,17 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +bootJar { + mainClass = 'com.example.Application' +} + +repositories { + mavenCentral() + maven { url "file:repository" } +} + +dependencies { + implementation("org.bouncycastle:bcprov-jdk18on:1.76") +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/FileUtils.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/FileUtils.java index ecfded739077..0347e1cbe6f4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/FileUtils.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/FileUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -18,6 +18,9 @@ import java.io.File; import java.io.IOException; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; /** * Utilities for manipulating files and directories in Spring Boot tooling. @@ -61,4 +64,31 @@ public static String sha1Hash(File file) throws IOException { return Digest.sha1(InputStreamSupplier.forFile(file)); } + /** + * Returns {@code true} if the given jar file has been signed. + * @param file the file to check + * @return if the file has been signed + * @throws IOException on IO error + */ + public static boolean isSignedJarFile(File file) throws IOException { + try (JarFile jarFile = new JarFile(file)) { + if (hasDigestEntry(jarFile.getManifest())) { + return true; + } + } + return false; + } + + private static boolean hasDigestEntry(Manifest manifest) { + return (manifest != null) && manifest.getEntries().values().stream().anyMatch(FileUtils::hasDigestName); + } + + private static boolean hasDigestName(Attributes attributes) { + return attributes.keySet().stream().anyMatch(FileUtils::isDigestName); + } + + private static boolean isDigestName(Object name) { + return String.valueOf(name).toUpperCase().endsWith("-DIGEST"); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java index af4dff233012..b04ac4501543 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java @@ -217,6 +217,7 @@ private void write(JarFile sourceJar, AbstractJarWriter writer, PackagedLibrarie if (isLayered()) { writeLayerIndex(writer); } + writeSignatureFileIfNecessary(writtenLibraries, writer); } private void writeLoaderClasses(AbstractJarWriter writer) throws IOException { @@ -263,6 +264,10 @@ private void writeLayerIndex(AbstractJarWriter writer) throws IOException { } } + protected void writeSignatureFileIfNecessary(Map writtenLibraries, AbstractJarWriter writer) + throws IOException { + } + private EntryTransformer getEntityTransformer() { if (getLayout() instanceof RepackagingLayout repackagingLayout) { return new RepackagingEntryTransformer(repackagingLayout); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java index 07da873c83a4..764c84f9fde8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Repackager.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -19,6 +19,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.attribute.FileTime; +import java.util.Map; import java.util.jar.JarFile; import org.springframework.util.Assert; @@ -46,6 +47,24 @@ public Repackager(File source) { super(source); } + @Override + protected void writeSignatureFileIfNecessary(Map writtenLibraries, AbstractJarWriter writer) + throws IOException { + if (getSource().getName().toLowerCase().endsWith(".jar") && hasSignedLibrary(writtenLibraries)) { + writer.writeEntry("META-INF/BOOT.SF", (entryWriter) -> { + }); + } + } + + private boolean hasSignedLibrary(Map writtenLibraries) throws IOException { + for (Library library : writtenLibraries.values()) { + if (!(library instanceof JarModeLibrary) && FileUtils.isSignedJarFile(library.getFile())) { + return true; + } + } + return false; + } + /** * Sets if source files should be backed up when they would be overwritten. * @param backupSource if source files should be backed up diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java index c4986aba5685..1193ce35959b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java @@ -660,7 +660,7 @@ private File createLibraryJar() throws IOException { return library.getFile(); } - private Library newLibrary(File file, LibraryScope scope, boolean unpackRequired) { + protected Library newLibrary(File file, LibraryScope scope, boolean unpackRequired) { return new Library(null, file, scope, null, unpackRequired, false, true); } @@ -687,7 +687,7 @@ protected boolean hasPackagedLauncherClasses() throws IOException { && hasPackagedEntry("org/springframework/boot/loader/launch/JarLauncher.class"); } - private boolean hasPackagedEntry(String name) throws IOException { + protected boolean hasPackagedEntry(String name) throws IOException { return getPackagedEntry(name) != null; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/FileUtilsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/FileUtilsTests.java index edf8b38b889a..e6e084bc4d6f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/FileUtilsTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/FileUtilsTests.java @@ -17,9 +17,13 @@ package org.springframework.boot.loader.tools; import java.io.File; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -99,4 +103,28 @@ void hash() throws Exception { assertThat(FileUtils.sha1Hash(file)).isEqualTo("7037807198c22a7d2b0807371d763779a84fdfcf"); } + @Test + void isSignedJarFileWhenSignedReturnsTrue() throws IOException { + Manifest manifest = new Manifest(getClass().getResourceAsStream("signed-manifest.mf")); + File jarFile = new File(this.tempDir, "test.jar"); + writeTestJar(manifest, jarFile); + assertThat(FileUtils.isSignedJarFile(jarFile)).isTrue(); + } + + @Test + void isSignedJarFileWhenNotSignedReturnsFalse() throws IOException { + Manifest manifest = new Manifest(); + File jarFile = new File(this.tempDir, "test.jar"); + writeTestJar(manifest, jarFile); + assertThat(FileUtils.isSignedJarFile(jarFile)).isFalse(); + } + + private void writeTestJar(Manifest manifest, File jarFile) throws IOException, FileNotFoundException { + try (JarOutputStream out = new JarOutputStream(new FileOutputStream(jarFile))) { + out.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF")); + manifest.write(out); + out.closeEntry(); + } + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java index f1dd7d583d25..239c0cc381e4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/RepackagerTests.java @@ -28,6 +28,7 @@ import java.util.Collection; import java.util.Enumeration; import java.util.List; +import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.Manifest; @@ -218,6 +219,20 @@ void repackagingDeeplyNestedPackageIsNotProhibitivelySlow() throws IOException { assertThat(stopWatch.getTotalTimeMillis()).isLessThan(5000); } + @Test + void signedJar() throws Exception { + Repackager packager = createPackager(); + packager.setMainClass("a.b.C"); + Manifest manifest = new Manifest(); + Attributes attributes = new Attributes(); + attributes.putValue("SHA1-Digest", "0000"); + manifest.getEntries().put("a/b/C.class", attributes); + TestJarFile libJar = new TestJarFile(this.tempDir); + libJar.addManifest(manifest); + execute(packager, (callback) -> callback.library(newLibrary(libJar.getFile(), LibraryScope.COMPILE, false))); + assertThat(hasPackagedEntry("META-INF/BOOT.SF")).isTrue(); + } + private boolean hasLauncherClasses(File file) throws IOException { return hasEntry(file, "org/springframework/boot/") && hasEntry(file, "org/springframework/boot/loader/launch/JarLauncher.class"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/signed-manifest.mf b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/signed-manifest.mf new file mode 100644 index 000000000000..8316a0550d50 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/resources/org/springframework/boot/loader/tools/signed-manifest.mf @@ -0,0 +1,9 @@ +Manifest-Version: 1.0 +Created-By: 1.5.0_08 (Sun Microsystems Inc.) +Specification-Version: 1.1 + +Name: org/bouncycastle/pqc/legacy/math/linearalgebra/GoppaCode.class +SHA-256-Digest: wNhEfeTvNG9ggqKfLjQDDoFoDqeWwGUc47JiL7VqxqU= + +Name: org/bouncycastle/crypto/modes/gcm/Tables8kGCMMultiplier.class +SHA-256-Digest: nqljr9DNx4nNie4sbkZajVenvd3LdMF3X5s5dmSMToM= diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java index c903da23a2cd..b89459cdf340 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java @@ -459,4 +459,12 @@ void whenJarIsRepackagedWithOutputTimestampConfiguredThenLibrariesAreSorted(Mave }); } + @TestTemplate + void whenSigned(MavenBuild mavenBuild) { + mavenBuild.project("jar-signed").execute((project) -> { + File repackaged = new File(project, "target/jar-signed-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar(repackaged)).hasEntryWithName("META-INF/BOOT.SF"); + }); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-signed/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-signed/pom.xml new file mode 100644 index 000000000000..375d3c60b3dc --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-signed/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-signed + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + + + + org.apache.maven.plugins + maven-jar-plugin + @maven-jar-plugin.version@ + + + + some.random.Main + + + Foo + + + + + + + + + org.springframework + spring-context + @spring-framework.version@ + + + jakarta.servlet + jakarta.servlet-api + @jakarta-servlet.version@ + provided + + + org.bouncycastle + bcprov-jdk18on + 1.76 + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-signed/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-signed/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..5e51546d4e0d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-signed/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2023 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.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java index ac6592bc9d6d..3151e334d9e2 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java @@ -37,7 +37,6 @@ import org.springframework.util.Assert; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assumptions.assumeThat; /** * Integration tests loader that supports fat jars. @@ -66,7 +65,6 @@ void readUrlsWithoutWarning(JavaRuntime javaRuntime) { @ParameterizedTest @MethodSource("javaRuntimes") void runSignedJar(JavaRuntime javaRuntime) { - assumeThat(javaRuntime.toString()).isNotEqualTo("Oracle JDK 17"); // gh-28837 try (GenericContainer container = createContainer(javaRuntime, "spring-boot-loader-tests-signed-jar", null)) { container.start(); From fb2ce355af1f45acbc46b77f3becc9d28f7dea15 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 17 Oct 2023 09:03:08 +0100 Subject: [PATCH 0636/1215] Upgrade to GraphQL Java 21.2 Closes gh-37906 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e4ab5ec66a1a..91d3e2b40624 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -323,7 +323,7 @@ bom { ] } } - library("GraphQL Java", "21.1") { + library("GraphQL Java", "21.2") { prohibit { startsWith(["2018-", "2019-", "2020-", "2021-", "230521-"]) because "These are snapshots that we don't want to see" From 613cc4f5b9fd51dd84e97b8a702edc3cf6cc062a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 17 Oct 2023 09:03:13 +0100 Subject: [PATCH 0637/1215] Upgrade to Log4j2 2.21.0 Closes gh-37907 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 91d3e2b40624..c6a36d1624c1 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -838,7 +838,7 @@ bom { ] } } - library("Log4j2", "2.20.0") { + library("Log4j2", "2.21.0") { group("org.apache.logging.log4j") { imports = [ "log4j-bom" From 4e21be9086fba3286713c8728407bb9d71363c65 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 17 Oct 2023 09:03:18 +0100 Subject: [PATCH 0638/1215] Upgrade to OkHttp 4.12.0 Closes gh-37908 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c6a36d1624c1..35032e75c49c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1083,7 +1083,7 @@ bom { ] } } - library("OkHttp", "4.11.0") { + library("OkHttp", "4.12.0") { group("com.squareup.okhttp3") { imports = [ "okhttp-bom" From d5b1acc41fe6e833b9580307ae80d9300fa39c16 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 17 Oct 2023 09:03:19 +0100 Subject: [PATCH 0639/1215] Upgrade to Spring AMQP 3.1.0-RC1 Closes gh-37706 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 35032e75c49c..7fce5673369f 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1493,7 +1493,7 @@ bom { ] } } - library("Spring AMQP", "3.1.0-SNAPSHOT") { + library("Spring AMQP", "3.1.0-RC1") { considerSnapshots() group("org.springframework.amqp") { imports = [ From d546936d53cd531dccfb53f122a67919676fba22 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 17 Oct 2023 09:03:19 +0100 Subject: [PATCH 0640/1215] Upgrade to Spring Kafka 3.1.0-RC1 Closes gh-37712 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 7fce5673369f..5a75538973c1 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1560,7 +1560,7 @@ bom { ] } } - library("Spring Kafka", "3.1.0-SNAPSHOT") { + library("Spring Kafka", "3.1.0-RC1") { considerSnapshots() group("org.springframework.kafka") { modules = [ From de91771cb3ec72766a09b7d88867b674771a5f3c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 17 Oct 2023 09:03:20 +0100 Subject: [PATCH 0641/1215] Upgrade to Spring Security 6.2.0-RC2 Closes gh-37715 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5a75538973c1..157b1d8023a8 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1606,7 +1606,7 @@ bom { ] } } - library("Spring Security", "6.2.0-M3") { + library("Spring Security", "6.2.0-RC2") { considerSnapshots() group("org.springframework.security") { imports = [ From 84d83c1964e71e88c07fb5b72fe742f0aa35e806 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 17 Oct 2023 14:24:41 +0100 Subject: [PATCH 0642/1215] Upgrade to Elasticsearch Client 8.10.4 Closes gh-37912 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 157b1d8023a8..139d63314ec4 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -264,7 +264,7 @@ bom { ] } } - library("Elasticsearch Client", "8.10.3") { + library("Elasticsearch Client", "8.10.4") { group("org.elasticsearch.client") { modules = [ "elasticsearch-rest-client" { From f0cdc57a2c05508566c5636abbd2f032df999d78 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 17 Oct 2023 14:24:42 +0100 Subject: [PATCH 0643/1215] Upgrade to Spring Authorization Server 1.2.0-RC1 Closes gh-37707 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 139d63314ec4..2cc4d573e9ba 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1501,7 +1501,7 @@ bom { ] } } - library("Spring Authorization Server", "1.2.0-SNAPSHOT") { + library("Spring Authorization Server", "1.2.0-RC1") { considerSnapshots() group("org.springframework.security") { modules = [ From 8b115c8ceb858957a5ee8f22c182709ee759360f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 17 Oct 2023 14:24:42 +0100 Subject: [PATCH 0644/1215] Upgrade to Spring Session 3.2.0-RC1 Closes gh-37716 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 2cc4d573e9ba..575906031dba 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1614,7 +1614,7 @@ bom { ] } } - library("Spring Session", "3.2.0-SNAPSHOT") { + library("Spring Session", "3.2.0-RC1") { considerSnapshots() prohibit { startsWith(["Apple-", "Bean-", "Corn-", "Dragonfruit-"]) From 02c49b02870f15a9ed58f0707fea83b3b01841bf Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 17 Oct 2023 13:31:33 +0100 Subject: [PATCH 0645/1215] When virtual threads are enabled, configure Pulsar to use them Closes gh-36347 --- .../pulsar/PulsarAutoConfiguration.java | 13 +++++- .../pulsar/PulsarAutoConfigurationTests.java | 45 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfiguration.java index 9ed6ae3b09e7..c60565b5918f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfiguration.java @@ -31,10 +31,13 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.thread.Threading; import org.springframework.boot.util.LambdaSafe; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.core.env.Environment; +import org.springframework.core.task.VirtualThreadTaskExecutor; import org.springframework.pulsar.annotation.EnablePulsar; import org.springframework.pulsar.config.ConcurrentPulsarListenerContainerFactory; import org.springframework.pulsar.config.DefaultPulsarReaderContainerFactory; @@ -149,10 +152,13 @@ private void applyConsumerBuilderCustomizers(List> @ConditionalOnMissingBean(name = "pulsarListenerContainerFactory") ConcurrentPulsarListenerContainerFactory pulsarListenerContainerFactory( PulsarConsumerFactory pulsarConsumerFactory, SchemaResolver schemaResolver, - TopicResolver topicResolver) { + TopicResolver topicResolver, Environment environment) { PulsarContainerProperties containerProperties = new PulsarContainerProperties(); containerProperties.setSchemaResolver(schemaResolver); containerProperties.setTopicResolver(topicResolver); + if (Threading.VIRTUAL.isActive(environment)) { + containerProperties.setConsumerTaskExecutor(new VirtualThreadTaskExecutor()); + } this.propertiesMapper.customizeContainerProperties(containerProperties); return new ConcurrentPulsarListenerContainerFactory<>(pulsarConsumerFactory, containerProperties); } @@ -178,9 +184,12 @@ private void applyReaderBuilderCustomizers(List> cust @Bean @ConditionalOnMissingBean(name = "pulsarReaderContainerFactory") DefaultPulsarReaderContainerFactory pulsarReaderContainerFactory(PulsarReaderFactory pulsarReaderFactory, - SchemaResolver schemaResolver) { + SchemaResolver schemaResolver, Environment environment) { PulsarReaderContainerProperties readerContainerProperties = new PulsarReaderContainerProperties(); readerContainerProperties.setSchemaResolver(schemaResolver); + if (Threading.VIRTUAL.isActive(environment)) { + readerContainerProperties.setReaderTaskExecutor(new VirtualThreadTaskExecutor()); + } this.propertiesMapper.customizeReaderContainerProperties(readerContainerProperties); return new DefaultPulsarReaderContainerFactory<>(pulsarReaderFactory, readerContainerProperties); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java index 16e66c9a4737..7e56f4129ead 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java @@ -29,6 +29,8 @@ import org.apache.pulsar.common.schema.SchemaType; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -39,6 +41,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; +import org.springframework.core.task.VirtualThreadTaskExecutor; import org.springframework.pulsar.annotation.PulsarBootstrapConfiguration; import org.springframework.pulsar.annotation.PulsarListenerAnnotationBeanPostProcessor; import org.springframework.pulsar.annotation.PulsarReaderAnnotationBeanPostProcessor; @@ -464,6 +467,27 @@ void whenObservationsDisabledDoesNotEnableObservation() { .hasFieldOrPropertyWithValue("containerProperties.observationEnabled", false)); } + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void whenVirtualThreadsAreEnabledOnJava21AndLaterListenerContainerShouldUseVirtualThreads() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> { + ConcurrentPulsarListenerContainerFactory factory = context + .getBean(ConcurrentPulsarListenerContainerFactory.class); + assertThat(factory.getContainerProperties().getConsumerTaskExecutor()) + .isInstanceOf(VirtualThreadTaskExecutor.class); + }); + } + + @Test + @EnabledForJreRange(max = JRE.JAVA_20) + void whenVirtualThreadsAreEnabledOnJava20AndEarlierListenerContainerShouldNotUseVirtualThreads() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> { + ConcurrentPulsarListenerContainerFactory factory = context + .getBean(ConcurrentPulsarListenerContainerFactory.class); + assertThat(factory.getContainerProperties().getConsumerTaskExecutor()).isNull(); + }); + } + } @Nested @@ -498,6 +522,27 @@ void whenHasUserDefinedCustomizersAppliesInCorrectOrder() { }); } + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void whenVirtualThreadsAreEnabledOnJava21AndLaterReaderShouldUseVirtualThreads() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> { + DefaultPulsarReaderContainerFactory factory = context + .getBean(DefaultPulsarReaderContainerFactory.class); + assertThat(factory.getContainerProperties().getReaderTaskExecutor()) + .isInstanceOf(VirtualThreadTaskExecutor.class); + }); + } + + @Test + @EnabledForJreRange(max = JRE.JAVA_20) + void whenVirtualThreadsAreEnabledOnJava20AndEarlierReaderShouldNotUseVirtualThreads() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> { + DefaultPulsarReaderContainerFactory factory = context + .getBean(DefaultPulsarReaderContainerFactory.class); + assertThat(factory.getContainerProperties().getReaderTaskExecutor()).isNull(); + }); + } + @TestConfiguration(proxyBeanMethods = false) static class ReaderBuilderCustomizersConfig { From daa903ab318283b0b5960d988a35dc51b16f77fb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 17 Oct 2023 17:54:36 +0100 Subject: [PATCH 0646/1215] Add filters to MockMvc with their init params and dispatcher types Closes gh-37835 --- .../SpringBootMockMvcBuilderCustomizer.java | 8 ++--- ...ringBootMockMvcBuilderCustomizerTests.java | 27 ++++++++++++--- .../AbstractFilterRegistrationBean.java | 34 +++++++++++++------ 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java index d739a2766b4a..357be7e93bde 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java @@ -116,12 +116,8 @@ private void addFilters(ConfigurableMockMvcBuilder builder) { private void addFilter(ConfigurableMockMvcBuilder builder, AbstractFilterRegistrationBean registration) { Filter filter = registration.getFilter(); Collection urls = registration.getUrlPatterns(); - if (urls.isEmpty()) { - builder.addFilters(filter); - } - else { - builder.addFilter(filter, StringUtils.toStringArray(urls)); - } + builder.addFilter(filter, registration.getInitParameters(), registration.determineDispatcherTypes(), + StringUtils.toStringArray(urls)); } public void setAddFilters(boolean addFilters) { diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java index f4ddd7165428..f3e7ffac61fd 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java @@ -18,16 +18,21 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; import java.util.List; +import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import jakarta.servlet.DispatcherType; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServlet; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.SpringBootMockMvcBuilderCustomizer.DeferredLinesWriter; @@ -37,11 +42,12 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.mock.web.MockServletContext; -import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import static org.assertj.core.api.Assertions.as; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; /** * Tests for {@link SpringBootMockMvcBuilderCustomizer}. @@ -51,7 +57,6 @@ class SpringBootMockMvcBuilderCustomizerTests { @Test - @SuppressWarnings("unchecked") void customizeShouldAddFilters() { AnnotationConfigServletWebApplicationContext context = new AnnotationConfigServletWebApplicationContext(); MockServletContext servletContext = new MockServletContext(); @@ -65,8 +70,11 @@ void customizeShouldAddFilters() { .getBean("filterRegistrationBean"); Filter testFilter = (Filter) context.getBean("testFilter"); Filter otherTestFilter = registrationBean.getFilter(); - List filters = (List) ReflectionTestUtils.getField(builder, "filters"); - assertThat(filters).containsExactlyInAnyOrder(testFilter, otherTestFilter); + assertThat(builder).extracting("filters", as(InstanceOfAssertFactories.LIST)) + .extracting("delegate", "initParams", "dispatcherTypes") + .containsExactlyInAnyOrder(tuple(testFilter, Collections.emptyMap(), EnumSet.of(DispatcherType.REQUEST)), + tuple(otherTestFilter, Map.of("a", "alpha", "b", "bravo"), + EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR))); } @Test @@ -130,7 +138,11 @@ static class FilterConfiguration { @Bean FilterRegistrationBean filterRegistrationBean() { - return new FilterRegistrationBean<>(new OtherTestFilter()); + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>( + new OtherTestFilter()); + filterRegistrationBean.setInitParameters(Map.of("a", "alpha", "b", "bravo")); + filterRegistrationBean.setDispatcherTypes(EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR)); + return filterRegistrationBean; } @Bean @@ -182,4 +194,9 @@ public void destroy() { } + static record RegisteredFilter(Filter filter, Map initParameters, + EnumSet dispatcherTypes) { + + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/AbstractFilterRegistrationBean.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/AbstractFilterRegistrationBean.java index 71f8c5086dcf..d7202385c54d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/AbstractFilterRegistrationBean.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/AbstractFilterRegistrationBean.java @@ -158,6 +158,28 @@ public void addUrlPatterns(String... urlPatterns) { Collections.addAll(this.urlPatterns, urlPatterns); } + /** + * Determines the {@link DispatcherType dispatcher types} for which the filter should + * be registered. Applies defaults based on the type of filter being registered if + * none have been configured. Modifications to the returned {@link EnumSet} will have + * no effect on the registration. + * @return the dispatcher types, never {@code null} + * @since 3.2.0 + */ + public EnumSet determineDispatcherTypes() { + if (this.dispatcherTypes == null) { + T filter = getFilter(); + if (ClassUtils.isPresent("org.springframework.web.filter.OncePerRequestFilter", + filter.getClass().getClassLoader()) && filter instanceof OncePerRequestFilter) { + return EnumSet.allOf(DispatcherType.class); + } + else { + return EnumSet.of(DispatcherType.REQUEST); + } + } + return EnumSet.copyOf(this.dispatcherTypes); + } + /** * Convenience method to {@link #setDispatcherTypes(EnumSet) set dispatcher types} * using the specified elements. @@ -216,17 +238,7 @@ protected Dynamic addRegistration(String description, ServletContext servletCont @Override protected void configure(FilterRegistration.Dynamic registration) { super.configure(registration); - EnumSet dispatcherTypes = this.dispatcherTypes; - if (dispatcherTypes == null) { - T filter = getFilter(); - if (ClassUtils.isPresent("org.springframework.web.filter.OncePerRequestFilter", - filter.getClass().getClassLoader()) && filter instanceof OncePerRequestFilter) { - dispatcherTypes = EnumSet.allOf(DispatcherType.class); - } - else { - dispatcherTypes = EnumSet.of(DispatcherType.REQUEST); - } - } + EnumSet dispatcherTypes = determineDispatcherTypes(); Set servletNames = new LinkedHashSet<>(); for (ServletRegistrationBean servletRegistrationBean : this.servletRegistrationBeans) { servletNames.add(servletRegistrationBean.getServletName()); From 4d0e614f5f9abcce5e81705453f7f2a52b018a57 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 18 Oct 2023 07:58:38 +0100 Subject: [PATCH 0647/1215] Upgrade to Spring Integration 6.2.0-RC1 Closes gh-37711 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 575906031dba..a01ea428f38c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1552,7 +1552,7 @@ bom { ] } } - library("Spring Integration", "6.2.0-SNAPSHOT") { + library("Spring Integration", "6.2.0-RC1") { considerSnapshots() group("org.springframework.integration") { imports = [ From fa954c0c87d4dfbfb9722286038c4d5ba7fe66c4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 18 Oct 2023 07:59:28 +0100 Subject: [PATCH 0648/1215] Upgrade to Spring Pulsar 1.0.0-RC1 Closes gh-37918 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index a01ea428f38c..e60ee7fc5210 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1580,7 +1580,7 @@ bom { ] } } - library("Spring Pulsar", "1.0.0-M2") { + library("Spring Pulsar", "1.0.0-RC1") { group("org.springframework.pulsar") { modules = [ "spring-pulsar", From cdf42bd70b535652bf67a582184b1354162da117 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 18 Oct 2023 11:40:23 +0100 Subject: [PATCH 0649/1215] Upgrade to Kotlin 1.9.20-RC Closes gh-37926 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a520f2c15434..7f2a839097cc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ commonsCodecVersion=1.16.0 hamcrestVersion=2.2 jacksonVersion=2.15.3 junitJupiterVersion=5.10.0 -kotlinVersion=1.9.10 +kotlinVersion=1.9.20-RC mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.27 springFrameworkVersion=6.1.0-RC1 From 79ab4c4fa787d8a9c46b52ae9db600977bd08d53 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 18 Oct 2023 12:40:39 +0100 Subject: [PATCH 0650/1215] Upgrade to Undertow 2.3.10.Final Closes gh-37934 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e60ee7fc5210..fc1566173124 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1701,7 +1701,7 @@ bom { ] } } - library("Undertow", "2.3.9.Final") { + library("Undertow", "2.3.10.Final") { group("io.undertow") { modules = [ "undertow-core", From 1559485f5602021cfbca76688c1e8474694edef3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 18 Oct 2023 14:07:49 +0100 Subject: [PATCH 0651/1215] Fix Gradle plugin test classpath after Kotlin upgrade See gh-37926 --- .../boot/testsupport/gradle/testkit/GradleBuild.java | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-test-support/src/main/java/org/springframework/boot/testsupport/gradle/testkit/GradleBuild.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-test-support/src/main/java/org/springframework/boot/testsupport/gradle/testkit/GradleBuild.java index 22b8b43a2c02..6fb983549f13 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-test-support/src/main/java/org/springframework/boot/testsupport/gradle/testkit/GradleBuild.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-test-support/src/main/java/org/springframework/boot/testsupport/gradle/testkit/GradleBuild.java @@ -118,7 +118,6 @@ private List pluginClasspath() { new File(pathOfJarContaining(ClassVisitor.class)), new File(pathOfJarContaining(DependencyManagementPlugin.class)), new File(pathOfJarContaining("org.jetbrains.kotlin.cli.common.PropertiesKt")), - new File(pathOfJarContaining("org.jetbrains.kotlin.compilerRunner.KotlinLogger")), new File(pathOfJarContaining(KotlinPlatformJvmPlugin.class)), new File(pathOfJarContaining(KotlinProject.class)), new File(pathOfJarContaining(KotlinToolingVersion.class)), From 851e6def764bb0ba27bd2514bbb58a9a54f940c2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 18 Oct 2023 14:08:41 +0100 Subject: [PATCH 0652/1215] Fix tests on Windows See gh-37808 --- .../autoconfigure/ssl/SslPropertiesBundleRegistrar.java | 8 +++----- .../boot/autoconfigure/ssl/FileWatcherTests.java | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java index 531fb8d574f5..810ff772a39f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java @@ -16,8 +16,6 @@ package org.springframework.boot.autoconfigure.ssl; -import java.io.FileNotFoundException; -import java.io.UncheckedIOException; import java.net.URL; import java.nio.file.Path; import java.util.LinkedHashSet; @@ -106,10 +104,10 @@ private Path toPath(String bundleName, Location watchableLocation) { URL url = ResourceUtils.getURL(value); Assert.state("file".equalsIgnoreCase(url.getProtocol()), () -> "SSL bundle '%s' '%s' URL '%s' doesn't point to a file".formatted(bundleName, field, url)); - return Path.of(url.getFile()).toAbsolutePath(); + return Path.of(url.toURI()).toAbsolutePath(); } - catch (FileNotFoundException ex) { - throw new UncheckedIOException( + catch (Exception ex) { + throw new RuntimeException( "SSL bundle '%s' '%s' location '%s' cannot be watched".formatted(bundleName, field, value), ex); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/FileWatcherTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/FileWatcherTests.java index e680479d0e45..ef09b28481d1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/FileWatcherTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/FileWatcherTests.java @@ -111,7 +111,7 @@ void shouldFailIfDirectoryOrFileDoesNotExist(@TempDir Path tempDir) { Path directory = tempDir.resolve("dir1"); assertThatExceptionOfType(UncheckedIOException.class) .isThrownBy(() -> this.fileWatcher.watch(Set.of(directory), new WaitingCallback())) - .withMessageMatching("Failed to register paths for watching: \\[.+/dir1]"); + .withMessage("Failed to register paths for watching: [%s]".formatted(directory)); } @Test From 1aa704b619a3324e60d51c08c973b2740d2c69b3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 18 Oct 2023 15:47:27 +0100 Subject: [PATCH 0653/1215] Upgrade to Spring Batch 5.1.0-RC1 Closes gh-37708 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index fc1566173124..99cad246324a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1509,7 +1509,7 @@ bom { ] } } - library("Spring Batch", "5.1.0-SNAPSHOT") { + library("Spring Batch", "5.1.0-RC1") { considerSnapshots() group("org.springframework.batch") { imports = [ From ec6415f04bf3e3ec47ab9600c744e5199ca85be0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 18 Oct 2023 16:29:29 -0500 Subject: [PATCH 0654/1215] Add SSL bundle support to Apache Kafka auto-configuration Closes gh-37629 Co-authored-by: Scott Frederick --- .../KafkaAnnotationDrivenConfiguration.java | 10 +- .../kafka/KafkaAutoConfiguration.java | 15 +- .../autoconfigure/kafka/KafkaProperties.java | 189 ++++++++++++++---- ...aStreamsAnnotationDrivenConfiguration.java | 6 +- .../kafka/SslBundleSslEngineFactory.java | 95 +++++++++ ...afkaAutoConfigurationIntegrationTests.java | 3 + .../kafka/KafkaAutoConfigurationTests.java | 4 +- .../kafka/KafkaPropertiesTests.java | 59 +++++- 8 files changed, 325 insertions(+), 56 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/SslBundleSslEngineFactory.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAnnotationDrivenConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAnnotationDrivenConfiguration.java index f7a103305f05..abbc834f466f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAnnotationDrivenConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAnnotationDrivenConfiguration.java @@ -23,6 +23,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading; import org.springframework.boot.autoconfigure.thread.Threading; +import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.task.SimpleAsyncTaskExecutor; @@ -53,6 +54,8 @@ * @author Eddú Meléndez * @author Thomas Kåsene * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Scott Frederick */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(EnableKafka.class) @@ -149,10 +152,11 @@ private ConcurrentKafkaListenerContainerFactoryConfigurer configurer() { ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory( ConcurrentKafkaListenerContainerFactoryConfigurer configurer, ObjectProvider> kafkaConsumerFactory, - ObjectProvider>> kafkaContainerCustomizer) { + ObjectProvider>> kafkaContainerCustomizer, + ObjectProvider sslBundles) { ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); - configurer.configure(factory, kafkaConsumerFactory - .getIfAvailable(() -> new DefaultKafkaConsumerFactory<>(this.properties.buildConsumerProperties()))); + configurer.configure(factory, kafkaConsumerFactory.getIfAvailable(() -> new DefaultKafkaConsumerFactory<>( + this.properties.buildConsumerProperties(sslBundles.getIfAvailable())))); kafkaContainerCustomizer.ifAvailable(factory::setContainerCustomizer); return factory; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfiguration.java index 9d73f58cd56c..9e2b2f22c8c5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfiguration.java @@ -35,6 +35,7 @@ import org.springframework.boot.autoconfigure.kafka.KafkaProperties.Retry.Topic; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.kafka.core.ConsumerFactory; @@ -64,6 +65,8 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Andy Wilkinson + * @author Scott Frederick * @since 1.5.0 */ @AutoConfiguration @@ -107,8 +110,8 @@ public LoggingProducerListener kafkaProducerListener() { @Bean @ConditionalOnMissingBean(ConsumerFactory.class) public DefaultKafkaConsumerFactory kafkaConsumerFactory(KafkaConnectionDetails connectionDetails, - ObjectProvider customizers) { - Map properties = this.properties.buildConsumerProperties(); + ObjectProvider customizers, ObjectProvider sslBundles) { + Map properties = this.properties.buildConsumerProperties(sslBundles.getIfAvailable()); applyKafkaConnectionDetailsForConsumer(properties, connectionDetails); DefaultKafkaConsumerFactory factory = new DefaultKafkaConsumerFactory<>(properties); customizers.orderedStream().forEach((customizer) -> customizer.customize(factory)); @@ -118,8 +121,8 @@ public LoggingProducerListener kafkaProducerListener() { @Bean @ConditionalOnMissingBean(ProducerFactory.class) public DefaultKafkaProducerFactory kafkaProducerFactory(KafkaConnectionDetails connectionDetails, - ObjectProvider customizers) { - Map properties = this.properties.buildProducerProperties(); + ObjectProvider customizers, ObjectProvider sslBundles) { + Map properties = this.properties.buildProducerProperties(sslBundles.getIfAvailable()); applyKafkaConnectionDetailsForProducer(properties, connectionDetails); DefaultKafkaProducerFactory factory = new DefaultKafkaProducerFactory<>(properties); String transactionIdPrefix = this.properties.getProducer().getTransactionIdPrefix(); @@ -155,8 +158,8 @@ public KafkaJaasLoginModuleInitializer kafkaJaasInitializer() throws IOException @Bean @ConditionalOnMissingBean - public KafkaAdmin kafkaAdmin(KafkaConnectionDetails connectionDetails) { - Map properties = this.properties.buildAdminProperties(); + public KafkaAdmin kafkaAdmin(KafkaConnectionDetails connectionDetails, ObjectProvider sslBundles) { + Map properties = this.properties.buildAdminProperties(sslBundles.getIfAvailable()); applyKafkaConnectionDetailsForAdmin(properties, connectionDetails); KafkaAdmin kafkaAdmin = new KafkaAdmin(properties); KafkaProperties.Admin admin = this.properties.getAdmin(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java index 7227df43f233..a7da43eea30e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java @@ -38,6 +38,8 @@ import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException; import org.springframework.boot.convert.DurationUnit; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; import org.springframework.core.io.Resource; import org.springframework.kafka.listener.ContainerProperties.AckMode; import org.springframework.kafka.security.jaas.KafkaJaasLoginModuleInitializer; @@ -55,6 +57,8 @@ * @author Artem Bilan * @author Nakul Mishra * @author Tomaz Fernandes + * @author Andy Wilkinson + * @author Scott Frederick * @since 1.5.0 */ @ConfigurationProperties(prefix = "spring.kafka") @@ -157,7 +161,7 @@ public Retry getRetry() { return this.retry; } - private Map buildCommonProperties() { + private Map buildCommonProperties(SslBundles sslBundles) { Map properties = new HashMap<>(); if (this.bootstrapServers != null) { properties.put(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, this.bootstrapServers); @@ -165,7 +169,7 @@ private Map buildCommonProperties() { if (this.clientId != null) { properties.put(CommonClientConfigs.CLIENT_ID_CONFIG, this.clientId); } - properties.putAll(this.ssl.buildProperties()); + properties.putAll(this.ssl.buildProperties(sslBundles)); properties.putAll(this.security.buildProperties()); if (!CollectionUtils.isEmpty(this.properties)) { properties.putAll(this.properties); @@ -177,13 +181,29 @@ private Map buildCommonProperties() { * Create an initial map of consumer properties from the state of this instance. *

    * This allows you to add additional properties, if necessary, and override the - * default kafkaConsumerFactory bean. + * default {@code kafkaConsumerFactory} bean. * @return the consumer properties initialized with the customizations defined on this * instance + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of + * {@link #buildConsumerProperties(SslBundles)}} */ + @Deprecated(since = "3.2.0", forRemoval = true) public Map buildConsumerProperties() { - Map properties = buildCommonProperties(); - properties.putAll(this.consumer.buildProperties()); + return buildConsumerProperties(null); + } + + /** + * Create an initial map of consumer properties from the state of this instance. + *

    + * This allows you to add additional properties, if necessary, and override the + * default {@code kafkaConsumerFactory} bean. + * @param sslBundles bundles providing SSL trust material + * @return the consumer properties initialized with the customizations defined on this + * instance + */ + public Map buildConsumerProperties(SslBundles sslBundles) { + Map properties = buildCommonProperties(sslBundles); + properties.putAll(this.consumer.buildProperties(sslBundles)); return properties; } @@ -191,13 +211,29 @@ public Map buildConsumerProperties() { * Create an initial map of producer properties from the state of this instance. *

    * This allows you to add additional properties, if necessary, and override the - * default kafkaProducerFactory bean. + * default {@code kafkaProducerFactory} bean. * @return the producer properties initialized with the customizations defined on this * instance + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of + * {@link #buildProducerProperties(SslBundles)}} */ + @Deprecated(since = "3.2.0", forRemoval = true) public Map buildProducerProperties() { - Map properties = buildCommonProperties(); - properties.putAll(this.producer.buildProperties()); + return buildProducerProperties(null); + } + + /** + * Create an initial map of producer properties from the state of this instance. + *

    + * This allows you to add additional properties, if necessary, and override the + * default {@code kafkaProducerFactory} bean. + * @param sslBundles bundles providing SSL trust material + * @return the producer properties initialized with the customizations defined on this + * instance + */ + public Map buildProducerProperties(SslBundles sslBundles) { + Map properties = buildCommonProperties(sslBundles); + properties.putAll(this.producer.buildProperties(sslBundles)); return properties; } @@ -205,13 +241,29 @@ public Map buildProducerProperties() { * Create an initial map of admin properties from the state of this instance. *

    * This allows you to add additional properties, if necessary, and override the - * default kafkaAdmin bean. + * default {@code kafkaAdmin} bean. * @return the admin properties initialized with the customizations defined on this * instance + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of + * {@link #buildAdminProperties(SslBundles)}} */ + @Deprecated(since = "3.2.0", forRemoval = true) public Map buildAdminProperties() { - Map properties = buildCommonProperties(); - properties.putAll(this.admin.buildProperties()); + return buildAdminProperties(null); + } + + /** + * Create an initial map of admin properties from the state of this instance. + *

    + * This allows you to add additional properties, if necessary, and override the + * default {@code kafkaAdmin} bean. + * @param sslBundles bundles providing SSL trust material + * @return the admin properties initialized with the customizations defined on this + * instance + */ + public Map buildAdminProperties(SslBundles sslBundles) { + Map properties = buildCommonProperties(sslBundles); + properties.putAll(this.admin.buildProperties(sslBundles)); return properties; } @@ -221,10 +273,25 @@ public Map buildAdminProperties() { * This allows you to add additional properties, if necessary. * @return the streams properties initialized with the customizations defined on this * instance + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of + * {@link #buildStreamsProperties(SslBundles)}} */ + @Deprecated(since = "3.2.0", forRemoval = true) public Map buildStreamsProperties() { - Map properties = buildCommonProperties(); - properties.putAll(this.streams.buildProperties()); + return buildStreamsProperties(null); + } + + /** + * Create an initial map of streams properties from the state of this instance. + *

    + * This allows you to add additional properties, if necessary. + * @param sslBundles bundles providing SSL trust material + * @return the streams properties initialized with the customizations defined on this + * instance + */ + public Map buildStreamsProperties(SslBundles sslBundles) { + Map properties = buildCommonProperties(sslBundles); + properties.putAll(this.streams.buildProperties(sslBundles)); return properties; } @@ -426,7 +493,7 @@ public Map getProperties() { return this.properties; } - public Map buildProperties() { + public Map buildProperties(SslBundles sslBundles) { Properties properties = new Properties(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(this::getAutoCommitInterval) @@ -451,7 +518,7 @@ public Map buildProperties() { map.from(this::getKeyDeserializer).to(properties.in(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG)); map.from(this::getValueDeserializer).to(properties.in(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG)); map.from(this::getMaxPollRecords).to(properties.in(ConsumerConfig.MAX_POLL_RECORDS_CONFIG)); - return properties.with(this.ssl, this.security, this.properties); + return properties.with(this.ssl, this.security, this.properties, sslBundles); } } @@ -613,7 +680,7 @@ public Map getProperties() { return this.properties; } - public Map buildProperties() { + public Map buildProperties(SslBundles sslBundles) { Properties properties = new Properties(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(this::getAcks).to(properties.in(ProducerConfig.ACKS_CONFIG)); @@ -627,7 +694,7 @@ public Map buildProperties() { map.from(this::getKeySerializer).to(properties.in(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG)); map.from(this::getRetries).to(properties.in(ProducerConfig.RETRIES_CONFIG)); map.from(this::getValueSerializer).to(properties.in(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG)); - return properties.with(this.ssl, this.security, this.properties); + return properties.with(this.ssl, this.security, this.properties, sslBundles); } } @@ -734,11 +801,11 @@ public Map getProperties() { return this.properties; } - public Map buildProperties() { + public Map buildProperties(SslBundles sslBundles) { Properties properties = new Properties(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(this::getClientId).to(properties.in(ProducerConfig.CLIENT_ID_CONFIG)); - return properties.with(this.ssl, this.security, this.properties); + return properties.with(this.ssl, this.security, this.properties, sslBundles); } } @@ -885,7 +952,7 @@ public Map getProperties() { return this.properties; } - public Map buildProperties() { + public Map buildProperties(SslBundles sslBundles) { Properties properties = new Properties(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(this::getApplicationId).to(properties.in("application.id")); @@ -899,7 +966,7 @@ public Map buildProperties() { map.from(this::getClientId).to(properties.in(CommonClientConfigs.CLIENT_ID_CONFIG)); map.from(this::getReplicationFactor).to(properties.in("replication.factor")); map.from(this::getStateDir).to(properties.in("state.dir")); - return properties.with(this.ssl, this.security, this.properties); + return properties.with(this.ssl, this.security, this.properties, sslBundles); } } @@ -1198,6 +1265,11 @@ public void setChangeConsumerThreadName(Boolean changeConsumerThreadName) { public static class Ssl { + /** + * Name of the SSL bundle to use. + */ + private String bundle; + /** * Password of the private key in either key store key or key store file. */ @@ -1253,6 +1325,14 @@ public static class Ssl { */ private String protocol; + public String getBundle() { + return this.bundle; + } + + public void setBundle(String bundle) { + this.bundle = bundle; + } + public String getKeyPassword() { return this.keyPassword; } @@ -1341,26 +1421,39 @@ public void setProtocol(String protocol) { this.protocol = protocol; } + @Deprecated(since = "3.2.0", forRemoval = true) public Map buildProperties() { + return buildProperties(null); + } + + public Map buildProperties(SslBundles sslBundles) { validate(); Properties properties = new Properties(); - PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(this::getKeyPassword).to(properties.in(SslConfigs.SSL_KEY_PASSWORD_CONFIG)); - map.from(this::getKeyStoreCertificateChain) - .to(properties.in(SslConfigs.SSL_KEYSTORE_CERTIFICATE_CHAIN_CONFIG)); - map.from(this::getKeyStoreKey).to(properties.in(SslConfigs.SSL_KEYSTORE_KEY_CONFIG)); - map.from(this::getKeyStoreLocation) - .as(this::resourceToPath) - .to(properties.in(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG)); - map.from(this::getKeyStorePassword).to(properties.in(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG)); - map.from(this::getKeyStoreType).to(properties.in(SslConfigs.SSL_KEYSTORE_TYPE_CONFIG)); - map.from(this::getTrustStoreCertificates).to(properties.in(SslConfigs.SSL_TRUSTSTORE_CERTIFICATES_CONFIG)); - map.from(this::getTrustStoreLocation) - .as(this::resourceToPath) - .to(properties.in(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG)); - map.from(this::getTrustStorePassword).to(properties.in(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG)); - map.from(this::getTrustStoreType).to(properties.in(SslConfigs.SSL_TRUSTSTORE_TYPE_CONFIG)); - map.from(this::getProtocol).to(properties.in(SslConfigs.SSL_PROTOCOL_CONFIG)); + if (getBundle() != null) { + properties.in(SslConfigs.SSL_ENGINE_FACTORY_CLASS_CONFIG) + .accept(SslBundleSslEngineFactory.class.getName()); + properties.in(SslBundle.class.getName()).accept(sslBundles.getBundle(getBundle())); + } + else { + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(this::getKeyPassword).to(properties.in(SslConfigs.SSL_KEY_PASSWORD_CONFIG)); + map.from(this::getKeyStoreCertificateChain) + .to(properties.in(SslConfigs.SSL_KEYSTORE_CERTIFICATE_CHAIN_CONFIG)); + map.from(this::getKeyStoreKey).to(properties.in(SslConfigs.SSL_KEYSTORE_KEY_CONFIG)); + map.from(this::getKeyStoreLocation) + .as(this::resourceToPath) + .to(properties.in(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG)); + map.from(this::getKeyStorePassword).to(properties.in(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG)); + map.from(this::getKeyStoreType).to(properties.in(SslConfigs.SSL_KEYSTORE_TYPE_CONFIG)); + map.from(this::getTrustStoreCertificates) + .to(properties.in(SslConfigs.SSL_TRUSTSTORE_CERTIFICATES_CONFIG)); + map.from(this::getTrustStoreLocation) + .as(this::resourceToPath) + .to(properties.in(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG)); + map.from(this::getTrustStorePassword).to(properties.in(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG)); + map.from(this::getTrustStoreType).to(properties.in(SslConfigs.SSL_TRUSTSTORE_TYPE_CONFIG)); + map.from(this::getProtocol).to(properties.in(SslConfigs.SSL_PROTOCOL_CONFIG)); + } return properties; } @@ -1373,6 +1466,22 @@ private void validate() { entries.put("spring.kafka.ssl.trust-store-certificates", getTrustStoreCertificates()); entries.put("spring.kafka.ssl.trust-store-location", getTrustStoreLocation()); }); + MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn((entries) -> { + entries.put("spring.kafka.ssl.bundle", getBundle()); + entries.put("spring.kafka.ssl.key-store-key", getKeyStoreKey()); + }); + MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn((entries) -> { + entries.put("spring.kafka.ssl.bundle", getBundle()); + entries.put("spring.kafka.ssl.key-store-location", getKeyStoreLocation()); + }); + MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn((entries) -> { + entries.put("spring.kafka.ssl.bundle", getBundle()); + entries.put("spring.kafka.ssl.trust-store-certificates", getTrustStoreCertificates()); + }); + MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn((entries) -> { + entries.put("spring.kafka.ssl.bundle", getBundle()); + entries.put("spring.kafka.ssl.trust-store-location", getTrustStoreLocation()); + }); } private String resourceToPath(Resource resource) { @@ -1628,8 +1737,8 @@ java.util.function.Consumer in(String key) { return (value) -> put(key, value); } - Properties with(Ssl ssl, Security security, Map properties) { - putAll(ssl.buildProperties()); + Properties with(Ssl ssl, Security security, Map properties, SslBundles sslBundles) { + putAll(ssl.buildProperties(sslBundles)); putAll(security.buildProperties()); putAll(properties); return this; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaStreamsAnnotationDrivenConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaStreamsAnnotationDrivenConfiguration.java index 28e35d3699f4..701384326303 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaStreamsAnnotationDrivenConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaStreamsAnnotationDrivenConfiguration.java @@ -30,6 +30,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; +import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; @@ -46,6 +47,7 @@ * @author Eddú Meléndez * @author Moritz Halbritter * @author Andy Wilkinson + * @author Scott Frederick */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(StreamsBuilder.class) @@ -61,8 +63,8 @@ class KafkaStreamsAnnotationDrivenConfiguration { @ConditionalOnMissingBean @Bean(KafkaStreamsDefaultConfiguration.DEFAULT_STREAMS_CONFIG_BEAN_NAME) KafkaStreamsConfiguration defaultKafkaStreamsConfig(Environment environment, - KafkaConnectionDetails connectionDetails) { - Map properties = this.properties.buildStreamsProperties(); + KafkaConnectionDetails connectionDetails, ObjectProvider sslBundles) { + Map properties = this.properties.buildStreamsProperties(sslBundles.getIfAvailable()); applyKafkaConnectionDetailsForStreams(properties, connectionDetails); if (this.properties.getStreams().getApplicationId() == null) { String applicationName = environment.getProperty("spring.application.name"); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/SslBundleSslEngineFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/SslBundleSslEngineFactory.java new file mode 100644 index 000000000000..5c5e93ebf56a --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/SslBundleSslEngineFactory.java @@ -0,0 +1,95 @@ +/* + * Copyright 2012-2023 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.kafka; + +import java.io.IOException; +import java.security.KeyStore; +import java.util.Map; +import java.util.Set; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; + +import org.apache.kafka.common.security.auth.SslEngineFactory; + +import org.springframework.boot.ssl.SslBundle; + +/** + * An {@link SslEngineFactory} that configures creates an {@link SSLEngine} from an + * {@link SslBundle}. + * + * @author Andy Wilkinson + * @author Scott Frederick + * @since 3.2.0 + */ +public class SslBundleSslEngineFactory implements SslEngineFactory { + + private static final String SSL_BUNDLE_CONFIG_NAME = SslBundle.class.getName(); + + private Map configs; + + private volatile SslBundle sslBundle; + + @Override + public void configure(Map configs) { + this.configs = configs; + this.sslBundle = (SslBundle) configs.get(SSL_BUNDLE_CONFIG_NAME); + } + + @Override + public void close() throws IOException { + + } + + @Override + public SSLEngine createClientSslEngine(String peerHost, int peerPort, String endpointIdentification) { + SSLEngine sslEngine = this.sslBundle.createSslContext().createSSLEngine(peerHost, peerPort); + sslEngine.setUseClientMode(true); + SSLParameters sslParams = sslEngine.getSSLParameters(); + sslParams.setEndpointIdentificationAlgorithm(endpointIdentification); + sslEngine.setSSLParameters(sslParams); + return sslEngine; + } + + @Override + public SSLEngine createServerSslEngine(String peerHost, int peerPort) { + SSLEngine sslEngine = this.sslBundle.createSslContext().createSSLEngine(peerHost, peerPort); + sslEngine.setUseClientMode(false); + return sslEngine; + } + + @Override + public boolean shouldBeRebuilt(Map nextConfigs) { + return !nextConfigs.equals(this.configs); + } + + @Override + public Set reconfigurableConfigs() { + return Set.of(SSL_BUNDLE_CONFIG_NAME); + } + + @Override + public KeyStore keystore() { + return this.sslBundle.getStores().getKeyStore(); + } + + @Override + public KeyStore truststore() { + return this.sslBundle.getStores().getTrustStore(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationIntegrationTests.java index 2b6d6e849a17..8b0668ffe8dd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationIntegrationTests.java @@ -33,6 +33,7 @@ import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.OS; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; @@ -58,6 +59,7 @@ * @author Gary Russell * @author Stephane Nicoll * @author Tomaz Fernandes + * @author Andy Wilkinson */ @DisabledOnOs(OS.WINDOWS) @EmbeddedKafka(topics = KafkaAutoConfigurationIntegrationTests.TEST_TOPIC) @@ -133,6 +135,7 @@ private void load(Class config, String... environment) { private AnnotationConfigApplicationContext doLoad(Class[] configs, String... environment) { AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.register(configs); + applicationContext.register(SslAutoConfiguration.class); applicationContext.register(KafkaAutoConfiguration.class); TestPropertyValues.of(environment).applyTo(applicationContext); applicationContext.refresh(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java index 516db1674508..2b8c48d3e02a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java @@ -46,6 +46,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ContextConsumer; @@ -107,11 +108,12 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ class KafkaAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(KafkaAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(KafkaAutoConfiguration.class, SslAutoConfiguration.class)); @Test void consumerProperties() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaPropertiesTests.java index 8ee0486d857b..dbd53fd59d81 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaPropertiesTests.java @@ -27,6 +27,8 @@ import org.springframework.boot.autoconfigure.kafka.KafkaProperties.IsolationLevel; import org.springframework.boot.autoconfigure.kafka.KafkaProperties.Listener; import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException; +import org.springframework.boot.ssl.DefaultSslBundleRegistry; +import org.springframework.boot.ssl.SslBundle; import org.springframework.core.io.ClassPathResource; import org.springframework.kafka.core.CleanupConfig; import org.springframework.kafka.core.KafkaAdmin; @@ -34,16 +36,19 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; /** * Tests for {@link KafkaProperties}. * * @author Stephane Nicoll * @author Madhura Bhave + * @author Scott Frederick */ class KafkaPropertiesTests { - @SuppressWarnings("rawtypes") + private final SslBundle sslBundle = mock(SslBundle.class); + @Test void isolationLevelEnumConsistentWithKafkaVersion() { org.apache.kafka.common.IsolationLevel[] original = org.apache.kafka.common.IsolationLevel.values(); @@ -75,20 +80,30 @@ void sslPemConfiguration() { properties.getSsl().setKeyStoreKey("-----BEGINkey"); properties.getSsl().setTrustStoreCertificates("-----BEGINtrust"); properties.getSsl().setKeyStoreCertificateChain("-----BEGINchain"); - Map consumerProperties = properties.buildConsumerProperties(); + Map consumerProperties = properties.buildConsumerProperties(null); assertThat(consumerProperties).containsEntry(SslConfigs.SSL_KEYSTORE_KEY_CONFIG, "-----BEGINkey"); assertThat(consumerProperties).containsEntry(SslConfigs.SSL_TRUSTSTORE_CERTIFICATES_CONFIG, "-----BEGINtrust"); assertThat(consumerProperties).containsEntry(SslConfigs.SSL_KEYSTORE_CERTIFICATE_CHAIN_CONFIG, "-----BEGINchain"); } + @Test + void sslBundleConfiguration() { + KafkaProperties properties = new KafkaProperties(); + properties.getSsl().setBundle("myBundle"); + Map consumerProperties = properties + .buildConsumerProperties(new DefaultSslBundleRegistry("myBundle", this.sslBundle)); + assertThat(consumerProperties).containsEntry(SslConfigs.SSL_ENGINE_FACTORY_CLASS_CONFIG, + SslBundleSslEngineFactory.class.getName()); + } + @Test void sslPropertiesWhenKeyStoreLocationAndKeySetShouldThrowException() { KafkaProperties properties = new KafkaProperties(); properties.getSsl().setKeyStoreKey("-----BEGIN"); properties.getSsl().setKeyStoreLocation(new ClassPathResource("ksLoc")); assertThatExceptionOfType(MutuallyExclusiveConfigurationPropertiesException.class) - .isThrownBy(properties::buildConsumerProperties); + .isThrownBy(() -> properties.buildConsumerProperties(null)); } @Test @@ -97,7 +112,43 @@ void sslPropertiesWhenTrustStoreLocationAndCertificatesSetShouldThrowException() properties.getSsl().setTrustStoreLocation(new ClassPathResource("tsLoc")); properties.getSsl().setTrustStoreCertificates("-----BEGIN"); assertThatExceptionOfType(MutuallyExclusiveConfigurationPropertiesException.class) - .isThrownBy(properties::buildConsumerProperties); + .isThrownBy(() -> properties.buildConsumerProperties(null)); + } + + @Test + void sslPropertiesWhenKeyStoreLocationAndBundleSetShouldThrowException() { + KafkaProperties properties = new KafkaProperties(); + properties.getSsl().setBundle("myBundle"); + properties.getSsl().setKeyStoreLocation(new ClassPathResource("ksLoc")); + assertThatExceptionOfType(MutuallyExclusiveConfigurationPropertiesException.class).isThrownBy( + () -> properties.buildConsumerProperties(new DefaultSslBundleRegistry("myBundle", this.sslBundle))); + } + + @Test + void sslPropertiesWhenKeyStoreKeyAndBundleSetShouldThrowException() { + KafkaProperties properties = new KafkaProperties(); + properties.getSsl().setBundle("myBundle"); + properties.getSsl().setKeyStoreKey("-----BEGIN"); + assertThatExceptionOfType(MutuallyExclusiveConfigurationPropertiesException.class).isThrownBy( + () -> properties.buildConsumerProperties(new DefaultSslBundleRegistry("myBundle", this.sslBundle))); + } + + @Test + void sslPropertiesWhenTrustStoreLocationAndBundleSetShouldThrowException() { + KafkaProperties properties = new KafkaProperties(); + properties.getSsl().setBundle("myBundle"); + properties.getSsl().setTrustStoreLocation(new ClassPathResource("tsLoc")); + assertThatExceptionOfType(MutuallyExclusiveConfigurationPropertiesException.class).isThrownBy( + () -> properties.buildConsumerProperties(new DefaultSslBundleRegistry("myBundle", this.sslBundle))); + } + + @Test + void sslPropertiesWhenTrustStoreCertificatesAndBundleSetShouldThrowException() { + KafkaProperties properties = new KafkaProperties(); + properties.getSsl().setBundle("myBundle"); + properties.getSsl().setTrustStoreCertificates("-----BEGIN"); + assertThatExceptionOfType(MutuallyExclusiveConfigurationPropertiesException.class).isThrownBy( + () -> properties.buildConsumerProperties(new DefaultSslBundleRegistry("myBundle", this.sslBundle))); } @Test From 4b495ca2a99095ef877dab49c642042edcc3432f Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 18 Oct 2023 15:00:50 -0700 Subject: [PATCH 0655/1215] Change `NestedLocation` to hold a `Path` rather than a `File` Refactor `NestedLocation` so that it holds a `Path` rather than a `File`. See gh-37668 --- .../net/protocol/jar/UrlJarFileFactory.java | 2 +- .../net/protocol/nested/NestedLocation.java | 15 ++++++++------- .../net/protocol/nested/NestedUrlConnection.java | 16 ++++++++++++---- .../nested/NestedUrlConnectionResources.java | 2 +- .../net/protocol/nested/NestedLocationTests.java | 11 ++++++----- 5 files changed, 28 insertions(+), 18 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java index 1fb8173b87a6..7a45caea98a2 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java @@ -82,7 +82,7 @@ private boolean isNestedUrl(URL url) { private JarFile createJarFileForNested(URL url, Runtime.Version version, Consumer closeAction) throws IOException { NestedLocation location = NestedLocation.fromUrl(url); - return new UrlNestedJarFile(location.file(), location.nestedEntryName(), version, closeAction); + return new UrlNestedJarFile(location.path().toFile(), location.nestedEntryName(), version, closeAction); } private JarFile createJarFileForStream(URL url, Version version, Consumer closeAction) throws IOException { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java index 3f0a016a6a6a..ff84b3bf8477 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java @@ -16,8 +16,9 @@ package org.springframework.boot.loader.net.protocol.nested; -import java.io.File; +import java.net.URI; import java.net.URL; +import java.nio.file.Path; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -43,18 +44,18 @@ * uncompressed entry that contains the nested jar, or a directory entry. The entry must * not start with a {@code '/'}. * - * @param file the zip file that contains the nested entry + * @param path the path to the zip that contains the nested entry * @param nestedEntryName the nested entry name * @author Phillip Webb * @since 3.2.0 */ -public record NestedLocation(File file, String nestedEntryName) { +public record NestedLocation(Path path, String nestedEntryName) { private static final Map cache = new ConcurrentHashMap<>(); public NestedLocation { - if (file == null) { - throw new IllegalArgumentException("'file' must not be null"); + if (path == null) { + throw new IllegalArgumentException("'path' must not be null"); } if (nestedEntryName == null || nestedEntryName.trim().isEmpty()) { throw new IllegalArgumentException("'nestedEntryName' must not be empty"); @@ -86,9 +87,9 @@ static NestedLocation parse(String path) { } private static NestedLocation create(int index, String location) { - String file = location.substring(0, index); + String path = location.substring(0, index); String nestedEntryName = location.substring(index + 2); - return new NestedLocation((!file.isEmpty()) ? new File(file) : null, nestedEntryName); + return new NestedLocation((!path.isEmpty()) ? Path.of(path) : null, nestedEntryName); } static void clearCache() { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnection.java index 308b32116d47..32051123abfc 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnection.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnection.java @@ -16,6 +16,7 @@ package org.springframework.boot.loader.net.protocol.nested; +import java.io.File; import java.io.FilePermission; import java.io.FilterInputStream; import java.io.IOException; @@ -25,6 +26,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; +import java.nio.file.Files; import java.security.Permission; import org.springframework.boot.loader.ref.Cleaner; @@ -43,7 +45,7 @@ class NestedUrlConnection extends URLConnection { private final Cleanable cleanup; - private long lastModified; + private long lastModified = -1; private FilePermission permission; @@ -91,8 +93,13 @@ public String getContentType() { @Override public long getLastModified() { - if (this.lastModified == 0) { - this.lastModified = this.resources.getLocation().file().lastModified(); + if (this.lastModified == -1) { + try { + this.lastModified = Files.getLastModifiedTime(this.resources.getLocation().path()).toMillis(); + } + catch (IOException ex) { + this.lastModified = 0; + } } return this.lastModified; } @@ -100,7 +107,8 @@ public long getLastModified() { @Override public Permission getPermission() throws IOException { if (this.permission == null) { - this.permission = new FilePermission(this.resources.getLocation().file().getCanonicalPath(), "read"); + File file = this.resources.getLocation().path().toFile(); + this.permission = new FilePermission(file.getCanonicalPath(), "read"); } return this.permission; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionResources.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionResources.java index 4582e197c464..5806c2392882 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionResources.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionResources.java @@ -51,7 +51,7 @@ NestedLocation getLocation() { void connect() throws IOException { synchronized (this) { if (this.zipContent == null) { - this.zipContent = ZipContent.open(this.location.file().toPath(), this.location.nestedEntryName()); + this.zipContent = ZipContent.open(this.location.path(), this.location.nestedEntryName()); try { connectData(); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java index 0ec9c1f66ed6..f992624c4f5d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java @@ -18,6 +18,7 @@ import java.io.File; import java.net.URL; +import java.nio.file.Path; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -44,20 +45,20 @@ static void registerHandlers() { } @Test - void createWhenFileIsNullThrowsException() { + void createWhenPathIsNullThrowsException() { assertThatIllegalArgumentException().isThrownBy(() -> new NestedLocation(null, "nested.jar")) - .withMessageContaining("'file' must not be null"); + .withMessageContaining("'path' must not be null"); } @Test void createWhenNestedEntryNameIsNullThrowsException() { - assertThatIllegalArgumentException().isThrownBy(() -> new NestedLocation(new File("test.jar"), null)) + assertThatIllegalArgumentException().isThrownBy(() -> new NestedLocation(Path.of("test.jar"), null)) .withMessageContaining("'nestedEntryName' must not be empty"); } @Test void createWhenNestedEntryNameIsEmptyThrowsException() { - assertThatIllegalArgumentException().isThrownBy(() -> new NestedLocation(new File("test.jar"), null)) + assertThatIllegalArgumentException().isThrownBy(() -> new NestedLocation(Path.of("test.jar"), null)) .withMessageContaining("'nestedEntryName' must not be empty"); } @@ -91,7 +92,7 @@ void fromUrlReturnsNestedLocation() throws Exception { File file = new File(this.temp, "test.jar"); NestedLocation location = NestedLocation .fromUrl(new URL("nested:" + file.getAbsolutePath() + "/!lib/nested.jar")); - assertThat(location.file()).isEqualTo(file); + assertThat(location.path()).isEqualTo(file.toPath()); assertThat(location.nestedEntryName()).isEqualTo("lib/nested.jar"); } From 3c62defb9db07c1e64b3d527407dbadefe08ff09 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 18 Oct 2023 14:57:05 -0700 Subject: [PATCH 0656/1215] Support java.nio.file Paths and FileSystems with nested jars Add a `NestedFileSystemProvider` implementation so that the JDK's `ZipFileSystem` can load content from nested jars and nested directory entries. Creating a `ZipFileSystem` may be a relatively expensive operation as zip structures need to be parsed and in the case of directory entries a virtual datablock nees to be generated on the fly. As such, we install the `ZipFileSystem` as late as possible since in a typical application it may never be needed. This commit also tweaks Gradle and Maven plugins to ensure that the service loader file is written to repackaged jars. Closes gh-7161 --- .../tasks/bundling/LoaderZipEntries.java | 6 +- .../boot/loader/tools/AbstractJarWriter.java | 6 +- .../net/protocol/nested/NestedLocation.java | 13 + .../loader/nio/file/NestedByteChannel.java | 176 +++++++++++ .../boot/loader/nio/file/NestedFileStore.java | 106 +++++++ .../loader/nio/file/NestedFileSystem.java | 229 +++++++++++++++ .../nio/file/NestedFileSystemProvider.java | 186 ++++++++++++ .../boot/loader/nio/file/NestedPath.java | 221 ++++++++++++++ .../boot/loader/nio/file/package-info.java | 20 ++ .../java.nio.file.spi.FileSystemProvider | 1 + .../protocol/nested/NestedLocationTests.java | 29 ++ .../nio/file/NestedByteChannelTests.java | 178 ++++++++++++ .../loader/nio/file/NestedFileStoreTests.java | 150 ++++++++++ .../file/NestedFileSystemProviderTests.java | 273 ++++++++++++++++++ .../nio/file/NestedFileSystemTests.java | 191 ++++++++++++ ...leSystemZipFileSystemIntegrationTests.java | 77 +++++ .../boot/loader/nio/file/NestedPathTests.java | 235 +++++++++++++++ .../spring-boot-loader-tests-app/build.gradle | 2 + .../boot/loaderapp/LoaderTestApplication.java | 18 ++ .../src/main/resources/gh-7161/example.txt | 0 .../boot/loader/LoaderIntegrationTests.java | 3 +- 21 files changed, 2115 insertions(+), 5 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedByteChannel.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileStore.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystem.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystemProvider.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedPath.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/package-info.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedByteChannelTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileStoreTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemProviderTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemZipFileSystemIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedPathTests.java create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/resources/gh-7161/example.txt diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java index 8a7851f07c0c..64b3ea1685ee 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java @@ -66,8 +66,8 @@ WrittenEntries writeTo(ZipArchiveOutputStream out) throws IOException { writeDirectory(new ZipArchiveEntry(entry), out); written.addDirectory(entry); } - else if (entry.getName().endsWith(".class")) { - writeClass(new ZipArchiveEntry(entry), loaderJar, out); + else if (entry.getName().endsWith(".class") || entry.getName().startsWith("META-INF/services/")) { + writeFile(new ZipArchiveEntry(entry), loaderJar, out); written.addFile(entry); } entry = loaderJar.getNextEntry(); @@ -82,7 +82,7 @@ private void writeDirectory(ZipArchiveEntry entry, ZipArchiveOutputStream out) t out.closeArchiveEntry(); } - private void writeClass(ZipArchiveEntry entry, ZipInputStream in, ZipArchiveOutputStream out) throws IOException { + private void writeFile(ZipArchiveEntry entry, ZipInputStream in, ZipArchiveOutputStream out) throws IOException { prepareEntry(entry, this.fileMode); out.putArchiveEntry(entry); copy(in, out); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java index 7e807b1f9d82..429f63c2b36f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java @@ -220,7 +220,7 @@ public void writeLoaderClasses(String loaderJarResourceName) throws IOException try (JarInputStream inputStream = new JarInputStream(new BufferedInputStream(loaderJar.openStream()))) { JarEntry entry; while ((entry = inputStream.getNextJarEntry()) != null) { - if (isDirectoryEntry(entry) || isClassEntry(entry)) { + if (isDirectoryEntry(entry) || isClassEntry(entry) || isServicesEntry(entry)) { writeEntry(new JarArchiveEntry(entry), new InputStreamEntryWriter(inputStream)); } } @@ -235,6 +235,10 @@ private boolean isClassEntry(JarEntry entry) { return entry.getName().endsWith(".class"); } + private boolean isServicesEntry(JarEntry entry) { + return !entry.isDirectory() && entry.getName().startsWith("META-INF/services/"); + } + private void writeEntry(JarArchiveEntry entry, EntryWriter entryWriter) throws IOException { writeEntry(entry, null, entryWriter, UnpackHandler.NEVER); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java index ff84b3bf8477..c37555dec87f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java @@ -75,6 +75,19 @@ public static NestedLocation fromUrl(URL url) { return parse(UrlDecoder.decode(url.getPath())); } + /** + * Create a new {@link NestedLocation} from the given URI. + * @param uri the nested URI + * @return a new {@link NestedLocation} instance + * @throws IllegalArgumentException if the URI is not valid + */ + public static NestedLocation fromUri(URI uri) { + if (uri == null || !"nested".equalsIgnoreCase(uri.getScheme())) { + throw new IllegalArgumentException("'uri' must not be null and must use 'nested' scheme"); + } + return parse(uri.getSchemeSpecificPart()); + } + static NestedLocation parse(String path) { if (path == null || path.isEmpty()) { throw new IllegalArgumentException("'path' must not be empty"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedByteChannel.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedByteChannel.java new file mode 100644 index 000000000000..e41b27fd655d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedByteChannel.java @@ -0,0 +1,176 @@ +/* + * Copyright 2012-2023 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.loader.nio.file; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.ref.Cleaner.Cleanable; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.NonWritableChannelException; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.Path; + +import org.springframework.boot.loader.net.protocol.nested.NestedLocation; +import org.springframework.boot.loader.ref.Cleaner; +import org.springframework.boot.loader.zip.CloseableDataBlock; +import org.springframework.boot.loader.zip.DataBlock; +import org.springframework.boot.loader.zip.ZipContent; + +/** + * {@link SeekableByteChannel} implementation for {@link NestedLocation nested} jar files. + * + * @author Phillip Webb + * @see NestedFileSystemProvider + */ +class NestedByteChannel implements SeekableByteChannel { + + private long position; + + private final Resources resources; + + private final Cleanable cleanup; + + private final long size; + + private volatile boolean closed; + + NestedByteChannel(Path path, String nestedEntryName) throws IOException { + this(path, nestedEntryName, Cleaner.instance); + } + + NestedByteChannel(Path path, String nestedEntryName, Cleaner cleaner) throws IOException { + this.resources = new Resources(path, nestedEntryName); + this.cleanup = cleaner.register(this, this.resources); + this.size = this.resources.getData().size(); + } + + @Override + public boolean isOpen() { + return !this.closed; + } + + @Override + public void close() throws IOException { + if (this.closed) { + return; + } + this.closed = true; + try { + this.cleanup.clean(); + } + catch (UncheckedIOException ex) { + throw ex.getCause(); + } + } + + @Override + public int read(ByteBuffer dst) throws IOException { + assertNotClosed(); + int count = this.resources.getData().read(dst, this.position); + if (count > 0) { + this.position += count; + } + return count; + } + + @Override + public int write(ByteBuffer src) throws IOException { + throw new NonWritableChannelException(); + } + + @Override + public long position() throws IOException { + assertNotClosed(); + return this.position; + } + + @Override + public SeekableByteChannel position(long position) throws IOException { + assertNotClosed(); + if (position < 0 || position >= this.size) { + throw new IllegalArgumentException("Position must be in bounds"); + } + this.position = position; + return this; + } + + @Override + public long size() throws IOException { + assertNotClosed(); + return this.size; + } + + @Override + public SeekableByteChannel truncate(long size) throws IOException { + throw new NonWritableChannelException(); + } + + private void assertNotClosed() throws ClosedChannelException { + if (this.closed) { + throw new ClosedChannelException(); + } + } + + /** + * Resources used by the channel and suitable for registration with a {@link Cleaner}. + */ + static class Resources implements Runnable { + + private final ZipContent zipContent; + + private final CloseableDataBlock data; + + Resources(Path path, String nestedEntryName) throws IOException { + this.zipContent = ZipContent.open(path, nestedEntryName); + this.data = this.zipContent.openRawZipData(); + } + + DataBlock getData() { + return this.data; + } + + @Override + public void run() { + releaseAll(); + } + + private void releaseAll() { + IOException exception = null; + try { + this.data.close(); + } + catch (IOException ex) { + exception = ex; + } + try { + this.zipContent.close(); + } + catch (IOException ex) { + if (exception != null) { + ex.addSuppressed(exception); + } + exception = ex; + } + if (exception != null) { + throw new UncheckedIOException(exception); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileStore.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileStore.java new file mode 100644 index 000000000000..c5a7edb559eb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileStore.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-2023 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.loader.nio.file; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.FileStore; +import java.nio.file.Files; +import java.nio.file.attribute.FileAttributeView; +import java.nio.file.attribute.FileStoreAttributeView; + +import org.springframework.boot.loader.net.protocol.nested.NestedLocation; + +/** + * {@link FileStore} implementation for {@link NestedLocation nested} jar files. + * + * @author Phillip Webb + * @see NestedFileSystemProvider + */ +class NestedFileStore extends FileStore { + + private final NestedFileSystem fileSystem; + + NestedFileStore(NestedFileSystem fileSystem) { + this.fileSystem = fileSystem; + } + + @Override + public String name() { + return this.fileSystem.toString(); + } + + @Override + public String type() { + return "nestedfs"; + } + + @Override + public boolean isReadOnly() { + return this.fileSystem.isReadOnly(); + } + + @Override + public long getTotalSpace() throws IOException { + return 0; + } + + @Override + public long getUsableSpace() throws IOException { + return 0; + } + + @Override + public long getUnallocatedSpace() throws IOException { + return 0; + } + + @Override + public boolean supportsFileAttributeView(Class type) { + return getJarPathFileStore().supportsFileAttributeView(type); + } + + @Override + public boolean supportsFileAttributeView(String name) { + return getJarPathFileStore().supportsFileAttributeView(name); + } + + @Override + public V getFileStoreAttributeView(Class type) { + return getJarPathFileStore().getFileStoreAttributeView(type); + } + + @Override + public Object getAttribute(String attribute) throws IOException { + try { + return getJarPathFileStore().getAttribute(attribute); + } + catch (UncheckedIOException ex) { + throw ex.getCause(); + } + } + + protected FileStore getJarPathFileStore() { + try { + return Files.getFileStore(this.fileSystem.getJarPath()); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystem.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystem.java new file mode 100644 index 000000000000..be38b10cd020 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystem.java @@ -0,0 +1,229 @@ +/* + * Copyright 2012-2023 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.loader.nio.file; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.ClosedFileSystemException; +import java.nio.file.FileStore; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.WatchService; +import java.nio.file.attribute.UserPrincipalLookupService; +import java.nio.file.spi.FileSystemProvider; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.springframework.boot.loader.net.protocol.nested.NestedLocation; + +/** + * {@link FileSystem} implementation for {@link NestedLocation nested} jar files. + * + * @author Phillip Webb + * @see NestedFileSystemProvider + */ +class NestedFileSystem extends FileSystem { + + private static final Set SUPPORTED_FILE_ATTRIBUTE_VIEWS = Set.of("basic"); + + private static final String FILE_SYSTEMS_CLASS_NAME = FileSystems.class.getName(); + + private static final Object EXISTING_FILE_SYSTEM = new Object(); + + private final NestedFileSystemProvider provider; + + private final Path jarPath; + + private volatile boolean closed; + + private final Map zipFileSystems = new HashMap<>(); + + NestedFileSystem(NestedFileSystemProvider provider, Path jarPath) { + if (provider == null || jarPath == null) { + throw new IllegalArgumentException("Provider and JarPath must not be null"); + } + this.provider = provider; + this.jarPath = jarPath; + } + + void installZipFileSystemIfNecessary(String nestedEntryName) { + try { + boolean seen; + synchronized (this.zipFileSystems) { + seen = this.zipFileSystems.putIfAbsent(nestedEntryName, EXISTING_FILE_SYSTEM) != null; + } + if (!seen) { + URI uri = new URI("jar:nested:" + this.jarPath.toUri().getPath() + "/!" + nestedEntryName); + if (!hasFileSystem(uri)) { + FileSystem zipFileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap()); + synchronized (this.zipFileSystems) { + this.zipFileSystems.put(nestedEntryName, zipFileSystem); + } + } + } + } + catch (Exception ex) { + // Ignore + } + } + + private boolean hasFileSystem(URI uri) { + try { + FileSystems.getFileSystem(uri); + return true; + } + catch (FileSystemNotFoundException ex) { + return isCreatingNewFileSystem(); + } + } + + private boolean isCreatingNewFileSystem() { + StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + if (stack != null) { + for (StackTraceElement element : stack) { + if (FILE_SYSTEMS_CLASS_NAME.equals(element.getClassName())) { + return "newFileSystem".equals(element.getMethodName()); + } + } + } + return false; + } + + @Override + public FileSystemProvider provider() { + return this.provider; + } + + Path getJarPath() { + return this.jarPath; + } + + @Override + public void close() throws IOException { + if (this.closed) { + return; + } + this.closed = true; + synchronized (this.zipFileSystems) { + this.zipFileSystems.values() + .stream() + .filter(FileSystem.class::isInstance) + .map(FileSystem.class::cast) + .forEach(this::closeZipFileSystem); + } + this.provider.removeFileSystem(this); + } + + private void closeZipFileSystem(FileSystem zipFileSystem) { + try { + zipFileSystem.close(); + } + catch (Exception ex) { + } + } + + @Override + public boolean isOpen() { + return !this.closed; + } + + @Override + public boolean isReadOnly() { + return true; + } + + @Override + public String getSeparator() { + return "/!"; + } + + @Override + public Iterable getRootDirectories() { + assertNotClosed(); + return Collections.emptySet(); + } + + @Override + public Iterable getFileStores() { + assertNotClosed(); + return Collections.emptySet(); + } + + @Override + public Set supportedFileAttributeViews() { + assertNotClosed(); + return SUPPORTED_FILE_ATTRIBUTE_VIEWS; + } + + @Override + public Path getPath(String first, String... more) { + assertNotClosed(); + if (first == null || first.isBlank() || more.length != 0) { + throw new IllegalArgumentException("Nested paths must contain a single element"); + } + return new NestedPath(this, first); + } + + @Override + public PathMatcher getPathMatcher(String syntaxAndPattern) { + throw new UnsupportedOperationException("Nested paths do not support path matchers"); + } + + @Override + public UserPrincipalLookupService getUserPrincipalLookupService() { + throw new UnsupportedOperationException("Nested paths do not have a user principal lookup service"); + } + + @Override + public WatchService newWatchService() throws IOException { + throw new UnsupportedOperationException("Nested paths do not support the WacherService"); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + NestedFileSystem other = (NestedFileSystem) obj; + return this.jarPath.equals(other.jarPath); + } + + @Override + public int hashCode() { + return this.jarPath.hashCode(); + } + + @Override + public String toString() { + return this.jarPath.toAbsolutePath().toString(); + } + + private void assertNotClosed() { + if (this.closed) { + throw new ClosedFileSystemException(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystemProvider.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystemProvider.java new file mode 100644 index 000000000000..ca136748df8c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystemProvider.java @@ -0,0 +1,186 @@ +/* + * Copyright 2012-2023 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.loader.nio.file; + +import java.io.IOException; +import java.net.URI; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.AccessMode; +import java.nio.file.CopyOption; +import java.nio.file.DirectoryStream; +import java.nio.file.DirectoryStream.Filter; +import java.nio.file.FileStore; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemAlreadyExistsException; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.LinkOption; +import java.nio.file.NotDirectoryException; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.ReadOnlyFileSystemException; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileAttributeView; +import java.nio.file.spi.FileSystemProvider; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.springframework.boot.loader.net.protocol.nested.NestedLocation; + +/** + * {@link FileSystemProvider} implementation for {@link NestedLocation nested} jar files. + * + * @author Phillip Webb + * @since 3.2.0 + */ +public class NestedFileSystemProvider extends FileSystemProvider { + + private Map fileSystems = new HashMap<>(); + + @Override + public String getScheme() { + return "nested"; + } + + @Override + public FileSystem newFileSystem(URI uri, Map env) throws IOException { + NestedLocation location = NestedLocation.fromUri(uri); + Path jarPath = location.path(); + synchronized (this.fileSystems) { + if (this.fileSystems.containsKey(jarPath)) { + throw new FileSystemAlreadyExistsException(); + } + NestedFileSystem fileSystem = new NestedFileSystem(this, location.path()); + this.fileSystems.put(location.path(), fileSystem); + return fileSystem; + } + } + + @Override + public FileSystem getFileSystem(URI uri) { + NestedLocation location = NestedLocation.fromUri(uri); + synchronized (this.fileSystems) { + NestedFileSystem fileSystem = this.fileSystems.get(location.path()); + if (fileSystem == null) { + throw new FileSystemNotFoundException(); + } + return fileSystem; + } + } + + @Override + public Path getPath(URI uri) { + NestedLocation location = NestedLocation.fromUri(uri); + synchronized (this.fileSystems) { + NestedFileSystem fileSystem = this.fileSystems.computeIfAbsent(location.path(), + (path) -> new NestedFileSystem(this, path)); + fileSystem.installZipFileSystemIfNecessary(location.nestedEntryName()); + return fileSystem.getPath(location.nestedEntryName()); + } + } + + void removeFileSystem(NestedFileSystem fileSystem) { + synchronized (this.fileSystems) { + this.fileSystems.remove(fileSystem.getJarPath()); + } + } + + @Override + public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) + throws IOException { + NestedPath nestedPath = NestedPath.cast(path); + return new NestedByteChannel(nestedPath.getJarPath(), nestedPath.getNestedEntryName()); + } + + @Override + public DirectoryStream newDirectoryStream(Path dir, Filter filter) throws IOException { + throw new NotDirectoryException(NestedPath.cast(dir).toString()); + } + + @Override + public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { + throw new ReadOnlyFileSystemException(); + } + + @Override + public void delete(Path path) throws IOException { + throw new ReadOnlyFileSystemException(); + } + + @Override + public void copy(Path source, Path target, CopyOption... options) throws IOException { + throw new ReadOnlyFileSystemException(); + } + + @Override + public void move(Path source, Path target, CopyOption... options) throws IOException { + throw new ReadOnlyFileSystemException(); + } + + @Override + public boolean isSameFile(Path path, Path path2) throws IOException { + return path.equals(path2); + } + + @Override + public boolean isHidden(Path path) throws IOException { + return false; + } + + @Override + public FileStore getFileStore(Path path) throws IOException { + NestedPath nestedPath = NestedPath.cast(path); + nestedPath.assertExists(); + return new NestedFileStore(nestedPath.getFileSystem()); + } + + @Override + public void checkAccess(Path path, AccessMode... modes) throws IOException { + Path jarPath = getJarPath(path); + jarPath.getFileSystem().provider().checkAccess(jarPath, modes); + } + + @Override + public V getFileAttributeView(Path path, Class type, LinkOption... options) { + Path jarPath = getJarPath(path); + return jarPath.getFileSystem().provider().getFileAttributeView(jarPath, type, options); + } + + @Override + public A readAttributes(Path path, Class type, LinkOption... options) + throws IOException { + Path jarPath = getJarPath(path); + return jarPath.getFileSystem().provider().readAttributes(jarPath, type, options); + } + + @Override + public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException { + Path jarPath = getJarPath(path); + return jarPath.getFileSystem().provider().readAttributes(jarPath, attributes, options); + } + + protected Path getJarPath(Path path) { + return NestedPath.cast(path).getJarPath(); + } + + @Override + public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException { + throw new ReadOnlyFileSystemException(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedPath.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedPath.java new file mode 100644 index 000000000000..163af41784a9 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedPath.java @@ -0,0 +1,221 @@ +/* + * Copyright 2012-2023 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.loader.nio.file; + +import java.io.IOError; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.ProviderMismatchException; +import java.nio.file.WatchEvent.Kind; +import java.nio.file.WatchEvent.Modifier; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.util.Objects; + +import org.springframework.boot.loader.net.protocol.nested.NestedLocation; +import org.springframework.boot.loader.zip.ZipContent; + +/** + * {@link Path} implementation for {@link NestedLocation nested} jar files. + * + * @author Phillip Webb + * @see NestedFileSystemProvider + */ +final class NestedPath implements Path { + + private final NestedFileSystem fileSystem; + + private final String nestedEntryName; + + private volatile Boolean entryExists; + + NestedPath(NestedFileSystem fileSystem, String nestedEntryName) { + if (fileSystem == null || nestedEntryName == null || nestedEntryName.isBlank()) { + throw new IllegalArgumentException("'filesSystem' and 'nestedEntryName' are required"); + } + this.fileSystem = fileSystem; + this.nestedEntryName = nestedEntryName; + } + + Path getJarPath() { + return this.fileSystem.getJarPath(); + } + + String getNestedEntryName() { + return this.nestedEntryName; + } + + @Override + public NestedFileSystem getFileSystem() { + return this.fileSystem; + } + + @Override + public boolean isAbsolute() { + return true; + } + + @Override + public Path getRoot() { + return null; + } + + @Override + public Path getFileName() { + return this; + } + + @Override + public Path getParent() { + return null; + } + + @Override + public int getNameCount() { + return 1; + } + + @Override + public Path getName(int index) { + if (index != 0) { + throw new IllegalArgumentException("Nested paths only have a single element"); + } + return this; + } + + @Override + public Path subpath(int beginIndex, int endIndex) { + if (beginIndex != 0 || endIndex != 1) { + throw new IllegalArgumentException("Nested paths only have a single element"); + } + return this; + } + + @Override + public boolean startsWith(Path other) { + return equals(other); + } + + @Override + public boolean endsWith(Path other) { + return equals(other); + } + + @Override + public Path normalize() { + return this; + } + + @Override + public Path resolve(Path other) { + throw new UnsupportedOperationException("Unable to resolve nested path"); + } + + @Override + public Path relativize(Path other) { + throw new UnsupportedOperationException("Unable to relativize nested path"); + } + + @Override + public URI toUri() { + try { + String jarFilePath = this.fileSystem.getJarPath().toUri().getPath(); + return new URI("nested:" + jarFilePath + "/!" + this.nestedEntryName); + } + catch (URISyntaxException ex) { + throw new IOError(ex); + } + } + + @Override + public Path toAbsolutePath() { + return this; + } + + @Override + public Path toRealPath(LinkOption... options) throws IOException { + return this; + } + + @Override + public WatchKey register(WatchService watcher, Kind[] events, Modifier... modifiers) throws IOException { + throw new UnsupportedOperationException("Nested paths cannot be watched"); + } + + @Override + public int compareTo(Path other) { + NestedPath otherNestedPath = cast(other); + return this.nestedEntryName.compareTo(otherNestedPath.nestedEntryName); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + NestedPath other = (NestedPath) obj; + return Objects.equals(this.fileSystem, other.fileSystem) + && Objects.equals(this.nestedEntryName, other.nestedEntryName); + } + + @Override + public int hashCode() { + return Objects.hash(this.fileSystem, this.nestedEntryName); + } + + @Override + public String toString() { + return this.fileSystem.getJarPath() + this.fileSystem.getSeparator() + this.nestedEntryName; + } + + void assertExists() throws NoSuchFileException { + if (!Files.isRegularFile(getJarPath())) { + throw new NoSuchFileException(toString()); + } + Boolean entryExists = this.entryExists; + if (entryExists == null) { + try { + try (ZipContent content = ZipContent.open(getJarPath(), this.nestedEntryName)) { + entryExists = true; + } + } + catch (IOException ex) { + entryExists = false; + } + this.entryExists = entryExists; + } + if (!entryExists) { + throw new NoSuchFileException(toString()); + } + } + + static NestedPath cast(Path path) { + if (path instanceof NestedPath nestedPath) { + return nestedPath; + } + throw new ProviderMismatchException(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/package-info.java new file mode 100644 index 000000000000..6431f845345c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Non-blocking IO {@link java.nio.file.FileSystem} implementation for nested suppoprt. + */ +package org.springframework.boot.loader.nio.file; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider new file mode 100644 index 000000000000..425737d36fdd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider @@ -0,0 +1 @@ +org.springframework.boot.loader.nio.file.NestedFileSystemProvider diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java index f992624c4f5d..5f4314de44dc 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.loader.net.protocol.nested; import java.io.File; +import java.net.URI; import java.net.URL; import java.nio.file.Path; @@ -96,4 +97,32 @@ void fromUrlReturnsNestedLocation() throws Exception { assertThat(location.nestedEntryName()).isEqualTo("lib/nested.jar"); } + @Test + void fromUriWhenUrlIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> NestedLocation.fromUri(null)) + .withMessageContaining("'uri' must not be null"); + } + + @Test + void fromUriWhenNotNestedProtocolThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> NestedLocation.fromUri(new URI("file://test.jar"))) + .withMessageContaining("must use 'nested' scheme"); + } + + @Test + void fromUriWhenNoSeparatorThrowsExceptiuon() { + assertThatIllegalArgumentException() + .isThrownBy(() -> NestedLocation.fromUri(new URI("nested:test.jar!nested.jar"))) + .withMessageContaining("'path' must contain '/!'"); + } + + @Test + void fromUriReturnsNestedLocation() throws Exception { + File file = new File(this.temp, "test.jar"); + NestedLocation location = NestedLocation + .fromUri(new URI("nested:" + file.getAbsolutePath() + "/!lib/nested.jar")); + assertThat(location.path()).isEqualTo(file.toPath()); + assertThat(location.nestedEntryName()).isEqualTo("lib/nested.jar"); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedByteChannelTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedByteChannelTests.java new file mode 100644 index 000000000000..e198d43d6f80 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedByteChannelTests.java @@ -0,0 +1,178 @@ +/* + * Copyright 2012-2023 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.loader.nio.file; + +import java.io.File; +import java.io.IOException; +import java.lang.ref.Cleaner.Cleanable; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.NonWritableChannelException; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.ArgumentCaptor; + +import org.springframework.boot.loader.ref.Cleaner; +import org.springframework.boot.loader.testsupport.TestJar; +import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed; +import org.springframework.boot.loader.zip.ZipContent; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link NestedByteChannel}. + * + * @author Phillip Webb + */ +@AssertFileChannelDataBlocksClosed +class NestedByteChannelTests { + + @TempDir + File temp; + + private File file; + + private NestedByteChannel channel; + + @BeforeEach + void setup() throws Exception { + this.file = new File(this.temp, "test.jar"); + TestJar.create(this.file); + this.channel = new NestedByteChannel(this.file.toPath(), "nested.jar"); + } + + @AfterEach + void cleanuo() throws Exception { + this.channel.close(); + } + + @Test + void isOpenWhenOpenReturnsTrue() { + assertThat(this.channel.isOpen()).isTrue(); + } + + @Test + void isOpenWhenClosedReturnsFalse() throws Exception { + this.channel.close(); + assertThat(this.channel.isOpen()).isFalse(); + } + + @Test + void closeCleansResources() throws Exception { + Cleaner cleaner = mock(Cleaner.class); + Cleanable cleanable = mock(Cleanable.class); + given(cleaner.register(any(), any())).willReturn(cleanable); + NestedByteChannel channel = new NestedByteChannel(this.file.toPath(), "nested.jar", cleaner); + channel.close(); + then(cleanable).should().clean(); + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(Runnable.class); + then(cleaner).should().register(any(), actionCaptor.capture()); + actionCaptor.getValue().run(); + } + + @Test + void closeWhenAlreadyClosedDoesNothing() throws IOException { + Cleaner cleaner = mock(Cleaner.class); + Cleanable cleanable = mock(Cleanable.class); + given(cleaner.register(any(), any())).willReturn(cleanable); + NestedByteChannel channel = new NestedByteChannel(this.file.toPath(), "nested.jar", cleaner); + channel.close(); + then(cleanable).should().clean(); + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(Runnable.class); + then(cleaner).should().register(any(), actionCaptor.capture()); + actionCaptor.getValue().run(); + channel.close(); + then(cleaner).shouldHaveNoMoreInteractions(); + } + + @Test + void readReadsBytesAndIncrementsPosition() throws IOException { + ByteBuffer dst = ByteBuffer.allocate(10); + assertThat(this.channel.position()).isZero(); + this.channel.read(dst); + assertThat(this.channel.position()).isEqualTo(10L); + assertThat(dst.array()).isNotEqualTo(ByteBuffer.allocate(10).array()); + } + + @Test + void writeThrowsException() { + assertThatExceptionOfType(NonWritableChannelException.class) + .isThrownBy(() -> this.channel.write(ByteBuffer.allocate(10))); + } + + @Test + void positionWhenClosedThrowsException() throws Exception { + this.channel.close(); + assertThatExceptionOfType(ClosedChannelException.class).isThrownBy(() -> this.channel.position()); + } + + @Test + void positionWhenOpenReturnsPosition() throws Exception { + assertThat(this.channel.position()).isEqualTo(0L); + } + + @Test + void positionWithLongWhenClosedThrowsException() throws Exception { + this.channel.close(); + assertThatExceptionOfType(ClosedChannelException.class).isThrownBy(() -> this.channel.position(0L)); + } + + @Test + void positionWithLongWhenLessThanZeroThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.channel.position(-1)); + } + + @Test + void positionWithLongWhenEqualToSizeThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.channel.position(this.channel.size())); + } + + @Test + void positionWithLongWhenOpenUpdatesPosition() throws Exception { + ByteBuffer dst1 = ByteBuffer.allocate(10); + ByteBuffer dst2 = ByteBuffer.allocate(10); + dst2.position(1); + this.channel.read(dst1); + this.channel.position(1); + this.channel.read(dst2); + dst2.array()[0] = dst1.array()[0]; + assertThat(dst1.array()).isEqualTo(dst2.array()); + } + + @Test + void sizeWhenClosedThrowsException() throws Exception { + this.channel.close(); + assertThatExceptionOfType(ClosedChannelException.class).isThrownBy(() -> this.channel.size()); + } + + @Test + void sizeWhenOpenReturnsSize() throws IOException { + try (ZipContent content = ZipContent.open(this.file.toPath())) { + assertThat(this.channel.size()).isEqualTo(content.getEntry("nested.jar").getUncompressedSize()); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileStoreTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileStoreTests.java new file mode 100644 index 000000000000..9baf2b4f9b10 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileStoreTests.java @@ -0,0 +1,150 @@ +/* + * Copyright 2012-2023 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.loader.nio.file; + +import java.io.File; +import java.nio.file.FileStore; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributeView; +import java.nio.file.attribute.FileStoreAttributeView; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link NestedFileStore}. + * + * @author Phillip Webb + */ +class NestedFileStoreTests { + + @TempDir + File temp; + + private NestedFileSystemProvider provider; + + private Path jarPath; + + private NestedFileSystem fileSystem; + + private TestNestedFileStore fileStore; + + @BeforeEach + void setup() { + this.provider = new NestedFileSystemProvider(); + this.jarPath = new File(this.temp, "test.jar").toPath(); + this.fileSystem = new NestedFileSystem(this.provider, this.jarPath); + this.fileStore = new TestNestedFileStore(this.fileSystem); + } + + @Test + void nameReturnsName() { + assertThat(this.fileStore.name()).isEqualTo(this.jarPath.toAbsolutePath().toString()); + } + + @Test + void typeReturnsNestedFs() { + assertThat(this.fileStore.type()).isEqualTo("nestedfs"); + } + + @Test + void isReadOnlyReturnsTrue() { + assertThat(this.fileStore.isReadOnly()).isTrue(); + } + + @Test + void getTotalSpaceReturnsZero() throws Exception { + assertThat(this.fileStore.getTotalSpace()).isZero(); + } + + @Test + void getUsableSpaceReturnsZero() throws Exception { + assertThat(this.fileStore.getUsableSpace()).isZero(); + } + + @Test + void getUnallocatedSpaceReturnsZero() throws Exception { + assertThat(this.fileStore.getUnallocatedSpace()).isZero(); + } + + @Test + void supportsFileAttributeViewWithClassDelegatesToJarPathFileStore() { + FileStore jarFileStore = mock(FileStore.class); + given(jarFileStore.supportsFileAttributeView(BasicFileAttributeView.class)).willReturn(true); + this.fileStore.setJarPathFileStore(jarFileStore); + assertThat(this.fileStore.supportsFileAttributeView(BasicFileAttributeView.class)).isTrue(); + then(jarFileStore).should().supportsFileAttributeView(BasicFileAttributeView.class); + } + + @Test + void supportsFileAttributeViewWithStringDelegatesToJarPathFileStore() { + FileStore jarFileStore = mock(FileStore.class); + given(jarFileStore.supportsFileAttributeView("basic")).willReturn(true); + this.fileStore.setJarPathFileStore(jarFileStore); + assertThat(this.fileStore.supportsFileAttributeView("basic")).isTrue(); + then(jarFileStore).should().supportsFileAttributeView("basic"); + } + + @Test + void getFileStoreAttributeViewDelegatesToJarPathFileStore() { + FileStore jarFileStore = mock(FileStore.class); + TestFileStoreAttributeView attributeView = mock(TestFileStoreAttributeView.class); + given(jarFileStore.getFileStoreAttributeView(TestFileStoreAttributeView.class)).willReturn(attributeView); + this.fileStore.setJarPathFileStore(jarFileStore); + assertThat(this.fileStore.getFileStoreAttributeView(TestFileStoreAttributeView.class)).isEqualTo(attributeView); + then(jarFileStore).should().getFileStoreAttributeView(TestFileStoreAttributeView.class); + } + + @Test + void getAttributeDelegatesToJarPathFileStore() throws Exception { + FileStore jarFileStore = mock(FileStore.class); + given(jarFileStore.getAttribute("test")).willReturn("spring"); + this.fileStore.setJarPathFileStore(jarFileStore); + assertThat(this.fileStore.getAttribute("test")).isEqualTo("spring"); + then(jarFileStore).should().getAttribute("test"); + } + + static class TestNestedFileStore extends NestedFileStore { + + TestNestedFileStore(NestedFileSystem fileSystem) { + super(fileSystem); + } + + private FileStore jarPathFileStore; + + void setJarPathFileStore(FileStore jarPathFileStore) { + this.jarPathFileStore = jarPathFileStore; + } + + @Override + protected FileStore getJarPathFileStore() { + return (this.jarPathFileStore != null) ? this.jarPathFileStore : super.getJarPathFileStore(); + } + + } + + abstract static class TestFileStoreAttributeView implements FileStoreAttributeView { + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemProviderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemProviderTests.java new file mode 100644 index 000000000000..dd357cb28d77 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemProviderTests.java @@ -0,0 +1,273 @@ +/* + * Copyright 2012-2023 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.loader.nio.file; + +import java.io.File; +import java.net.URI; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemAlreadyExistsException; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.NoSuchFileException; +import java.nio.file.NotDirectoryException; +import java.nio.file.Path; +import java.nio.file.ReadOnlyFileSystemException; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributeView; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.spi.FileSystemProvider; +import java.util.Collections; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.testsupport.TestJar; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link NestedFileSystemProvider}. + * + * @author Phillip Webb + */ +class NestedFileSystemProviderTests { + + @TempDir + File temp; + + private File file; + + private TestNestedFileSystemProvider provider = new TestNestedFileSystemProvider(); + + private String uriPrefix; + + @BeforeEach + void setup() throws Exception { + this.file = new File(this.temp, "test.jar"); + TestJar.create(this.file); + this.uriPrefix = "nested:" + this.file.toURI().getPath() + "/!"; + } + + @Test + void getSchemeReturnsScheme() { + assertThat(this.provider.getScheme()).isEqualTo("nested"); + } + + @Test + void newFilesSystemWhenBadUrlThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.provider.newFileSystem(new URI("bad:notreal"), Collections.emptyMap())) + .withMessageContaining("must use 'nested' scheme"); + } + + @Test + void newFileSystemWhenAlreadyExistsThrowsException() throws Exception { + this.provider.newFileSystem(new URI(this.uriPrefix + "nested.jar"), null); + assertThatExceptionOfType(FileSystemAlreadyExistsException.class) + .isThrownBy(() -> this.provider.newFileSystem(new URI(this.uriPrefix + "other.jar"), null)); + } + + @Test + void newFileSystemReturnsFileSystem() throws Exception { + FileSystem fileSystem = this.provider.newFileSystem(new URI(this.uriPrefix + "nested.jar"), null); + assertThat(fileSystem).isInstanceOf(NestedFileSystem.class); + } + + @Test + void getFileSystemWhenBadUrlThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.provider.getFileSystem(new URI("bad:notreal"))) + .withMessageContaining("must use 'nested' scheme"); + } + + @Test + void getFileSystemWhenNotCreatedThrowsException() { + assertThatExceptionOfType(FileSystemNotFoundException.class) + .isThrownBy(() -> this.provider.getFileSystem(new URI(this.uriPrefix + "nested.jar"))); + } + + @Test + void getFileSystemReturnsFileSystem() throws Exception { + FileSystem expected = this.provider.newFileSystem(new URI(this.uriPrefix + "nested.jar"), null); + assertThat(this.provider.getFileSystem(new URI(this.uriPrefix + "nested.jar"))).isSameAs(expected); + } + + @Test + void getPathWhenFileSystemExistsReturnsPath() throws Exception { + URI uri = new URI(this.uriPrefix + "nested.jar"); + this.provider.newFileSystem(uri, null); + assertThat(this.provider.getPath(uri)).isInstanceOf(NestedPath.class); + } + + @Test + void getPathWhenFileSystemDoesNtExistReturnsPath() throws Exception { + URI uri = new URI(this.uriPrefix + "nested.jar"); + assertThat(this.provider.getPath(uri)).isInstanceOf(NestedPath.class); + } + + @Test + void newByteChannelReturnsByteChannel() throws Exception { + URI uri = new URI(this.uriPrefix + "nested.jar"); + Path path = this.provider.getPath(uri); + SeekableByteChannel byteChannel = this.provider.newByteChannel(path, Set.of(StandardOpenOption.READ)); + assertThat(byteChannel).isInstanceOf(NestedByteChannel.class); + } + + @Test + void newDirectoryStreamThrowsException() throws Exception { + URI uri = new URI(this.uriPrefix + "nested.jar"); + Path path = this.provider.getPath(uri); + assertThatExceptionOfType(NotDirectoryException.class) + .isThrownBy(() -> this.provider.newDirectoryStream(path, null)); + } + + @Test + void createDirectoryThrowsException() throws Exception { + URI uri = new URI(this.uriPrefix + "nested.jar"); + Path path = this.provider.getPath(uri); + assertThatExceptionOfType(ReadOnlyFileSystemException.class) + .isThrownBy(() -> this.provider.createDirectory(path)); + } + + @Test + void deleteThrowsException() throws Exception { + URI uri = new URI(this.uriPrefix + "nested.jar"); + Path path = this.provider.getPath(uri); + assertThatExceptionOfType(ReadOnlyFileSystemException.class).isThrownBy(() -> this.provider.delete(path)); + } + + @Test + void copyThrowsException() throws Exception { + URI uri = new URI(this.uriPrefix + "nested.jar"); + Path path = this.provider.getPath(uri); + assertThatExceptionOfType(ReadOnlyFileSystemException.class).isThrownBy(() -> this.provider.copy(path, path)); + } + + @Test + void moveThrowsException() throws Exception { + URI uri = new URI(this.uriPrefix + "nested.jar"); + Path path = this.provider.getPath(uri); + assertThatExceptionOfType(ReadOnlyFileSystemException.class).isThrownBy(() -> this.provider.move(path, path)); + } + + @Test + void isSameFileWhenSameReturnsTrue() throws Exception { + Path p1 = this.provider.getPath(new URI(this.uriPrefix + "nested.jar")); + Path p2 = this.provider.getPath(new URI(this.uriPrefix + "nested.jar")); + assertThat(this.provider.isSameFile(p1, p1)).isTrue(); + assertThat(this.provider.isSameFile(p1, p2)).isTrue(); + } + + @Test + void isSameFileWhenDifferentReturnsFalse() throws Exception { + Path p1 = this.provider.getPath(new URI(this.uriPrefix + "nested.jar")); + Path p2 = this.provider.getPath(new URI(this.uriPrefix + "other.jar")); + assertThat(this.provider.isSameFile(p1, p2)).isFalse(); + } + + @Test + void isHiddenReturnsFalse() throws Exception { + Path path = this.provider.getPath(new URI(this.uriPrefix + "nested.jar")); + assertThat(this.provider.isHidden(path)).isFalse(); + } + + @Test + void getFileStoreWhenFileDoesNotExistThrowsException() throws Exception { + Path path = this.provider.getPath(new URI(this.uriPrefix + "missing.jar")); + assertThatExceptionOfType(NoSuchFileException.class).isThrownBy(() -> this.provider.getFileStore(path)); + } + + @Test + void getFileStoreReturnsFileStore() throws Exception { + Path path = this.provider.getPath(new URI(this.uriPrefix + "nested.jar")); + assertThat(this.provider.getFileStore(path)).isInstanceOf(NestedFileStore.class); + } + + @Test + void checkAccessDelegatesToJarPath() throws Exception { + Path path = this.provider.getPath(new URI(this.uriPrefix + "nested.jar")); + Path jarPath = mockJarPath(); + this.provider.setMockJarPath(jarPath); + this.provider.checkAccess(path); + then(jarPath.getFileSystem().provider()).should().checkAccess(jarPath); + } + + @Test + void getFileAttributeViewDelegatesToJarPath() throws Exception { + Path path = this.provider.getPath(new URI(this.uriPrefix + "nested.jar")); + Path jarPath = mockJarPath(); + this.provider.setMockJarPath(jarPath); + this.provider.getFileAttributeView(path, BasicFileAttributeView.class); + then(jarPath.getFileSystem().provider()).should().getFileAttributeView(jarPath, BasicFileAttributeView.class); + } + + @Test + void readAttributesWithTypeDelegatesToJarPath() throws Exception { + Path path = this.provider.getPath(new URI(this.uriPrefix + "nested.jar")); + Path jarPath = mockJarPath(); + this.provider.setMockJarPath(jarPath); + this.provider.readAttributes(path, BasicFileAttributes.class); + then(jarPath.getFileSystem().provider()).should().readAttributes(jarPath, BasicFileAttributes.class); + } + + @Test + void readAttributesWithNameDelegatesToJarPath() throws Exception { + Path path = this.provider.getPath(new URI(this.uriPrefix + "nested.jar")); + Path jarPath = mockJarPath(); + this.provider.setMockJarPath(jarPath); + this.provider.readAttributes(path, "basic"); + then(jarPath.getFileSystem().provider()).should().readAttributes(jarPath, "basic"); + } + + @Test + void setAttributeThrowsException() throws Exception { + Path path = this.provider.getPath(new URI(this.uriPrefix + "nested.jar")); + assertThatExceptionOfType(ReadOnlyFileSystemException.class) + .isThrownBy(() -> this.provider.setAttribute(path, "test", "test")); + } + + private Path mockJarPath() { + Path path = mock(Path.class); + FileSystem fileSystem = mock(FileSystem.class); + given(path.getFileSystem()).willReturn(fileSystem); + FileSystemProvider provider = mock(FileSystemProvider.class); + given(fileSystem.provider()).willReturn(provider); + return path; + } + + static class TestNestedFileSystemProvider extends NestedFileSystemProvider { + + private Path mockJarPath; + + @Override + protected Path getJarPath(Path path) { + return (this.mockJarPath != null) ? this.mockJarPath : super.getJarPath(path); + } + + void setMockJarPath(Path mockJarPath) { + this.mockJarPath = mockJarPath; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemTests.java new file mode 100644 index 000000000000..d8b8825dc8b4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemTests.java @@ -0,0 +1,191 @@ +/* + * Copyright 2012-2023 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.loader.nio.file; + +import java.io.File; +import java.nio.file.ClosedFileSystemException; +import java.nio.file.Path; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link NestedFileSystem}. + * + * @author Phillip Webb + */ +class NestedFileSystemTests { + + @TempDir + File temp; + + private NestedFileSystemProvider provider; + + private Path jarPath; + + private NestedFileSystem fileSystem; + + @BeforeEach + void setup() { + this.provider = new NestedFileSystemProvider(); + this.jarPath = new File(this.temp, "test.jar").toPath(); + this.fileSystem = new NestedFileSystem(this.provider, this.jarPath); + } + + @Test + void providerReturnsProvider() { + assertThat(this.fileSystem.provider()).isSameAs(this.provider); + } + + @Test + void getJarPathReturnsJarPath() { + assertThat(this.fileSystem.getJarPath()).isSameAs(this.jarPath); + } + + @Test + void closeClosesFileSystem() throws Exception { + this.fileSystem.close(); + assertThat(this.fileSystem.isOpen()).isFalse(); + } + + @Test + void closeWhenAlreadyClosedDoesNothing() throws Exception { + this.fileSystem.close(); + this.fileSystem.close(); + assertThat(this.fileSystem.isOpen()).isFalse(); + } + + @Test + void isOpenWhenOpenReturnsTrue() { + assertThat(this.fileSystem.isOpen()).isTrue(); + } + + @Test + void isOpenWhenClosedReturnsFalse() throws Exception { + this.fileSystem.close(); + assertThat(this.fileSystem.isOpen()).isFalse(); + } + + @Test + void isReadOnlyReturnsTrue() { + assertThat(this.fileSystem.isReadOnly()).isTrue(); + } + + @Test + void getSeparatorReturnsSeparator() { + assertThat(this.fileSystem.getSeparator()).isEqualTo("/!"); + } + + @Test + void getRootDirectoryWhenOpenReturnsEmptyIterable() { + assertThat(this.fileSystem.getRootDirectories()).isEmpty(); + } + + @Test + void getRootDirectoryWhenClosedThrowsException() throws Exception { + this.fileSystem.close(); + assertThatExceptionOfType(ClosedFileSystemException.class) + .isThrownBy(() -> this.fileSystem.getRootDirectories()); + } + + @Test + void supportedFileAttributeViewsWhenOpenReturnsBasic() { + assertThat(this.fileSystem.supportedFileAttributeViews()).containsExactly("basic"); + } + + @Test + void supportedFileAttributeViewsWhenClosedThrowsException() throws Exception { + this.fileSystem.close(); + assertThatExceptionOfType(ClosedFileSystemException.class) + .isThrownBy(() -> this.fileSystem.supportedFileAttributeViews()); + } + + @Test + void getPathWhenClosedThrowsException() throws Exception { + this.fileSystem.close(); + assertThatExceptionOfType(ClosedFileSystemException.class) + .isThrownBy(() -> this.fileSystem.getPath("nested.jar")); + } + + @Test + void getPathWhenFirstIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.fileSystem.getPath(null)) + .withMessage("Nested paths must contain a single element"); + } + + @Test + void getPathWhenFirstIsBlankThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.fileSystem.getPath("")) + .withMessage("Nested paths must contain a single element"); + } + + @Test + void getPathWhenMoreIsNotEmptyThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.fileSystem.getPath("nested.jar", "another.jar")) + .withMessage("Nested paths must contain a single element"); + } + + @Test + void getPathReturnsPath() { + assertThat(this.fileSystem.getPath("nested.jar")).isInstanceOf(NestedPath.class); + } + + @Test + void getPathMatchThrowsException() { + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> this.fileSystem.getPathMatcher("*")) + .withMessage("Nested paths do not support path matchers"); + } + + @Test + void getUserPrincipalLookupServiceThrowsException() { + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> this.fileSystem.getUserPrincipalLookupService()) + .withMessage("Nested paths do not have a user principal lookup service"); + } + + @Test + void newWatchServiceThrowsException() { + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> this.fileSystem.newWatchService()) + .withMessage("Nested paths do not support the WacherService"); + } + + @Test + void toStringReturnsString() { + assertThat(this.fileSystem).hasToString(this.jarPath.toAbsolutePath().toString()); + } + + @Test + void equalsAndHashCode() { + Path jp1 = new File(this.temp, "test1.jar").toPath(); + Path jp2 = new File(this.temp, "test1.jar").toPath(); + Path jp3 = new File(this.temp, "test2.jar").toPath(); + NestedFileSystem f1 = new NestedFileSystem(this.provider, jp1); + NestedFileSystem f2 = new NestedFileSystem(this.provider, jp1); + NestedFileSystem f3 = new NestedFileSystem(this.provider, jp2); + NestedFileSystem f4 = new NestedFileSystem(this.provider, jp3); + assertThat(f1.hashCode()).isEqualTo(f2.hashCode()); + assertThat(f1).isEqualTo(f1).isEqualTo(f2).isEqualTo(f3).isNotEqualTo(f4); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemZipFileSystemIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemZipFileSystemIntegrationTests.java new file mode 100644 index 000000000000..28e26b7f8d48 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemZipFileSystemIntegrationTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2023 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.loader.nio.file; + +import java.io.File; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.net.protocol.jar.JarUrl; +import org.springframework.boot.loader.testsupport.TestJar; +import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link NestedFileSystem} in combination with + * {@code ZipFileSystem}. + * + * @author Phillip Webb + */ +@AssertFileChannelDataBlocksClosed +class NestedFileSystemZipFileSystemIntegrationTests { + + @TempDir + File temp; + + @Test + void zip() throws Exception { + File file = new File(this.temp, "test.jar"); + TestJar.create(file); + URI uri = JarUrl.create(file).toURI(); + try (FileSystem fs = FileSystems.newFileSystem(uri, Collections.emptyMap())) { + assertThat(Files.readAllBytes(fs.getPath("1.dat"))).containsExactly(0x1); + } + } + + @Test + void nestedZip() throws Exception { + File file = new File(this.temp, "test.jar"); + TestJar.create(file); + URI uri = JarUrl.create(file, "nested.jar").toURI(); + try (FileSystem fs = FileSystems.newFileSystem(uri, Collections.emptyMap())) { + assertThat(Files.readAllBytes(fs.getPath("3.dat"))).containsExactly(0x3); + } + } + + @Test + void nestedZipWithoutNewFileSystem() throws Exception { + File file = new File(this.temp, "test.jar"); + TestJar.create(file); + URI uri = JarUrl.create(file, "nested.jar", "3.dat").toURI(); + Path path = Path.of(uri); + assertThat(Files.readAllBytes(path)).containsExactly(0x3); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedPathTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedPathTests.java new file mode 100644 index 000000000000..02558cf30521 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedPathTests.java @@ -0,0 +1,235 @@ +/* + * Copyright 2012-2023 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.loader.nio.file; + +import java.io.File; +import java.net.URI; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.ProviderMismatchException; +import java.nio.file.WatchService; +import java.util.Set; +import java.util.TreeSet; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.boot.loader.testsupport.TestJar; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link NestedPath}. + * + * @author Phillip Webb + */ +class NestedPathTests { + + @TempDir + File temp; + + private NestedFileSystem fileSystem; + + private NestedFileSystemProvider provider; + + private Path jarPath; + + private NestedPath path; + + @BeforeEach + void setup() { + this.jarPath = new File(this.temp, "test.jar").toPath(); + this.provider = new NestedFileSystemProvider(); + this.fileSystem = new NestedFileSystem(this.provider, this.jarPath); + this.path = new NestedPath(this.fileSystem, "nested.jar"); + } + + @Test + void getJarPathReturnsJarPath() { + assertThat(this.path.getJarPath()).isEqualTo(this.jarPath); + } + + @Test + void getNestedEntryNameReturnsNestedEntryName() { + assertThat(this.path.getNestedEntryName()).isEqualTo("nested.jar"); + } + + @Test + void getFileSystemReturnsFileSystem() { + assertThat(this.path.getFileSystem()).isSameAs(this.fileSystem); + } + + @Test + void isAbsoluteRerturnsTrue() { + assertThat(this.path.isAbsolute()).isTrue(); + } + + @Test + void getRootReturnsNull() { + assertThat(this.path.getRoot()).isNull(); + } + + @Test + void getFileNameReturnsPath() { + assertThat(this.path.getFileName()).isSameAs(this.path); + } + + @Test + void getParentReturnsNull() { + assertThat(this.path.getParent()).isNull(); + } + + @Test + void getNameCountReturnsOne() { + assertThat(this.path.getNameCount()).isEqualTo(1); + } + + @Test + void subPathWhenBeginZeroEndOneReturnsPath() { + assertThat(this.path.subpath(0, 1)).isSameAs(this.path); + } + + @Test + void subPathWhenBeginIndexNotZeroThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.path.subpath(1, 1)) + .withMessage("Nested paths only have a single element"); + } + + @Test + void subPathThenEndIndexNotOneThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.path.subpath(0, 2)) + .withMessage("Nested paths only have a single element"); + } + + @Test + void startsWithWhenStartsWithReturnsTrue() { + NestedPath otherPath = new NestedPath(this.fileSystem, "nested.jar"); + assertThat(this.path.startsWith(otherPath)).isTrue(); + } + + @Test + void startsWithWhenNotStartsWithReturnsFalse() { + NestedPath otherPath = new NestedPath(this.fileSystem, "other.jar"); + assertThat(this.path.startsWith(otherPath)).isFalse(); + } + + @Test + void endsWithWhenEndsWithReturnsTrue() { + NestedPath otherPath = new NestedPath(this.fileSystem, "nested.jar"); + assertThat(this.path.endsWith(otherPath)).isTrue(); + } + + @Test + void endsWithWhenNotEndsWithReturnsFalse() { + NestedPath otherPath = new NestedPath(this.fileSystem, "other.jar"); + assertThat(this.path.endsWith(otherPath)).isFalse(); + } + + @Test + void normalizeReturnsPath() { + assertThat(this.path.normalize()).isSameAs(this.path); + } + + @Test + void resolveThrowsException() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> this.path.resolve(this.path)) + .withMessage("Unable to resolve nested path"); + } + + @Test + void relativizeThrowsException() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> this.path.relativize(this.path)) + .withMessage("Unable to relativize nested path"); + } + + @Test + void toUriReturnsUri() throws Exception { + assertThat(this.path.toUri()).isEqualTo(new URI("nested:" + this.jarPath.toUri().getPath() + "/!nested.jar")); + } + + @Test + void toAbsolutePathReturnsPath() { + assertThat(this.path.toAbsolutePath()).isSameAs(this.path); + } + + @Test + void toRealPathReturnsPath() throws Exception { + assertThat(this.path.toRealPath()).isSameAs(this.path); + } + + @Test + void registerThrowsException() { + WatchService watcher = mock(WatchService.class); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> this.path.register(watcher)) + .withMessage("Nested paths cannot be watched"); + } + + @Test + void compareToComparesOnNestedEntryName() { + NestedPath a = new NestedPath(this.fileSystem, "a.jar"); + NestedPath b = new NestedPath(this.fileSystem, "b.jar"); + NestedPath c = new NestedPath(this.fileSystem, "c.jar"); + assertThat(new TreeSet<>(Set.of(c, a, b))).containsExactly(a, b, c); + } + + @Test + void hashCodeAndEquals() { + NestedFileSystem fs2 = new NestedFileSystem(this.provider, new File(this.temp, "test2.jar").toPath()); + NestedPath p1 = new NestedPath(this.fileSystem, "a.jar"); + NestedPath p2 = new NestedPath(this.fileSystem, "a.jar"); + NestedPath p3 = new NestedPath(this.fileSystem, "c.jar"); + NestedPath p4 = new NestedPath(fs2, "c.jar"); + assertThat(p1.hashCode()).isEqualTo(p2.hashCode()); + assertThat(p1).isEqualTo(p1).isEqualTo(p2).isNotEqualTo(p3).isNotEqualTo(p4); + } + + @Test + void toStringReturnsString() { + assertThat(this.path).hasToString(this.jarPath.toString() + "/!nested.jar"); + } + + @Test + void assertExistsWhenExists() throws Exception { + TestJar.create(this.jarPath.toFile()); + this.path.assertExists(); + } + + @Test + void assertExistsWhenDoesNotExistThrowsException() { + assertThatExceptionOfType(NoSuchFileException.class).isThrownBy(this.path::assertExists); + } + + @Test + void castWhenNestedPathReturnsNestedPath() { + assertThat(NestedPath.cast(this.path)).isSameAs(this.path); + } + + @Test + void castWhenNullThrowsException() { + assertThatExceptionOfType(ProviderMismatchException.class).isThrownBy(() -> NestedPath.cast(null)); + } + + @Test + void castWhenNotNestedPathThrowsException() { + assertThatExceptionOfType(ProviderMismatchException.class).isThrownBy(() -> NestedPath.cast(this.jarPath)); + } + +} diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/build.gradle index 37596c620634..8f8cf37e3aae 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/build.gradle +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/build.gradle @@ -1,6 +1,8 @@ plugins { id "java" id "org.springframework.boot" +// id 'org.springframework.boot' version '3.1.4' +// id 'io.spring.dependency-management' version '1.1.3' } apply plugin: "io.spring.dependency-management" diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java index 0c9d429350d8..245b471b7900 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java @@ -17,8 +17,13 @@ package org.springframework.boot.loaderapp; import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; import java.net.JarURLConnection; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; import jakarta.servlet.ServletContext; @@ -27,6 +32,8 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; import org.springframework.util.FileCopyUtils; @SpringBootApplication @@ -49,9 +56,20 @@ public CommandLineRunner commandLineRunner(ServletContext servletContext) { String message = (!Arrays.equals(resourceContent, directContent)) ? "NO MATCH" : directContent.length + " BYTES"; System.out.println(">>>>> " + message + " from " + resourceUrl); + testGh7161(); }; } + private void testGh7161() { + try { + Resource resource = new ClassPathResource("gh-7161"); + Path path = Paths.get(resource.getURI()); + System.out.println(">>>>> gh-7161 " + Files.list(path).toList()); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + public static void main(String[] args) { SpringApplication.run(LoaderTestApplication.class, args).close(); } diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/resources/gh-7161/example.txt b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/resources/gh-7161/example.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java index 3151e334d9e2..84b46b8f9a53 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java @@ -51,11 +51,12 @@ class LoaderIntegrationTests { @ParameterizedTest @MethodSource("javaRuntimes") - void readUrlsWithoutWarning(JavaRuntime javaRuntime) { + void runJar(JavaRuntime javaRuntime) { try (GenericContainer container = createContainer(javaRuntime, "spring-boot-loader-tests-app", null)) { container.start(); System.out.println(this.output.toUtf8String()); assertThat(this.output.toUtf8String()).contains(">>>>> 287649 BYTES from") + .contains(">>>>> gh-7161 [/gh-7161/example.txt]") .doesNotContain("WARNING:") .doesNotContain("illegal") .doesNotContain("jar written to temp"); From 93b562e632fc26eefd39cf66a3186b37f8fdd7cf Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 18 Oct 2023 20:52:12 -0700 Subject: [PATCH 0657/1215] Fix PulsarProperties lookupTimeout Prefer `null` to `-1` for the default timeout. See gh-34763 --- .../boot/autoconfigure/pulsar/PulsarProperties.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java index 3ed73df57f62..7597da0c08d1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java @@ -116,7 +116,7 @@ public static class Client { /** * Client lookup timeout. */ - private Duration lookupTimeout = Duration.ofMillis(-1); // FIXME + private Duration lookupTimeout; /** * Duration to wait for a connection to a broker to be established. From 3ccf29ef49268cf2db83a60b42675e4a037374dc Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 18 Oct 2023 20:53:06 -0700 Subject: [PATCH 0658/1215] Refine spring-boot-starter-data-redis-reactive dependencies Change `spring-boot-starter-data-redis-reactive` to be standalone and also declare an explicit dependency on reactor. Closes gh-37943 --- .../spring-boot-starter-data-redis-reactive/build.gradle | 5 ++++- .../spring-boot-starter-data-redis/build.gradle | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis-reactive/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis-reactive/build.gradle index d66f98dcfc40..f0c2d2bb4b22 100644 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis-reactive/build.gradle +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis-reactive/build.gradle @@ -5,5 +5,8 @@ plugins { description = "Starter for using Redis key-value data store with Spring Data Redis reactive and the Lettuce client" dependencies { - api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-data-redis")) + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) + api("io.lettuce:lettuce-core") + api("io.projectreactor:reactor-core") + api("org.springframework.data:spring-data-redis") } diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis/build.gradle index 11f150cd1eec..b76ff7e34fe1 100644 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis/build.gradle +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis/build.gradle @@ -6,6 +6,6 @@ description = "Starter for using Redis key-value data store with Spring Data Red dependencies { api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter")) - api("org.springframework.data:spring-data-redis") api("io.lettuce:lettuce-core") + api("org.springframework.data:spring-data-redis") } From 062b5444479f905e4634a0c4bfc3dccd76419203 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 18 Oct 2023 21:13:42 -0700 Subject: [PATCH 0659/1215] Upgrade to Ubuntu Jammy 20231004 Closes gh-37957 --- ci/images/ci-image-jdk21/Dockerfile | 2 +- ci/images/ci-image/Dockerfile | 2 +- .../intTest/resources/conf/Ubuntu/jammy-20230624/Dockerfile | 4 ++-- .../src/intTest/resources/conf/oracle-jdk-17/Dockerfile | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ci/images/ci-image-jdk21/Dockerfile b/ci/images/ci-image-jdk21/Dockerfile index 533d1f25847d..a56960bf6dd6 100644 --- a/ci/images/ci-image-jdk21/Dockerfile +++ b/ci/images/ci-image-jdk21/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:jammy-20230916 +FROM ubuntu:jammy-20231004 ADD setup.sh /setup.sh ADD get-jdk-url.sh /get-jdk-url.sh diff --git a/ci/images/ci-image/Dockerfile b/ci/images/ci-image/Dockerfile index f3fe4e6dcf03..45b125643abc 100644 --- a/ci/images/ci-image/Dockerfile +++ b/ci/images/ci-image/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:jammy-20230916 +FROM ubuntu:jammy-20231004 ADD setup.sh /setup.sh ADD get-jdk-url.sh /get-jdk-url.sh diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/intTest/resources/conf/Ubuntu/jammy-20230624/Dockerfile b/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/intTest/resources/conf/Ubuntu/jammy-20230624/Dockerfile index 4e9da10f8ffa..9aa2d4f2bbe9 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/intTest/resources/conf/Ubuntu/jammy-20230624/Dockerfile +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-launch-script-tests/src/intTest/resources/conf/Ubuntu/jammy-20230624/Dockerfile @@ -1,10 +1,10 @@ -FROM ubuntu:jammy-20230624 as prepare +FROM ubuntu:jammy-20231004 as prepare COPY downloads/* /opt/download/ RUN mkdir -p /opt/jdk && \ cd /opt/jdk && \ tar xzf /opt/download/* --strip-components=1 -FROM ubuntu:jammy-20230624 +FROM ubuntu:jammy-20231004 RUN apt-get update && apt-get install -y software-properties-common curl COPY --from=prepare /opt/jdk /opt/jdk ENV JAVA_HOME /opt/jdk diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile index 0b920d47fde1..594958d179a4 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/resources/conf/oracle-jdk-17/Dockerfile @@ -1,10 +1,10 @@ -FROM ubuntu:jammy-20230624 as prepare +FROM ubuntu:jammy-20231004 as prepare COPY downloads/* /opt/download/ RUN mkdir -p /opt/jdk && \ cd /opt/jdk && \ tar xzf /opt/download/* --strip-components=1 -FROM ubuntu:jammy-20230624 +FROM ubuntu:jammy-20231004 COPY --from=prepare /opt/jdk /opt/jdk ENV JAVA_HOME /opt/jdk ENV PATH $JAVA_HOME/bin:$PATH From a354c236bb0d47580a6cf13b058ed82886efef8f Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 18 Oct 2023 21:14:54 -0700 Subject: [PATCH 0660/1215] Upgrade to Java 17.0.9+11 See gh-37944 --- ci/images/get-jdk-url.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/images/get-jdk-url.sh b/ci/images/get-jdk-url.sh index f3492b9b9f24..85ec0fe79eb5 100755 --- a/ci/images/get-jdk-url.sh +++ b/ci/images/get-jdk-url.sh @@ -3,7 +3,7 @@ set -e case "$1" in java17) - echo "https://github.com/bell-sw/Liberica/releases/download/17.0.8.1+1/bellsoft-jdk17.0.8.1+1-linux-amd64.tar.gz" + echo "https://github.com/bell-sw/Liberica/releases/download/17.0.9+11/bellsoft-jdk17.0.9+11-linux-amd64.tar.gz" ;; java21) echo "https://github.com/bell-sw/Liberica/releases/download/21+37/bellsoft-jdk21+37-linux-amd64.tar.gz" From a8ad8ca9f7898651d33632d732e8bc408eae8383 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 18 Oct 2023 21:15:45 -0700 Subject: [PATCH 0661/1215] Upgrade to Java 21.0.1+12 Closes gh-37945 --- ci/images/get-jdk-url.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/images/get-jdk-url.sh b/ci/images/get-jdk-url.sh index 85ec0fe79eb5..db11e34f6094 100755 --- a/ci/images/get-jdk-url.sh +++ b/ci/images/get-jdk-url.sh @@ -6,7 +6,7 @@ case "$1" in echo "https://github.com/bell-sw/Liberica/releases/download/17.0.9+11/bellsoft-jdk17.0.9+11-linux-amd64.tar.gz" ;; java21) - echo "https://github.com/bell-sw/Liberica/releases/download/21+37/bellsoft-jdk21+37-linux-amd64.tar.gz" + echo "https://github.com/bell-sw/Liberica/releases/download/21.0.1+12/bellsoft-jdk21.0.1+12-linux-amd64.tar.gz" ;; *) echo $"Unknown java version" From b8365e3118358c1b88bc997f3f744cca0fe4912e Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 18 Oct 2023 22:58:45 -0700 Subject: [PATCH 0662/1215] Fix misconfigured Maven test See gh-37669 --- .../src/intTest/projects/jar-with-classic-loader/pom.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/pom.xml index 64d9d04f9949..ce29e60f4029 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/pom.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/jar-with-classic-loader/pom.xml @@ -21,6 +21,9 @@ repackage + + CLASSIC + @@ -37,7 +40,6 @@ Foo - CLASSIC From 55dc2963ef247560223a0c02c646c0c4dc5d925b Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 19 Oct 2023 09:22:27 +0200 Subject: [PATCH 0663/1215] Rename Testcontainers bean startup property Old name: spring.testcontainers.startup New name: spring.testcontainers.beans.startup Closes gh-37073 --- .../spring-boot-docs/src/docs/asciidoc/features/testing.adoc | 2 +- .../boot/testcontainers/lifecycle/TestcontainersStartup.java | 2 +- .../META-INF/additional-spring-configuration-metadata.json | 2 +- ...stcontainersLifecycleApplicationContextInitializerTests.java | 2 +- .../testcontainers/lifecycle/TestcontainersStartupTests.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc index ea0400bdf569..ffa626e6e1a8 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc @@ -1072,7 +1072,7 @@ include::code:test/MyContainersConfiguration[] NOTE: The lifecycle of `Container` beans is automatically managed by Spring Boot. Containers will be started and stopped automatically. -TIP: You can use the configprop:spring.testcontainers.startup[] property to change how containers are started. +TIP: You can use the configprop:spring.testcontainers.beans.startup[] property to change how containers are started. By default `sequential` startup is used, but you may also choose `parallel` if you wish to start multiple containers in parallel. Once you have defined your test configuration, you can use the `with(...)` method to attach it to your test launcher: diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersStartup.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersStartup.java index 4554dbc83be7..00009a07fa6c 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersStartup.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersStartup.java @@ -61,7 +61,7 @@ void start(Collection startables) { * The {@link Environment} property used to change the {@link TestcontainersStartup} * strategy. */ - public static final String PROPERTY = "spring.testcontainers.startup"; + public static final String PROPERTY = "spring.testcontainers.beans.startup"; abstract void start(Collection startables); diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 708bb7854907..ca82e22875ba 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1,7 +1,7 @@ { "properties": [ { - "name": "spring.testcontainers.startup", + "name": "spring.testcontainers.beans.startup", "type": "org.springframework.boot.testcontainers.lifecycle.TestcontainersStartup", "description": "Testcontainers startup modes.", "defaultValue": "sequential" diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleApplicationContextInitializerTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleApplicationContextInitializerTests.java index dca0b39d1dc1..a00c0da51de9 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleApplicationContextInitializerTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleApplicationContextInitializerTests.java @@ -114,7 +114,7 @@ void setupStartupBasedOnEnvironmentProperty() { AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.getEnvironment() .getPropertySources() - .addLast(new MapPropertySource("test", Map.of("spring.testcontainers.startup", "parallel"))); + .addLast(new MapPropertySource("test", Map.of("spring.testcontainers.beans.startup", "parallel"))); new TestcontainersLifecycleApplicationContextInitializer().initialize(applicationContext); AbstractBeanFactory beanFactory = (AbstractBeanFactory) applicationContext.getBeanFactory(); BeanPostProcessor beanPostProcessor = beanFactory.getBeanPostProcessors() diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersStartupTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersStartupTests.java index fd88210ccaba..848ece580290 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersStartupTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersStartupTests.java @@ -82,7 +82,7 @@ void getWhenUnknownPropertyThrowsException() { MockEnvironment environment = new MockEnvironment(); assertThatIllegalArgumentException() .isThrownBy(() -> TestcontainersStartup.get(environment.withProperty(PROPERTY, "bad"))) - .withMessage("Unknown 'spring.testcontainers.startup' property value 'bad'"); + .withMessage("Unknown 'spring.testcontainers.beans.startup' property value 'bad'"); } private List createTestStartables(int size) { From e25886f2de508f5c412293cecd43ec7c997738a0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 15 Sep 2023 16:46:03 +0100 Subject: [PATCH 0664/1215] Consider checkpoint restoration when logging start time and uptime Closes gh-37084 --- .../spring-boot-parent/build.gradle | 7 ++ spring-boot-project/spring-boot/build.gradle | 1 + .../boot/SpringApplication.java | 106 +++++++++++++++++- .../boot/StartupInfoLogger.java | 23 ++-- .../boot/StartupInfoLoggerTests.java | 57 +++++++++- 5 files changed, 170 insertions(+), 24 deletions(-) diff --git a/spring-boot-project/spring-boot-parent/build.gradle b/spring-boot-project/spring-boot-parent/build.gradle index de20568d7ad9..d259269ce8a0 100644 --- a/spring-boot-project/spring-boot-parent/build.gradle +++ b/spring-boot-project/spring-boot-parent/build.gradle @@ -48,6 +48,13 @@ bom { ] } } + library("Crac", "1.4.0") { + group("org.crac") { + modules = [ + "crac" + ] + } + } library("Jakarta Inject", "2.0.1") { group("jakarta.inject") { modules = [ diff --git a/spring-boot-project/spring-boot/build.gradle b/spring-boot-project/spring-boot/build.gradle index ebc27fffe715..bad1e5e57f21 100644 --- a/spring-boot-project/spring-boot/build.gradle +++ b/spring-boot-project/spring-boot/build.gradle @@ -56,6 +56,7 @@ dependencies { optional("org.assertj:assertj-core") optional("org.apache.groovy:groovy") optional("org.apache.groovy:groovy-xml") + optional("org.crac:crac") optional("org.eclipse.jetty:jetty-alpn-conscrypt-server") optional("org.eclipse.jetty:jetty-client") optional("org.eclipse.jetty:jetty-util") diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index 6af18efefe80..b7b1d308e7bf 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -17,6 +17,7 @@ package org.springframework.boot; import java.lang.StackWalker.StackFrame; +import java.lang.management.ManagementFactory; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -35,6 +36,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.crac.management.CRaCMXBean; import org.springframework.aot.AotDetector; import org.springframework.beans.BeansException; @@ -301,10 +303,10 @@ private Optional> findMainClass(Stream stack) { * @return a running {@link ApplicationContext} */ public ConfigurableApplicationContext run(String... args) { + Startup startup = Startup.create(); if (this.registerShutdownHook) { SpringApplication.shutdownHook.enableShutdowHookAddition(); } - long startTime = System.nanoTime(); DefaultBootstrapContext bootstrapContext = createBootstrapContext(); ConfigurableApplicationContext context = null; configureHeadlessProperty(); @@ -319,11 +321,11 @@ public ConfigurableApplicationContext run(String... args) { prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner); refreshContext(context); afterRefresh(context, applicationArguments); - Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime); + startup.started(); if (this.logStartupInfo) { - new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup); + new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), startup); } - listeners.started(context, timeTakenToStartup); + listeners.started(context, startup.timeTakenToStarted()); callRunners(context, applicationArguments); } catch (Throwable ex) { @@ -335,8 +337,7 @@ public ConfigurableApplicationContext run(String... args) { } try { if (context.isRunning()) { - Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime); - listeners.ready(context, timeTakenToReady); + listeners.ready(context, startup.ready()); } } catch (Throwable ex) { @@ -1657,4 +1658,97 @@ public void run() { } + abstract static class Startup { + + private Duration timeTakenToStarted; + + abstract long startTime(); + + abstract Long processUptime(); + + abstract String action(); + + final Duration started() { + long now = System.currentTimeMillis(); + this.timeTakenToStarted = Duration.ofMillis(now - startTime()); + return this.timeTakenToStarted; + } + + private Duration ready() { + long now = System.currentTimeMillis(); + return Duration.ofMillis(now - startTime()); + } + + Duration timeTakenToStarted() { + return this.timeTakenToStarted; + } + + static Startup create() { + if (ClassUtils.isPresent("jdk.crac.management.CRaCMXBean", Startup.class.getClassLoader())) { + return new CracStartup(); + } + return new StandardStartup(); + } + + } + + private static class CracStartup extends Startup { + + private final StandardStartup fallback = new StandardStartup(); + + @Override + Long processUptime() { + long uptime = CRaCMXBean.getCRaCMXBean().getUptimeSinceRestore(); + return (uptime >= 0) ? uptime : this.fallback.processUptime(); + } + + @Override + String action() { + if (restoreTime() >= 0) { + return "Restored"; + } + return this.fallback.action(); + } + + private long restoreTime() { + return CRaCMXBean.getCRaCMXBean().getRestoreTime(); + } + + @Override + long startTime() { + long restoreTime = restoreTime(); + if (restoreTime >= 0) { + return restoreTime; + } + return this.fallback.startTime(); + } + + } + + private static class StandardStartup extends Startup { + + private final Long startTime = System.currentTimeMillis(); + + @Override + long startTime() { + return this.startTime; + } + + @Override + Long processUptime() { + try { + return ManagementFactory.getRuntimeMXBean().getUptime(); + } + catch (Throwable ex) { + return null; + } + } + + @Override + String action() { + return "Started"; + } + + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/StartupInfoLogger.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/StartupInfoLogger.java index 1f3104bb60b1..98e4ac32a64a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/StartupInfoLogger.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/StartupInfoLogger.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -16,13 +16,12 @@ package org.springframework.boot; -import java.lang.management.ManagementFactory; -import java.time.Duration; import java.util.concurrent.Callable; import org.apache.commons.logging.Log; import org.springframework.aot.AotDetector; +import org.springframework.boot.SpringApplication.Startup; import org.springframework.boot.system.ApplicationHome; import org.springframework.boot.system.ApplicationPid; import org.springframework.context.ApplicationContext; @@ -52,9 +51,9 @@ void logStarting(Log applicationLog) { applicationLog.debug(LogMessage.of(this::getRunningMessage)); } - void logStarted(Log applicationLog, Duration timeTakenToStartup) { + void logStarted(Log applicationLog, Startup startup) { if (applicationLog.isInfoEnabled()) { - applicationLog.info(getStartedMessage(timeTakenToStartup)); + applicationLog.info(getStartedMessage(startup)); } } @@ -79,20 +78,18 @@ private CharSequence getRunningMessage() { return message; } - private CharSequence getStartedMessage(Duration timeTakenToStartup) { + private CharSequence getStartedMessage(Startup startup) { StringBuilder message = new StringBuilder(); - message.append("Started"); + message.append(startup.action()); appendApplicationName(message); message.append(" in "); - message.append(timeTakenToStartup.toMillis() / 1000.0); + message.append(startup.timeTakenToStarted().toMillis() / 1000.0); message.append(" seconds"); - try { - double uptime = ManagementFactory.getRuntimeMXBean().getUptime() / 1000.0; + Long uptimeMs = startup.processUptime(); + if (uptimeMs != null) { + double uptime = uptimeMs / 1000.0; message.append(" (process running for ").append(uptime).append(")"); } - catch (Throwable ex) { - // No JVM time available - } return message; } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/StartupInfoLoggerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/StartupInfoLoggerTests.java index 6bcd00527f9b..a6b1c90ba6de 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/StartupInfoLoggerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/StartupInfoLoggerTests.java @@ -16,11 +16,10 @@ package org.springframework.boot; -import java.time.Duration; - import org.apache.commons.logging.Log; import org.junit.jupiter.api.Test; +import org.springframework.boot.SpringApplication.Startup; import org.springframework.boot.system.ApplicationPid; import static org.assertj.core.api.Assertions.assertThat; @@ -72,11 +71,59 @@ void startingFormatInAotMode() { @Test void startedFormat() { given(this.log.isInfoEnabled()).willReturn(true); - Duration timeTakenToStartup = Duration.ofMillis(10); - new StartupInfoLogger(getClass()).logStarted(this.log, timeTakenToStartup); + new StartupInfoLogger(getClass()).logStarted(this.log, new TestStartup(1345L, "Started")); then(this.log).should() .info(assertArg((message) -> assertThat(message.toString()).matches("Started " + getClass().getSimpleName() - + " in \\d+\\.\\d{1,3} seconds \\(process running for \\d+\\.\\d{1,3}\\)"))); + + " in \\d+\\.\\d{1,3} seconds \\(process running for 1.345\\)"))); + } + + @Test + void startedWithoutUptimeFormat() { + given(this.log.isInfoEnabled()).willReturn(true); + new StartupInfoLogger(getClass()).logStarted(this.log, new TestStartup(null, "Started")); + then(this.log).should() + .info(assertArg((message) -> assertThat(message.toString()) + .matches("Started " + getClass().getSimpleName() + " in \\d+\\.\\d{1,3} seconds"))); + } + + @Test + void restoredFormat() { + given(this.log.isInfoEnabled()).willReturn(true); + new StartupInfoLogger(getClass()).logStarted(this.log, new TestStartup(null, "Restored")); + then(this.log).should() + .info(assertArg((message) -> assertThat(message.toString()) + .matches("Restored " + getClass().getSimpleName() + " in \\d+\\.\\d{1,3} seconds"))); + } + + static class TestStartup extends Startup { + + private final long startTime = System.currentTimeMillis(); + + private final Long uptime; + + private final String action; + + TestStartup(Long uptime, String action) { + this.uptime = uptime; + this.action = action; + started(); + } + + @Override + long startTime() { + return this.startTime; + } + + @Override + Long processUptime() { + return this.uptime; + } + + @Override + String action() { + return this.action; + } + } } From deb79425ee04b426f3f4ab3f3b1f115d930ecc8d Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 19 Oct 2023 21:47:13 -0700 Subject: [PATCH 0665/1215] Polish --- .../java/org/springframework/boot/SpringApplication.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index b7b1d308e7bf..c22c51722414 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -1684,15 +1684,13 @@ Duration timeTakenToStarted() { } static Startup create() { - if (ClassUtils.isPresent("jdk.crac.management.CRaCMXBean", Startup.class.getClassLoader())) { - return new CracStartup(); - } - return new StandardStartup(); + return (!ClassUtils.isPresent("jdk.crac.management.CRaCMXBean", Startup.class.getClassLoader())) + ? new StandardStartup() : new CoordinatedRestoreAtCheckpointStartup(); } } - private static class CracStartup extends Startup { + private static class CoordinatedRestoreAtCheckpointStartup extends Startup { private final StandardStartup fallback = new StandardStartup(); From 32e6ce210e1315b35e814344e986c44e78c9ed95 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 19 Oct 2023 22:01:23 -0700 Subject: [PATCH 0666/1215] Allow PemPrivateKeyParser to parse multiple keys Update `PemPrivateKeyParser` so that it can parse multiple keys in a single PEM file. Closes gh-37970 --- .../boot/ssl/pem/PemPrivateKeyParser.java | 18 +++++-- .../boot/ssl/pem/PemSslStoreBundle.java | 10 +++- .../ssl/pem/PemPrivateKeyParserTests.java | 48 ++++++++++--------- 3 files changed, 47 insertions(+), 29 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java index 2aa432afb810..0df0fbdf4d55 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java @@ -69,6 +69,10 @@ final class PemPrivateKeyParser { private static final String SEC1_EC_FOOTER = "-+END\\s+EC\\s+PRIVATE\\s+KEY[^-]*-+"; + private static final String PKCS1_DSA_HEADER = "-+BEGIN\\s+DSA\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+"; + + private static final String PKCS1_DSA_FOOTER = "-+END\\s+DSA\\s+PRIVATE\\s+KEY[^-]*-+"; + private static final String BASE64_TEXT = "([a-z0-9+/=\\r\\n]+)"; public static final int BASE64_TEXT_GROUP = 1; @@ -83,6 +87,9 @@ final class PemPrivateKeyParser { "RSASSA-PSS", "EC", "DSA", "EdDSA", "XDH")); parsers.add(new PemParser(PKCS8_ENCRYPTED_HEADER, PKCS8_ENCRYPTED_FOOTER, PemPrivateKeyParser::createKeySpecForPkcs8Encrypted, "RSA", "RSASSA-PSS", "EC", "DSA", "EdDSA", "XDH")); + parsers.add(new PemParser(PKCS1_DSA_HEADER, PKCS1_DSA_FOOTER, (bytes, password) -> { + throw new IllegalStateException("Unsupported private key format"); + })); PEM_PARSERS = Collections.unmodifiableList(parsers); } @@ -172,7 +179,7 @@ private static PKCS8EncodedKeySpec createKeySpecForPkcs8Encrypted(byte[] bytes, * @param key the private key to parse * @return the parsed private key */ - static PrivateKey parse(String key) { + static PrivateKey[] parse(String key) { return parse(key, null); } @@ -183,22 +190,23 @@ static PrivateKey parse(String key) { * @param password the password used to decrypt an encrypted private key * @return the parsed private key */ - static PrivateKey parse(String key, String password) { + static PrivateKey[] parse(String key, String password) { if (key == null) { return null; } + List keys = new ArrayList<>(); try { for (PemParser pemParser : PEM_PARSERS) { PrivateKey privateKey = pemParser.parse(key, password); if (privateKey != null) { - return privateKey; + keys.add(privateKey); } } - throw new IllegalStateException("Unrecognized private key format"); } catch (Exception ex) { throw new IllegalStateException("Error loading private key file: " + ex.getMessage(), ex); } + return keys.toArray(PrivateKey[]::new); } /** @@ -239,7 +247,7 @@ private PrivateKey parse(byte[] bytes, String password) { catch (InvalidKeySpecException | NoSuchAlgorithmException ex) { } } - return null; + throw new IllegalStateException("Unrecognized private key format"); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java index b71750f3612f..7221305656ce 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java @@ -27,6 +27,7 @@ import org.springframework.boot.ssl.SslStoreBundle; import org.springframework.boot.ssl.pem.KeyVerifier.Result; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -150,13 +151,18 @@ private static void verifyKeys(PrivateKey privateKey, X509Certificate[] certific private static PrivateKey loadPrivateKey(PemSslStoreDetails details) { String privateKeyContent = PemContent.load(details.privateKey()); - return PemPrivateKeyParser.parse(privateKeyContent, details.privateKeyPassword()); + if (privateKeyContent == null) { + return null; + } + PrivateKey[] privateKeys = PemPrivateKeyParser.parse(privateKeyContent, details.privateKeyPassword()); + Assert.state(!ObjectUtils.isEmpty(privateKeys), "Loaded private keys are empty"); + return privateKeys[0]; } private static X509Certificate[] loadCertificates(PemSslStoreDetails details) { String certificateContent = PemContent.load(details.certificate()); X509Certificate[] certificates = PemCertificateParser.parse(certificateContent); - Assert.state(certificates != null && certificates.length > 0, "Loaded certificates are empty"); + Assert.state(!ObjectUtils.isEmpty(certificates), "Loaded certificates are empty"); return certificates; } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java index 3f5796103ab2..0d5587fa5688 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java @@ -27,6 +27,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.core.io.ClassPathResource; +import org.springframework.util.ObjectUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -49,7 +50,7 @@ class PemPrivateKeyParserTests { }) // @formatter:on void shouldParseTraditionalPkcs8(String file, String algorithm) throws IOException { - PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs8/" + file)); + PrivateKey privateKey = parse(read("org/springframework/boot/web/server/pkcs8/" + file)); assertThat(privateKey).isNotNull(); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo(algorithm); @@ -62,7 +63,7 @@ void shouldParseTraditionalPkcs8(String file, String algorithm) throws IOExcepti }) // @formatter:on void shouldParseTraditionalPkcs1(String file, String algorithm) throws IOException { - PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs1/" + file)); + PrivateKey privateKey = parse(read("org/springframework/boot/web/server/pkcs1/" + file)); assertThat(privateKey).isNotNull(); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo(algorithm); @@ -76,11 +77,11 @@ void shouldParseTraditionalPkcs1(String file, String algorithm) throws IOExcepti // @formatter:on void shouldNotParseUnsupportedTraditionalPkcs1(String file) { assertThatIllegalStateException() - .isThrownBy(() -> PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs1/" + file))) + .isThrownBy(() -> parse(read("org/springframework/boot/web/server/pkcs1/" + file))) .withMessageContaining("Error loading private key file") .withCauseInstanceOf(IllegalStateException.class) .havingCause() - .withMessageContaining("Unrecognized private key format"); + .withMessageContaining("Unsupported private key format"); } @ParameterizedTest @@ -99,7 +100,7 @@ void shouldNotParseUnsupportedTraditionalPkcs1(String file) { }) // @formatter:on void shouldParseEcPkcs8(String file, String curveName, String oid) throws IOException { - PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs8/" + file)); + PrivateKey privateKey = parse(read("org/springframework/boot/web/server/pkcs8/" + file)); assertThat(privateKey).isNotNull(); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo("EC"); @@ -134,7 +135,7 @@ void shouldNotParseUnsupportedEcPkcs8(String file) { }) // @formatter:on void shouldParseEdDsaPkcs8(String file) throws IOException { - PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs8/" + file)); + PrivateKey privateKey = parse(read("org/springframework/boot/web/server/pkcs8/" + file)); assertThat(privateKey).isNotNull(); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo("EdDSA"); @@ -148,7 +149,7 @@ void shouldParseEdDsaPkcs8(String file) throws IOException { }) // @formatter:on void shouldParseXdhPkcs8(String file) throws IOException { - PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs8/" + file)); + PrivateKey privateKey = parse(read("org/springframework/boot/web/server/pkcs8/" + file)); assertThat(privateKey).isNotNull(); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo("XDH"); @@ -170,7 +171,7 @@ void shouldParseXdhPkcs8(String file) throws IOException { }) // @formatter:on void shouldParseEcSec1(String file, String curveName, String oid) throws IOException { - PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/sec1/" + file)); + PrivateKey privateKey = parse(read("org/springframework/boot/web/server/sec1/" + file)); assertThat(privateKey).isNotNull(); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo("EC"); @@ -198,8 +199,8 @@ void shouldNotParseUnsupportedEcSec1(String file) { } @Test - void parseWithNonKeyTextWillThrowException() { - assertThatIllegalStateException().isThrownBy(() -> PemPrivateKeyParser.parse(read("test-banner.txt"))); + void parseWithNonKeyTextWillReturnEmptyArray() throws Exception { + assertThat(PemPrivateKeyParser.parse(read("test-banner.txt"))).isEmpty(); } @ParameterizedTest @@ -217,9 +218,10 @@ void shouldParseEncryptedPkcs8(String file, String algorithm) throws IOException // openssl pkcs8 -topk8 -in -out -v2 // -passout pass:test // where is aes128 or aes256 - PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs8/" + file), - "test"); - assertThat(privateKey).isNotNull(); + String content = read("org/springframework/boot/web/server/pkcs8/" + file); + PrivateKey[] privateKeys = PemPrivateKeyParser.parse(content, "test"); + assertThat(privateKeys).isNotEmpty(); + PrivateKey privateKey = privateKeys[0]; assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo(algorithm); } @@ -248,24 +250,26 @@ void shouldNotParseEncryptedPkcs8NotUsingPbkdf2() { } @Test - void shouldNotParseEncryptedSec1() { + void shouldNotParseEncryptedSec1() throws Exception { // created with: // openssl ecparam -genkey -name prime256v1 | openssl ec -aes-128-cbc -out // prime256v1-aes-128-cbc.key - assertThatIllegalStateException() - .isThrownBy(() -> PemPrivateKeyParser - .parse(read("org/springframework/boot/web/server/sec1/prime256v1-aes-128-cbc.key"), "test")) - .withMessageContaining("Unrecognized private key format"); + assertThat(PemPrivateKeyParser + .parse(read("org/springframework/boot/web/server/sec1/prime256v1-aes-128-cbc.key"), "test")).isEmpty(); } @Test void shouldNotParseEncryptedPkcs1() throws Exception { // created with: // openssl genrsa -aes-256-cbc -out rsa-aes-256-cbc.key - assertThatIllegalStateException() - .isThrownBy(() -> PemPrivateKeyParser - .parse(read("org/springframework/boot/web/server/pkcs1/rsa-aes-256-cbc.key"), "test")) - .withMessageContaining("Unrecognized private key format"); + assertThat(PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs1/rsa-aes-256-cbc.key"), + "test")) + .isEmpty(); + } + + private PrivateKey parse(String key) { + PrivateKey[] keys = PemPrivateKeyParser.parse(key); + return (!ObjectUtils.isEmpty(keys)) ? keys[0] : null; } private String read(String path) throws IOException { From 25ce0ef3fc7ce22c6c10798cc9e6ef5895ca7a39 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 19 Oct 2023 23:19:45 -0700 Subject: [PATCH 0667/1215] Refine `PemContent` and PEM parsers --- .../boot/ssl/pem/PemCertificateParser.java | 10 ++-- .../boot/ssl/pem/PemContent.java | 53 +++++++++++++++++-- .../boot/ssl/pem/PemPrivateKeyParser.java | 16 +++--- .../boot/ssl/pem/PemSslStoreBundle.java | 21 ++++---- .../ssl/pem/PemCertificateParserTests.java | 11 ++-- .../boot/ssl/pem/PemContentTests.java | 6 +-- .../ssl/pem/PemPrivateKeyParserTests.java | 9 ++-- 7 files changed, 86 insertions(+), 40 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemCertificateParser.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemCertificateParser.java index 327dcc94a1ff..58cf1ac4447d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemCertificateParser.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemCertificateParser.java @@ -48,17 +48,17 @@ private PemCertificateParser() { /** * Parse certificates from the specified string. - * @param certificates the certificates to parse + * @param text the text to parse * @return the parsed certificates */ - static X509Certificate[] parse(String certificates) { - if (certificates == null) { + static List parse(String text) { + if (text == null) { return null; } CertificateFactory factory = getCertificateFactory(); List certs = new ArrayList<>(); - readCertificates(certificates, factory, certs::add); - return (!certs.isEmpty()) ? certs.toArray(X509Certificate[]::new) : null; + readCertificates(text, factory, certs::add); + return List.copyOf(certs); } private static CertificateFactory getCertificateFactory() { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java index 317828575064..ec0a0905c5ed 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java @@ -21,6 +21,10 @@ import java.io.Reader; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Objects; import java.util.regex.Pattern; import org.springframework.util.FileCopyUtils; @@ -38,17 +42,56 @@ final class PemContent { private static final Pattern PEM_FOOTER = Pattern.compile("-+END\\s+[^-]*-+", Pattern.CASE_INSENSITIVE); - private PemContent() { + private String text; + + private PemContent(String text) { + this.text = text; + } + + List getCertificates() { + return PemCertificateParser.parse(this.text); + } + + List getPrivateKeys() { + return PemPrivateKeyParser.parse(this.text); + } + + List getPrivateKeys(String password) { + return PemPrivateKeyParser.parse(this.text, password); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return Objects.equals(this.text, ((PemContent) obj).text); + } + + @Override + public int hashCode() { + return Objects.hash(this.text); + } + + @Override + public String toString() { + return this.text; } - static String load(String content) { - if (content == null || isPemContent(content)) { - return content; + static PemContent load(String content) { + if (content == null) { + return null; + } + if (isPemContent(content)) { + return new PemContent(content); } try { URL url = ResourceUtils.getURL(content); try (Reader reader = new InputStreamReader(url.openStream(), StandardCharsets.UTF_8)) { - return FileCopyUtils.copyToString(reader); + return new PemContent(FileCopyUtils.copyToString(reader)); } } catch (IOException ex) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java index 0df0fbdf4d55..1d9c06b24439 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java @@ -176,28 +176,28 @@ private static PKCS8EncodedKeySpec createKeySpecForPkcs8Encrypted(byte[] bytes, /** * Parse a private key from the specified string. - * @param key the private key to parse + * @param text the text to parse * @return the parsed private key */ - static PrivateKey[] parse(String key) { - return parse(key, null); + static List parse(String text) { + return parse(text, null); } /** * Parse a private key from the specified string, using the provided password for * decryption if necessary. - * @param key the private key to parse + * @param text the text to parse * @param password the password used to decrypt an encrypted private key * @return the parsed private key */ - static PrivateKey[] parse(String key, String password) { - if (key == null) { + static List parse(String text, String password) { + if (text == null) { return null; } List keys = new ArrayList<>(); try { for (PemParser pemParser : PEM_PARSERS) { - PrivateKey privateKey = pemParser.parse(key, password); + PrivateKey privateKey = pemParser.parse(text, password); if (privateKey != null) { keys.add(privateKey); } @@ -206,7 +206,7 @@ static PrivateKey[] parse(String key, String password) { catch (Exception ex) { throw new IllegalStateException("Error loading private key file: " + ex.getMessage(), ex); } - return keys.toArray(PrivateKey[]::new); + return List.copyOf(keys); } /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java index 7221305656ce..14077cdbb20d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java @@ -23,11 +23,12 @@ import java.security.PrivateKey; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.List; import org.springframework.boot.ssl.SslStoreBundle; import org.springframework.boot.ssl.pem.KeyVerifier.Result; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** @@ -150,20 +151,20 @@ private static void verifyKeys(PrivateKey privateKey, X509Certificate[] certific } private static PrivateKey loadPrivateKey(PemSslStoreDetails details) { - String privateKeyContent = PemContent.load(details.privateKey()); - if (privateKeyContent == null) { + PemContent pemContent = PemContent.load(details.privateKey()); + if (pemContent == null) { return null; } - PrivateKey[] privateKeys = PemPrivateKeyParser.parse(privateKeyContent, details.privateKeyPassword()); - Assert.state(!ObjectUtils.isEmpty(privateKeys), "Loaded private keys are empty"); - return privateKeys[0]; + List privateKeys = pemContent.getPrivateKeys(details.privateKeyPassword()); + Assert.state(!CollectionUtils.isEmpty(privateKeys), "Loaded private keys are empty"); + return privateKeys.get(0); } private static X509Certificate[] loadCertificates(PemSslStoreDetails details) { - String certificateContent = PemContent.load(details.certificate()); - X509Certificate[] certificates = PemCertificateParser.parse(certificateContent); - Assert.state(!ObjectUtils.isEmpty(certificates), "Loaded certificates are empty"); - return certificates; + PemContent pemContent = PemContent.load(details.certificate()); + List certificates = pemContent.getCertificates(); + Assert.state(!CollectionUtils.isEmpty(certificates), "Loaded certificates are empty"); + return certificates.toArray(X509Certificate[]::new); } private static KeyStore createKeyStore(PemSslStoreDetails details) diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemCertificateParserTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemCertificateParserTests.java index 20ceee1b9a4d..db8f71f6b744 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemCertificateParserTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemCertificateParserTests.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; +import java.util.List; import org.junit.jupiter.api.Test; @@ -35,19 +36,19 @@ class PemCertificateParserTests { @Test void parseCertificate() throws Exception { - X509Certificate[] certificates = PemCertificateParser.parse(read("test-cert.pem")); + List certificates = PemCertificateParser.parse(read("test-cert.pem")); assertThat(certificates).isNotNull(); assertThat(certificates).hasSize(1); - assertThat(certificates[0].getType()).isEqualTo("X.509"); + assertThat(certificates.get(0).getType()).isEqualTo("X.509"); } @Test void parseCertificateChain() throws Exception { - X509Certificate[] certificates = PemCertificateParser.parse(read("test-cert-chain.pem")); + List certificates = PemCertificateParser.parse(read("test-cert-chain.pem")); assertThat(certificates).isNotNull(); assertThat(certificates).hasSize(2); - assertThat(certificates[0].getType()).isEqualTo("X.509"); - assertThat(certificates[1].getType()).isEqualTo("X.509"); + assertThat(certificates.get(0).getType()).isEqualTo("X.509"); + assertThat(certificates.get(1).getType()).isEqualTo("X.509"); } private String read(String path) throws IOException { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java index 649d66f699b2..6a8ddedb5d5f 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java @@ -57,19 +57,19 @@ void loadWhenContentIsPemContentReturnsContent() { +lGuHKdhNOVW9CmqPD1y76o6c8PQKuF7KZEoY2jvy3GeIfddBvqXgZ4PbWvFz1jO 32C9XWHwRA4= -----END CERTIFICATE-----"""; - assertThat(PemContent.load(content)).isEqualTo(content); + assertThat(PemContent.load(content)).hasToString(content); } @Test void loadWhenClasspathLocationReturnsContent() throws IOException { - String actual = PemContent.load("classpath:test-cert.pem"); + String actual = PemContent.load("classpath:test-cert.pem").toString(); String expected = new ClassPathResource("test-cert.pem").getContentAsString(StandardCharsets.UTF_8); assertThat(actual).isEqualTo(expected); } @Test void loadWhenFileLocationReturnsContent() throws IOException { - String actual = PemContent.load("src/test/resources/test-cert.pem"); + String actual = PemContent.load("src/test/resources/test-cert.pem").toString(); String expected = new ClassPathResource("test-cert.pem").getContentAsString(StandardCharsets.UTF_8); assertThat(actual).isEqualTo(expected); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java index 0d5587fa5688..c61396a63cd0 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java @@ -20,6 +20,7 @@ import java.nio.charset.StandardCharsets; import java.security.PrivateKey; import java.security.interfaces.ECPrivateKey; +import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -219,9 +220,9 @@ void shouldParseEncryptedPkcs8(String file, String algorithm) throws IOException // -passout pass:test // where is aes128 or aes256 String content = read("org/springframework/boot/web/server/pkcs8/" + file); - PrivateKey[] privateKeys = PemPrivateKeyParser.parse(content, "test"); + List privateKeys = PemPrivateKeyParser.parse(content, "test"); assertThat(privateKeys).isNotEmpty(); - PrivateKey privateKey = privateKeys[0]; + PrivateKey privateKey = privateKeys.get(0); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo(algorithm); } @@ -268,8 +269,8 @@ void shouldNotParseEncryptedPkcs1() throws Exception { } private PrivateKey parse(String key) { - PrivateKey[] keys = PemPrivateKeyParser.parse(key); - return (!ObjectUtils.isEmpty(keys)) ? keys[0] : null; + List keys = PemPrivateKeyParser.parse(key); + return (!ObjectUtils.isEmpty(keys)) ? keys.get(0) : null; } private String read(String path) throws IOException { From 4161eb1853211bc488b449897ba46ef02c6bd0fe Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 20 Oct 2023 09:15:54 +0100 Subject: [PATCH 0668/1215] Fix path handling in NestedLocation on Windows See 4b495ca See gh-37668 --- .../loader/net/protocol/nested/NestedLocation.java | 12 ++++++++++-- .../net/protocol/nested/NestedLocationTests.java | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java index c37555dec87f..07e304048e67 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java @@ -16,6 +16,7 @@ package org.springframework.boot.loader.net.protocol.nested; +import java.io.File; import java.net.URI; import java.net.URL; import java.nio.file.Path; @@ -100,9 +101,16 @@ static NestedLocation parse(String path) { } private static NestedLocation create(int index, String location) { - String path = location.substring(0, index); + String locationPath = location.substring(0, index); + if (isWindows() && locationPath.startsWith("/")) { + locationPath = locationPath.substring(1, locationPath.length()); + } String nestedEntryName = location.substring(index + 2); - return new NestedLocation((!path.isEmpty()) ? Path.of(path) : null, nestedEntryName); + return new NestedLocation((!locationPath.isEmpty()) ? Path.of(locationPath) : null, nestedEntryName); + } + + private static boolean isWindows() { + return File.separatorChar == '\\'; } static void clearCache() { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java index 5f4314de44dc..b71dc266be39 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java @@ -120,7 +120,7 @@ void fromUriWhenNoSeparatorThrowsExceptiuon() { void fromUriReturnsNestedLocation() throws Exception { File file = new File(this.temp, "test.jar"); NestedLocation location = NestedLocation - .fromUri(new URI("nested:" + file.getAbsolutePath() + "/!lib/nested.jar")); + .fromUri(new URI("nested:" + file.getAbsoluteFile().toURI().getPath() + "/!lib/nested.jar")); assertThat(location.path()).isEqualTo(file.toPath()); assertThat(location.nestedEntryName()).isEqualTo("lib/nested.jar"); } From d22969ae0915e0ff3cb1948a5b6e5d22a8fd0713 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 20 Oct 2023 10:19:34 +0100 Subject: [PATCH 0669/1215] Tolerate race condition in shouldStopKeepAliveThreadIfContextIsClosed See gh-37736 --- .../org/springframework/boot/SpringApplicationTests.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java index df29e1134bb6..275ad5a2735a 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java @@ -16,6 +16,7 @@ package org.springframework.boot; +import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -31,6 +32,7 @@ import jakarta.annotation.PostConstruct; import org.assertj.core.api.Condition; +import org.awaitility.Awaitility; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -1423,7 +1425,11 @@ void shouldStopKeepAliveThreadIfContextIsClosed() { assertThat(threadsBeforeClose).filteredOn((thread) -> thread.getName().equals("keep-alive")).isNotEmpty(); this.context.close(); Set threadsAfterClose = getCurrentThreads(); - assertThat(threadsAfterClose).filteredOn((thread) -> thread.getName().equals("keep-alive")).isEmpty(); + Awaitility.await() + .atMost(Duration.ofSeconds(30)) + .untilAsserted( + () -> assertThat(threadsAfterClose).filteredOn((thread) -> thread.getName().equals("keep-alive")) + .isEmpty()); } private ArgumentMatcher isAvailabilityChangeEventWithState( From dad5dc6750114c10eb314b15c0a05b1e49695e21 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 20 Oct 2023 10:56:08 +0100 Subject: [PATCH 0670/1215] Try to fix NestedFileSystemProviderTests on Windows See gh-7161 --- .../file/NestedFileSystemProviderTests.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemProviderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemProviderTests.java index dd357cb28d77..2a52f5e436d1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemProviderTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemProviderTests.java @@ -30,9 +30,12 @@ import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.spi.FileSystemProvider; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Set; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -69,6 +72,11 @@ void setup() throws Exception { this.uriPrefix = "nested:" + this.file.toURI().getPath() + "/!"; } + @AfterEach + void cleanUp() { + this.provider.cleanUp(); + } + @Test void getSchemeReturnsScheme() { assertThat(this.provider.getScheme()).isEqualTo("nested"); @@ -259,6 +267,8 @@ static class TestNestedFileSystemProvider extends NestedFileSystemProvider { private Path mockJarPath; + private List paths = new ArrayList<>(); + @Override protected Path getJarPath(Path path) { return (this.mockJarPath != null) ? this.mockJarPath : super.getJarPath(path); @@ -268,6 +278,24 @@ void setMockJarPath(Path mockJarPath) { this.mockJarPath = mockJarPath; } + @Override + public Path getPath(URI uri) { + Path path = super.getPath(uri); + this.paths.add(path); + return path; + } + + private void cleanUp() { + this.paths.forEach((path) -> { + try { + Path.of(path.toUri()).getFileSystem().close(); + } + catch (Exception ex) { + // Ignore + } + }); + } + } } From 88bf3dfba2b7f32391d84adb82319949be652372 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 20 Oct 2023 11:22:16 +0100 Subject: [PATCH 0671/1215] Close byte channel created by newByteChannelReturnsByteChannel See gh-7161 --- .../boot/loader/nio/file/NestedFileSystemProviderTests.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemProviderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemProviderTests.java index 2a52f5e436d1..6a7ebe6a9d3d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemProviderTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemProviderTests.java @@ -137,8 +137,9 @@ void getPathWhenFileSystemDoesNtExistReturnsPath() throws Exception { void newByteChannelReturnsByteChannel() throws Exception { URI uri = new URI(this.uriPrefix + "nested.jar"); Path path = this.provider.getPath(uri); - SeekableByteChannel byteChannel = this.provider.newByteChannel(path, Set.of(StandardOpenOption.READ)); - assertThat(byteChannel).isInstanceOf(NestedByteChannel.class); + try (SeekableByteChannel byteChannel = this.provider.newByteChannel(path, Set.of(StandardOpenOption.READ))) { + assertThat(byteChannel).isInstanceOf(NestedByteChannel.class); + } } @Test From d13d38a1411a514ea9401bdec43031c113b2ac50 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 20 Oct 2023 11:51:42 +0100 Subject: [PATCH 0672/1215] Trim multiple leading slashes in NestedLocation See gh-37668 --- .../boot/loader/net/protocol/nested/NestedLocation.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java index 07e304048e67..c350a34fdbf7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java @@ -102,8 +102,10 @@ static NestedLocation parse(String path) { private static NestedLocation create(int index, String location) { String locationPath = location.substring(0, index); - if (isWindows() && locationPath.startsWith("/")) { - locationPath = locationPath.substring(1, locationPath.length()); + if (isWindows()) { + while (locationPath.startsWith("/")) { + locationPath = locationPath.substring(1, locationPath.length()); + } } String nestedEntryName = location.substring(index + 2); return new NestedLocation((!locationPath.isEmpty()) ? Path.of(locationPath) : null, nestedEntryName); From 9897576562b65f93c8ab4523ded7b1aa9785fd45 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 20 Oct 2023 12:57:17 +0100 Subject: [PATCH 0673/1215] Polish d22969ae The current threads must be retrieved each time so that we can see the keep-alive thread dying. See gh-37736 --- .../org/springframework/boot/SpringApplicationTests.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java index 275ad5a2735a..92eefe0e3fe8 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java @@ -1421,15 +1421,12 @@ void shouldStopKeepAliveThreadIfContextIsClosed() { application.setWebApplicationType(WebApplicationType.NONE); application.setKeepAlive(true); this.context = application.run(); - Set threadsBeforeClose = getCurrentThreads(); - assertThat(threadsBeforeClose).filteredOn((thread) -> thread.getName().equals("keep-alive")).isNotEmpty(); + assertThat(getCurrentThreads()).filteredOn((thread) -> thread.getName().equals("keep-alive")).isNotEmpty(); this.context.close(); - Set threadsAfterClose = getCurrentThreads(); Awaitility.await() .atMost(Duration.ofSeconds(30)) - .untilAsserted( - () -> assertThat(threadsAfterClose).filteredOn((thread) -> thread.getName().equals("keep-alive")) - .isEmpty()); + .untilAsserted(() -> assertThat(getCurrentThreads()) + .filteredOn((thread) -> thread.getName().equals("keep-alive"))); } private ArgumentMatcher isAvailabilityChangeEventWithState( From e1bd24695de7d91ce1f405d4997ffab31ae28f81 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 20 Oct 2023 10:05:43 -0700 Subject: [PATCH 0674/1215] Try `@AssertFileChannelDataBlocksClosed` for Windows fix This updates commit dad5dc6750114c10eb314b15c0a05b1e49695e21 to see if @AssertFileChannelDataBlocksClosed will take care of closing files. See gh-7161 --- .../file/NestedFileSystemProviderTests.java | 30 ++----------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemProviderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemProviderTests.java index 6a7ebe6a9d3d..1204705a2098 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemProviderTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemProviderTests.java @@ -30,17 +30,15 @@ import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.spi.FileSystemProvider; -import java.util.ArrayList; import java.util.Collections; -import java.util.List; import java.util.Set; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.springframework.boot.loader.testsupport.TestJar; +import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -54,6 +52,7 @@ * * @author Phillip Webb */ +@AssertFileChannelDataBlocksClosed class NestedFileSystemProviderTests { @TempDir @@ -72,11 +71,6 @@ void setup() throws Exception { this.uriPrefix = "nested:" + this.file.toURI().getPath() + "/!"; } - @AfterEach - void cleanUp() { - this.provider.cleanUp(); - } - @Test void getSchemeReturnsScheme() { assertThat(this.provider.getScheme()).isEqualTo("nested"); @@ -268,8 +262,6 @@ static class TestNestedFileSystemProvider extends NestedFileSystemProvider { private Path mockJarPath; - private List paths = new ArrayList<>(); - @Override protected Path getJarPath(Path path) { return (this.mockJarPath != null) ? this.mockJarPath : super.getJarPath(path); @@ -279,24 +271,6 @@ void setMockJarPath(Path mockJarPath) { this.mockJarPath = mockJarPath; } - @Override - public Path getPath(URI uri) { - Path path = super.getPath(uri); - this.paths.add(path); - return path; - } - - private void cleanUp() { - this.paths.forEach((path) -> { - try { - Path.of(path.toUri()).getFileSystem().close(); - } - catch (Exception ex) { - // Ignore - } - }); - } - } } From e988bf4212e26cd26e6cfa2e987a1cc5b311e7ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 20 Oct 2023 14:44:25 +0200 Subject: [PATCH 0675/1215] Add JVM Checkpoint Restore documentation See gh-37975 --- .../src/docs/asciidoc/deployment/efficient.adoc | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc index 5b8db54d370d..c247d62dd007 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc @@ -65,3 +65,16 @@ It implies the following restrictions: - Properties that change if a bean is created are not supported (for example, `@ConditionalOnProperty` and `.enable` properties). To learn more about ahead-of-time processing, please see the <>. + +[[deployment.efficient.crac]] +=== Checkpoint and Restore With the JVM + +https://wiki.openjdk.org/display/crac/Main[CRaC] is an OpenJDK project that defines a new Java API to allow you to checkpoint and restore an application on the HotSpot JVM. It is based on https://github.com/checkpoint-restore/criu[CRIU], a project that implements checkpoint/restore functionality on Linux. + +The principle is the following: you start your application almost as usual but with a CRaC enabled version of the JDK like https://www.azul.com/downloads/?package=jdk-crac#zulu[the one provided by Azul]. Then at some point, potentially after some workloads that will make your JVM hot by executing all common code paths, you trigger a checkpoint using an API call, a `jcmd` command, an HTTP endpoint, or another mechanism. + +A memory representation of the running JVM, including its warmness, is then serialized to disk, allowing a very fast restoration at a later point, potentially on another machine with a similar operating system and CPU architecture. The restored process retains all the capabilities of the HotSpot JVM, including further JIT optimizations at runtime. + +Based on the foundations provided by Spring Framework, Spring Boot provides support for checkpointing and restoring your application, and manages out-of-the-box the lifecycle of resources such as socket, files and thread pools https://github.com/spring-projects/spring-checkpoint-restore-smoke-tests/blob/main/STATUS.adoc[on a limited scope]. Additional lifecycle management is expected for other dependencies and potentially for the application code dealing with such resources. + +You can find more details about the 2 modes supported ("on demand checkpoint/restore of a running application" and "automatic checkpoint/restore at startup"), how to enable Project CRaC support and some guidelines in {spring-framework-docs}/integration/checkpoint-restore.html[the Spring Framework JVM Checkpoint Restore support documentation]. \ No newline at end of file From 68d8fa14bf9d9d60945fb4c1bb9b17a01103b85f Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Mon, 23 Oct 2023 11:01:25 +0200 Subject: [PATCH 0676/1215] Polish "Add JVM Checkpoint Restore documentation" See gh-37975 --- .../src/docs/asciidoc/deployment/efficient.adoc | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc index c247d62dd007..1880abcd4731 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc @@ -66,15 +66,19 @@ It implies the following restrictions: To learn more about ahead-of-time processing, please see the <>. -[[deployment.efficient.crac]] +[[deployment.efficient.checkpoint-restore]] === Checkpoint and Restore With the JVM -https://wiki.openjdk.org/display/crac/Main[CRaC] is an OpenJDK project that defines a new Java API to allow you to checkpoint and restore an application on the HotSpot JVM. It is based on https://github.com/checkpoint-restore/criu[CRIU], a project that implements checkpoint/restore functionality on Linux. +https://wiki.openjdk.org/display/crac/Main[CRaC] is an OpenJDK project that defines a new Java API to allow you to checkpoint and restore an application on the HotSpot JVM. +It is based on https://github.com/checkpoint-restore/criu[CRIU], a project that implements checkpoint/restore functionality on Linux. -The principle is the following: you start your application almost as usual but with a CRaC enabled version of the JDK like https://www.azul.com/downloads/?package=jdk-crac#zulu[the one provided by Azul]. Then at some point, potentially after some workloads that will make your JVM hot by executing all common code paths, you trigger a checkpoint using an API call, a `jcmd` command, an HTTP endpoint, or another mechanism. +The principle is the following: you start your application almost as usual but with a CRaC enabled version of the JDK like https://www.azul.com/downloads/?package=jdk-crac#zulu[the one provided by Azul]. +Then at some point, potentially after some workloads that will warm up your JVM by executing all common code paths, you trigger a checkpoint using an API call, a `jcmd` command, an HTTP endpoint, or a different mechanism. -A memory representation of the running JVM, including its warmness, is then serialized to disk, allowing a very fast restoration at a later point, potentially on another machine with a similar operating system and CPU architecture. The restored process retains all the capabilities of the HotSpot JVM, including further JIT optimizations at runtime. +A memory representation of the running JVM, including its warmness, is then serialized to disk, allowing a fast restoration at a later point, potentially on another machine with a similar operating system and CPU architecture. +The restored process retains all the capabilities of the HotSpot JVM, including further JIT optimizations at runtime. -Based on the foundations provided by Spring Framework, Spring Boot provides support for checkpointing and restoring your application, and manages out-of-the-box the lifecycle of resources such as socket, files and thread pools https://github.com/spring-projects/spring-checkpoint-restore-smoke-tests/blob/main/STATUS.adoc[on a limited scope]. Additional lifecycle management is expected for other dependencies and potentially for the application code dealing with such resources. +Based on the foundations provided by Spring Framework, Spring Boot provides support for checkpointing and restoring your application, and manages out-of-the-box the lifecycle of resources such as socket, files and thread pools https://github.com/spring-projects/spring-checkpoint-restore-smoke-tests/blob/main/STATUS.adoc[on a limited scope]. +Additional lifecycle management is expected for other dependencies and potentially for the application code dealing with such resources. -You can find more details about the 2 modes supported ("on demand checkpoint/restore of a running application" and "automatic checkpoint/restore at startup"), how to enable Project CRaC support and some guidelines in {spring-framework-docs}/integration/checkpoint-restore.html[the Spring Framework JVM Checkpoint Restore support documentation]. \ No newline at end of file +You can find more details about the two modes supported ("on demand checkpoint/restore of a running application" and "automatic checkpoint/restore at startup"), how to enable checkpoint and restore support and some guidelines in {spring-framework-docs}/integration/checkpoint-restore.html[the Spring Framework JVM Checkpoint Restore support documentation]. From 1571097048942f440db472a778a3e5cde2ab74a7 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Sat, 21 Oct 2023 13:45:50 -0500 Subject: [PATCH 0677/1215] Fix links in Spring Kafka section of ref guide Spring for Apache Kafka recently moved to Antora docs which in turn modified the base url slightly. This commmit adjusts for the move. See gh-37987 --- .../spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc | 2 +- .../spring-boot-docs/src/docs/asciidoc/attributes.adoc | 2 +- .../spring-boot-docs/src/docs/asciidoc/messaging/kafka.adoc | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc index 2b0a5dd18042..2285fe4fe7fb 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc @@ -968,7 +968,7 @@ Metrics are published under the `spring.integration.` meter name. ==== Kafka Metrics Auto-configuration registers a `MicrometerConsumerListener` and `MicrometerProducerListener` for the auto-configured consumer factory and producer factory, respectively. It also registers a `KafkaStreamsMicrometerListener` for `StreamsBuilderFactoryBean`. -For more detail, see the {spring-kafka-docs}#micrometer-native[Micrometer Native Metrics] section of the Spring Kafka documentation. +For more detail, see the {spring-kafka-docs}kafka/micrometer.html#micrometer-native[Micrometer Native Metrics] section of the Spring Kafka documentation. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc index 92a2374e94c6..0cfd35b5abdd 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc @@ -89,7 +89,7 @@ :spring-graphql-docs: https://docs.spring.io/spring-graphql/reference/{spring-graphql-version}/ :spring-integration: https://spring.io/projects/spring-integration :spring-integration-docs: https://docs.spring.io/spring-integration/reference/{spring-integration-version}/ -:spring-kafka-docs: https://docs.spring.io/spring-kafka/docs/{spring-kafka-version}/reference/html/ +:spring-kafka-docs: https://docs.spring.io/spring-kafka/docs/{spring-kafka-version}/reference/ :spring-pulsar-docs: https://docs.spring.io/spring-pulsar/docs/{spring-pulsar-version}/reference/html/ :spring-restdocs: https://spring.io/projects/spring-restdocs :spring-security: https://spring.io/projects/spring-security diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/kafka.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/kafka.adoc index 9956d84f5f8e..314b0e3a0a93 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/kafka.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/kafka.adoc @@ -142,7 +142,7 @@ IMPORTANT: Properties set in this way override any configuration item that Sprin === Testing with Embedded Kafka Spring for Apache Kafka provides a convenient way to test projects with an embedded Apache Kafka broker. To use this feature, annotate a test class with `@EmbeddedKafka` from the `spring-kafka-test` module. -For more information, please see the Spring for Apache Kafka {spring-kafka-docs}#embedded-kafka-annotation[reference manual]. +For more information, please see the Spring for Apache Kafka {spring-kafka-docs}testing.html#ekb[reference manual]. To make Spring Boot auto-configuration work with the aforementioned embedded Apache Kafka broker, you need to remap a system property for embedded broker addresses (populated by the `EmbeddedKafkaBroker`) into the Spring Boot configuration property for Apache Kafka. There are several ways to do that: From ddd093f4e2070e1df22076381749a500d7850dde Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Sat, 21 Oct 2023 13:34:44 -0500 Subject: [PATCH 0678/1215] Fix links in Spring Pulsar section of ref guide Spring for Apache Pulsar recently moved to Antora docs which in turn modified the base url slightly. This commmit adjusts for the move. See gh-37986 --- .../spring-boot-docs/src/docs/asciidoc/attributes.adoc | 2 +- .../src/docs/asciidoc/messaging/pulsar.adoc | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc index 0cfd35b5abdd..6862e2efcc88 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc @@ -90,7 +90,7 @@ :spring-integration: https://spring.io/projects/spring-integration :spring-integration-docs: https://docs.spring.io/spring-integration/reference/{spring-integration-version}/ :spring-kafka-docs: https://docs.spring.io/spring-kafka/docs/{spring-kafka-version}/reference/ -:spring-pulsar-docs: https://docs.spring.io/spring-pulsar/docs/{spring-pulsar-version}/reference/html/ +:spring-pulsar-docs: https://docs.spring.io/spring-pulsar/docs/{spring-pulsar-version}/reference/ :spring-restdocs: https://spring.io/projects/spring-restdocs :spring-security: https://spring.io/projects/spring-security :spring-security-docs: https://docs.spring.io/spring-security/reference/{spring-security-version} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/pulsar.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/pulsar.adoc index 0294fab362b9..4a4d435bb5f4 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/pulsar.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/messaging/pulsar.adoc @@ -57,9 +57,9 @@ If you use other forms, such as `issuerurl` or `issuer-url`, the setting will no [[messaging.pulsar.connecting.ssl]] ==== SSL By default, Pulsar clients communicate with Pulsar services in plain text. -You can follow {spring-pulsar-docs}#tls-encryption[these steps] in the Spring for Apache Pulsar reference documentation to enable TLS encryption. +You can follow {spring-pulsar-docs}reference/pulsar.html#tls-encryption[these steps] in the Spring for Apache Pulsar reference documentation to enable TLS encryption. -For complete details on the client and authentication see the Spring for Apache Pulsar {spring-pulsar-docs}#pulsar-client[reference documentation]. +For complete details on the client and authentication see the Spring for Apache Pulsar {spring-pulsar-docs}reference/pulsar.html#pulsar-client[reference documentation]. @@ -85,7 +85,7 @@ If you need more control over the configuration, consider registering one or mor [[messaging.pulsar.admin.auth]] ==== Authentication When accessing a Pulsar cluster that requires authentication, the admin client requires the same security configuration as the regular Pulsar client. -You can use the aforementioned <> by replacing `spring.pulsar.client.authentication` with `spring.pulsar.admin.authentication`. +You can use the aforementioned <> by replacing `spring.pulsar.client.authentication` with `spring.pulsar.admin.authentication`. TIP: To create a topic on startup, add a bean of type `PulsarTopic`. If the topic already exists, the bean is ignored. From b5d4983829d9eeb32129ab35603f1f7be6336fe7 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Sat, 21 Oct 2023 16:45:11 +0900 Subject: [PATCH 0679/1215] Polish See gh-37984 --- .../OpenTelemetryAutoConfiguration.java | 2 +- .../tracing/MicrometerTracingAutoConfiguration.java | 4 ++-- .../WavefrontPropertiesConfigAdapterTests.java | 8 ++++---- .../MicrometerTracingAutoConfigurationTests.java | 9 ++------- .../tracing/zipkin/ZipkinWebClientSenderTests.java | 2 +- .../web/servlet/MultipartProperties.java | 2 +- .../data/redis/RedisAutoConfigurationJedisTests.java | 6 +++--- .../data/redis/RedisAutoConfigurationTests.java | 6 +++--- .../graphql/GraphQlAutoConfigurationTests.java | 2 +- .../validation/ValidationAutoConfigurationTests.java | 2 +- .../MyHealthMetricsExportConfiguration.kt | 12 +++++------- .../testsupport/testcontainers/DockerImageNames.java | 1 - .../boot/logging/logback/LogbackLoggingSystem.java | 2 +- ...backLoggingSystemParallelInitializationTests.java | 3 +-- .../boot/image/paketo/PaketoBuilderTests.java | 8 +++----- 15 files changed, 29 insertions(+), 40 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java index 3ffe4d0bb386..f87bfa6548dd 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java @@ -53,7 +53,7 @@ public class OpenTelemetryAutoConfiguration { */ private static final String DEFAULT_APPLICATION_NAME = "application"; - static final AttributeKey ATTRIBUTE_KEY_SERVICE_NAME = AttributeKey.stringKey("service.name"); + private static final AttributeKey ATTRIBUTE_KEY_SERVICE_NAME = AttributeKey.stringKey("service.name"); @Bean @ConditionalOnMissingBean(OpenTelemetry.class) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java index 40721dcbb90a..93d8acaa0e3d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java @@ -99,13 +99,13 @@ public PropagatingReceiverTracingObservationHandler propagatingReceiverTracin static class SpanAspectConfiguration { @Bean - @ConditionalOnMissingBean + @ConditionalOnMissingBean(NewSpanParser.class) DefaultNewSpanParser newSpanParser() { return new DefaultNewSpanParser(); } @Bean - @ConditionalOnMissingBean + @ConditionalOnMissingBean(MethodInvocationProcessor.class) ImperativeMethodInvocationProcessor imperativeMethodInvocationProcessor(NewSpanParser newSpanParser, Tracer tracer, ObjectProvider spanTagAnnotationHandler) { ImperativeMethodInvocationProcessor methodInvocationProcessor = new ImperativeMethodInvocationProcessor( diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapterTests.java index 6fd14b676d51..9c4439f538ef 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapterTests.java @@ -120,11 +120,11 @@ void whenPropertiesReportDayDistributionIsSetAdapterReportDayDistributionReturns CSP_CLIENT_CREDENTIALS, CSP_CLIENT_CREDENTIALS """) void whenTokenTypeIsSetAdapterReturnsIt(String property, String wavefront) { - TokenType propertyToken = property.equals("null") ? null : TokenType.valueOf(property); - Type wavefrontToken = Type.valueOf(wavefront); + TokenType propertyTokenType = property.equals("null") ? null : TokenType.valueOf(property); + Type wavefrontTokenType = Type.valueOf(wavefront); WavefrontProperties properties = new WavefrontProperties(); - properties.setApiTokenType(propertyToken); - assertThat(new WavefrontPropertiesConfigAdapter(properties).apiTokenType()).isEqualTo(wavefrontToken); + properties.setApiTokenType(propertyTokenType); + assertThat(new WavefrontPropertiesConfigAdapter(properties).apiTokenType()).isEqualTo(wavefrontTokenType); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java index 3ca8ff2364af..9c6c61f6f012 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java @@ -34,12 +34,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -158,9 +156,8 @@ void shouldConfigureSpanTagAnnotationHandler() { .run((context) -> { assertThat(context).hasSingleBean(DefaultNewSpanParser.class); assertThat(context).hasSingleBean(SpanAspect.class); - assertThat(ReflectionTestUtils.getField(context.getBean(ImperativeMethodInvocationProcessor.class), - "spanTagAnnotationHandler")) - .isSameAs(context.getBean(SpanTagAnnotationHandler.class)); + assertThat(context.getBean(ImperativeMethodInvocationProcessor.class)).hasFieldOrPropertyWithValue( + "spanTagAnnotationHandler", context.getBean(SpanTagAnnotationHandler.class)); }); } @@ -208,14 +205,12 @@ DefaultNewSpanParser customDefaultNewSpanParser() { } @Bean - @ConditionalOnMissingBean ImperativeMethodInvocationProcessor customImperativeMethodInvocationProcessor(NewSpanParser newSpanParser, Tracer tracer) { return new ImperativeMethodInvocationProcessor(newSpanParser, tracer); } @Bean - @ConditionalOnMissingBean SpanAspect customSpanAspect(MethodInvocationProcessor methodInvocationProcessor) { return new SpanAspect(methodInvocationProcessor); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSenderTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSenderTests.java index 4754d40c88a4..70b8402eaa8e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSenderTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinWebClientSenderTests.java @@ -162,7 +162,7 @@ void shouldTimeout(boolean async) { if (async) { CallbackResult callbackResult = makeAsyncRequest(sender, Collections.emptyList()); assertThat(callbackResult.success()).isFalse(); - assertThat(callbackResult.error()).isNotNull().isInstanceOf(TimeoutException.class); + assertThat(callbackResult.error()).isInstanceOf(TimeoutException.class); } else { assertThatException().isThrownBy(() -> makeSyncRequest(sender, Collections.emptyList())) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/MultipartProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/MultipartProperties.java index 6e3a6ccfa2e2..6e38a93927dd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/MultipartProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/MultipartProperties.java @@ -81,7 +81,7 @@ public class MultipartProperties { private boolean resolveLazily = false; /** - * Whether to resolve the multipart request strictly comply with the Servlet + * Whether to resolve the multipart request strictly complying with the Servlet * specification, only to be used for "multipart/form-data" requests. */ private boolean strictServletCompliance = false; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java index 2cbf2d5b0679..517b4d5a3bef 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java @@ -287,9 +287,9 @@ void shouldUsePlatformThreadsByDefault() { void shouldUseVirtualThreadsIfEnabled() { this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> { JedisConnectionFactory factory = context.getBean(JedisConnectionFactory.class); - SimpleAsyncTaskExecutor executor = (SimpleAsyncTaskExecutor) ReflectionTestUtils.getField(factory, - "executor"); - SimpleAsyncTaskExecutorAssert.assertThat(executor).usesVirtualThreads(); + assertThat(factory).extracting("executor") + .satisfies((executor) -> SimpleAsyncTaskExecutorAssert.assertThat((SimpleAsyncTaskExecutor) executor) + .usesVirtualThreads()); }); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java index 387ffe7da559..79d33e99f302 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java @@ -599,9 +599,9 @@ void shouldUsePlatformThreadsByDefault() { void shouldUseVirtualThreadsIfEnabled() { this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> { LettuceConnectionFactory factory = context.getBean(LettuceConnectionFactory.class); - SimpleAsyncTaskExecutor executor = (SimpleAsyncTaskExecutor) ReflectionTestUtils.getField(factory, - "executor"); - SimpleAsyncTaskExecutorAssert.assertThat(executor).usesVirtualThreads(); + assertThat(factory).extracting("executor") + .satisfies((executor) -> SimpleAsyncTaskExecutorAssert.assertThat((SimpleAsyncTaskExecutor) executor) + .usesVirtualThreads()); }); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java index 3b7182e18ab2..4d121314b5ff 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java @@ -214,7 +214,7 @@ void shouldContributeConnectionTypeDefinitionConfigurer() { GraphQlSource graphQlSource = context.getBean(GraphQlSource.class); GraphQLSchema schema = graphQlSource.schema(); GraphQLOutputType bookConnection = schema.getQueryType().getField("books").getType(); - assertThat(bookConnection).isNotNull().isInstanceOf(GraphQLObjectType.class); + assertThat(bookConnection).isInstanceOf(GraphQLObjectType.class); assertThat((GraphQLObjectType) bookConnection) .satisfies((connection) -> assertThat(connection.getFieldDefinition("edges")).isNotNull()); }); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfigurationTests.java index 4a46ae5ddc82..66dc324099c3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/ValidationAutoConfigurationTests.java @@ -217,7 +217,7 @@ void userDefinedMethodValidationPostProcessorTakesPrecedence() { .isSameAs(userMethodValidationPostProcessor); assertThat(context.getBeansOfType(MethodValidationPostProcessor.class)).hasSize(1); Object validator = ReflectionTestUtils.getField(userMethodValidationPostProcessor, "validator"); - assertThat(validator).isNotNull().isInstanceOf(Supplier.class); + assertThat(validator).isInstanceOf(Supplier.class); assertThat(context.getBean(Validator.class)).isNotSameAs(((Supplier) validator).get()); }); } diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/howto/actuator/maphealthindicatorstometrics/MyHealthMetricsExportConfiguration.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/howto/actuator/maphealthindicatorstometrics/MyHealthMetricsExportConfiguration.kt index 950e30242ce4..38e8f94f4c07 100644 --- a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/howto/actuator/maphealthindicatorstometrics/MyHealthMetricsExportConfiguration.kt +++ b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/howto/actuator/maphealthindicatorstometrics/MyHealthMetricsExportConfiguration.kt @@ -32,13 +32,11 @@ class MyHealthMetricsExportConfiguration(registry: MeterRegistry, healthEndpoint }.strongReference(true).register(registry) } - private fun getStatusCode(health: HealthEndpoint): Int { - return when (health.health().status) { - Status.UP -> 3 - Status.OUT_OF_SERVICE -> 2 - Status.DOWN -> 1 - else -> 0 - } + private fun getStatusCode(health: HealthEndpoint) = when (health.health().status) { + Status.UP -> 3 + Status.OUT_OF_SERVICE -> 2 + Status.DOWN -> 1 + else -> 0 } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java index 3e6feccdaa49..3089127fc417 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java @@ -160,7 +160,6 @@ public static DockerImageName oracleXe() { /** * Return a {@link DockerImageName} suitable for running OpenTelemetry. * @return a docker image name for running OpenTelemetry - * @since 3.2.0 */ public static DockerImageName opentelemetry() { return DockerImageName.parse("otel/opentelemetry-collector-contrib").withTag(OPENTELEMETRY_VERSION); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java index 668ec8375b72..e531e74ded60 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java @@ -412,7 +412,7 @@ private ILoggerFactory getLoggerFactory() { } catch (InterruptedException ex) { Thread.currentThread().interrupt(); - throw new IllegalStateException("Interrupted while waiting for non-subtitute logger factory", ex); + throw new IllegalStateException("Interrupted while waiting for non-substitute logger factory", ex); } factory = LoggerFactory.getILoggerFactory(); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemParallelInitializationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemParallelInitializationTests.java index 2a1b69567b5f..ea9d69cdf8fe 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemParallelInitializationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemParallelInitializationTests.java @@ -39,8 +39,7 @@ */ class LogbackLoggingSystemParallelInitializationTests { - private final LoggingSystem loggingSystem = LoggingSystem - .get(LogbackLoggingSystemParallelInitializationTests.class.getClassLoader()); + private final LoggingSystem loggingSystem = LoggingSystem.get(getClass().getClassLoader()); @AfterEach void cleanUp() { diff --git a/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java b/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java index 82d0fdd7d80a..d9af5bd4bf44 100644 --- a/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java +++ b/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/paketo/PaketoBuilderTests.java @@ -475,11 +475,9 @@ private String javaMajorVersion() { if (javaVersion.startsWith("1.")) { return javaVersion.substring(2, 3); } - else { - int firstDotIndex = javaVersion.indexOf("."); - if (firstDotIndex != -1) { - return javaVersion.substring(0, firstDotIndex); - } + int firstDotIndex = javaVersion.indexOf("."); + if (firstDotIndex != -1) { + return javaVersion.substring(0, firstDotIndex); } return javaVersion; } From 959311bf0e3973f9b08758ab7baefdfd34f2975c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 23 Oct 2023 15:25:13 +0100 Subject: [PATCH 0680/1215] Start building against Spring Framework 6.1.0 snapshots See gh-37995 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7f2a839097cc..9d3858acc5a1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ junitJupiterVersion=5.10.0 kotlinVersion=1.9.20-RC mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.27 -springFrameworkVersion=6.1.0-RC1 +springFrameworkVersion=6.1.0-SNAPSHOT tomcatVersion=10.1.15 kotlin.stdlib.default.dependency=false From a9469d9c8eb8a2a3b8bd60365a4a08f69ec1fce8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 23 Oct 2023 18:07:38 +0100 Subject: [PATCH 0681/1215] Adapt to changes in Framework's internals See gh-37995 --- .../web/reactive/error/AbstractErrorWebExceptionHandler.java | 3 +-- .../reactive/error/DefaultErrorWebExceptionHandlerTests.java | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java index 11c1bf536cc9..8b7daafd5c9c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java @@ -62,13 +62,12 @@ public abstract class AbstractErrorWebExceptionHandler implements ErrorWebExceptionHandler, InitializingBean { /** - * Currently duplicated from Spring WebFlux HttpWebHandlerAdapter. + * Currently duplicated from Spring Web's DisconnectedClientHelper. */ private static final Set DISCONNECTED_CLIENT_EXCEPTIONS; static { Set exceptions = new HashSet<>(); - exceptions.add("AbortedException"); exceptions.add("ClientAbortException"); exceptions.add("EOFException"); exceptions.add("EofException"); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerTests.java index c61f09fba999..a704411e835f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerTests.java @@ -38,7 +38,7 @@ import org.springframework.web.reactive.result.view.View; import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.adapter.HttpWebHandlerAdapter; +import org.springframework.web.util.DisconnectedClientHelper; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -58,8 +58,7 @@ class DefaultErrorWebExceptionHandlerTests { void disconnectedClientExceptionsMatchesFramework() { Object errorHandlers = ReflectionTestUtils.getField(AbstractErrorWebExceptionHandler.class, "DISCONNECTED_CLIENT_EXCEPTIONS"); - Object webHandlers = ReflectionTestUtils.getField(HttpWebHandlerAdapter.class, - "DISCONNECTED_CLIENT_EXCEPTIONS"); + Object webHandlers = ReflectionTestUtils.getField(DisconnectedClientHelper.class, "EXCEPTION_TYPE_NAMES"); assertThat(errorHandlers).isNotNull().isEqualTo(webHandlers); } From 2ac69160c7ecb582ab8b3fd684d953f7373f1adb Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 23 Oct 2023 18:32:12 -0700 Subject: [PATCH 0682/1215] Revert "Allow PemPrivateKeyParser to parse multiple keys" This reverts commit 32e6ce210e1315b35e814344e986c44e78c9ed95. Closes gh-37999 --- .../boot/ssl/pem/PemContent.java | 6 +-- .../boot/ssl/pem/PemPrivateKeyParser.java | 18 +++----- .../boot/ssl/pem/PemSslStoreBundle.java | 4 +- .../ssl/pem/PemPrivateKeyParserTests.java | 42 ++++++++++++------- 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java index ec0a0905c5ed..9d704598365e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java @@ -52,11 +52,7 @@ List getCertificates() { return PemCertificateParser.parse(this.text); } - List getPrivateKeys() { - return PemPrivateKeyParser.parse(this.text); - } - - List getPrivateKeys(String password) { + PrivateKey getPrivateKeys(String password) { return PemPrivateKeyParser.parse(this.text, password); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java index 1d9c06b24439..068ae51f6296 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java @@ -69,10 +69,6 @@ final class PemPrivateKeyParser { private static final String SEC1_EC_FOOTER = "-+END\\s+EC\\s+PRIVATE\\s+KEY[^-]*-+"; - private static final String PKCS1_DSA_HEADER = "-+BEGIN\\s+DSA\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+"; - - private static final String PKCS1_DSA_FOOTER = "-+END\\s+DSA\\s+PRIVATE\\s+KEY[^-]*-+"; - private static final String BASE64_TEXT = "([a-z0-9+/=\\r\\n]+)"; public static final int BASE64_TEXT_GROUP = 1; @@ -87,9 +83,6 @@ final class PemPrivateKeyParser { "RSASSA-PSS", "EC", "DSA", "EdDSA", "XDH")); parsers.add(new PemParser(PKCS8_ENCRYPTED_HEADER, PKCS8_ENCRYPTED_FOOTER, PemPrivateKeyParser::createKeySpecForPkcs8Encrypted, "RSA", "RSASSA-PSS", "EC", "DSA", "EdDSA", "XDH")); - parsers.add(new PemParser(PKCS1_DSA_HEADER, PKCS1_DSA_FOOTER, (bytes, password) -> { - throw new IllegalStateException("Unsupported private key format"); - })); PEM_PARSERS = Collections.unmodifiableList(parsers); } @@ -179,7 +172,7 @@ private static PKCS8EncodedKeySpec createKeySpecForPkcs8Encrypted(byte[] bytes, * @param text the text to parse * @return the parsed private key */ - static List parse(String text) { + static PrivateKey parse(String text) { return parse(text, null); } @@ -190,23 +183,22 @@ static List parse(String text) { * @param password the password used to decrypt an encrypted private key * @return the parsed private key */ - static List parse(String text, String password) { + static PrivateKey parse(String text, String password) { if (text == null) { return null; } - List keys = new ArrayList<>(); try { for (PemParser pemParser : PEM_PARSERS) { PrivateKey privateKey = pemParser.parse(text, password); if (privateKey != null) { - keys.add(privateKey); + return privateKey; } } + throw new IllegalStateException("Unrecognized private key format"); } catch (Exception ex) { throw new IllegalStateException("Error loading private key file: " + ex.getMessage(), ex); } - return List.copyOf(keys); } /** @@ -247,7 +239,7 @@ private PrivateKey parse(byte[] bytes, String password) { catch (InvalidKeySpecException | NoSuchAlgorithmException ex) { } } - throw new IllegalStateException("Unrecognized private key format"); + return null; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java index 14077cdbb20d..c1db6b4d9ae2 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java @@ -155,9 +155,7 @@ private static PrivateKey loadPrivateKey(PemSslStoreDetails details) { if (pemContent == null) { return null; } - List privateKeys = pemContent.getPrivateKeys(details.privateKeyPassword()); - Assert.state(!CollectionUtils.isEmpty(privateKeys), "Loaded private keys are empty"); - return privateKeys.get(0); + return pemContent.getPrivateKeys(details.privateKeyPassword()); } private static X509Certificate[] loadCertificates(PemSslStoreDetails details) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java index c61396a63cd0..431dca2af4ca 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java @@ -28,7 +28,6 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.core.io.ClassPathResource; -import org.springframework.util.ObjectUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -51,7 +50,7 @@ class PemPrivateKeyParserTests { }) // @formatter:on void shouldParseTraditionalPkcs8(String file, String algorithm) throws IOException { - PrivateKey privateKey = parse(read("org/springframework/boot/web/server/pkcs8/" + file)); + PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs8/" + file)); assertThat(privateKey).isNotNull(); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo(algorithm); @@ -64,7 +63,7 @@ void shouldParseTraditionalPkcs8(String file, String algorithm) throws IOExcepti }) // @formatter:on void shouldParseTraditionalPkcs1(String file, String algorithm) throws IOException { - PrivateKey privateKey = parse(read("org/springframework/boot/web/server/pkcs1/" + file)); + PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs1/" + file)); assertThat(privateKey).isNotNull(); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo(algorithm); @@ -78,11 +77,11 @@ void shouldParseTraditionalPkcs1(String file, String algorithm) throws IOExcepti // @formatter:on void shouldNotParseUnsupportedTraditionalPkcs1(String file) { assertThatIllegalStateException() - .isThrownBy(() -> parse(read("org/springframework/boot/web/server/pkcs1/" + file))) + .isThrownBy(() -> PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs1/" + file))) .withMessageContaining("Error loading private key file") .withCauseInstanceOf(IllegalStateException.class) .havingCause() - .withMessageContaining("Unsupported private key format"); + .withMessageContaining("Unrecognized private key format"); } @ParameterizedTest @@ -101,7 +100,7 @@ void shouldNotParseUnsupportedTraditionalPkcs1(String file) { }) // @formatter:on void shouldParseEcPkcs8(String file, String curveName, String oid) throws IOException { - PrivateKey privateKey = parse(read("org/springframework/boot/web/server/pkcs8/" + file)); + PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs8/" + file)); assertThat(privateKey).isNotNull(); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo("EC"); @@ -136,7 +135,7 @@ void shouldNotParseUnsupportedEcPkcs8(String file) { }) // @formatter:on void shouldParseEdDsaPkcs8(String file) throws IOException { - PrivateKey privateKey = parse(read("org/springframework/boot/web/server/pkcs8/" + file)); + PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs8/" + file)); assertThat(privateKey).isNotNull(); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo("EdDSA"); @@ -150,7 +149,7 @@ void shouldParseEdDsaPkcs8(String file) throws IOException { }) // @formatter:on void shouldParseXdhPkcs8(String file) throws IOException { - PrivateKey privateKey = parse(read("org/springframework/boot/web/server/pkcs8/" + file)); + PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs8/" + file)); assertThat(privateKey).isNotNull(); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo("XDH"); @@ -172,7 +171,7 @@ void shouldParseXdhPkcs8(String file) throws IOException { }) // @formatter:on void shouldParseEcSec1(String file, String curveName, String oid) throws IOException { - PrivateKey privateKey = parse(read("org/springframework/boot/web/server/sec1/" + file)); + PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/sec1/" + file)); assertThat(privateKey).isNotNull(); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo("EC"); @@ -200,8 +199,8 @@ void shouldNotParseUnsupportedEcSec1(String file) { } @Test - void parseWithNonKeyTextWillReturnEmptyArray() throws Exception { - assertThat(PemPrivateKeyParser.parse(read("test-banner.txt"))).isEmpty(); + void parseWithNonKeyTextWillThrowException() { + assertThatIllegalStateException().isThrownBy(() -> PemPrivateKeyParser.parse(read("test-banner.txt"))); } @ParameterizedTest @@ -219,10 +218,16 @@ void shouldParseEncryptedPkcs8(String file, String algorithm) throws IOException // openssl pkcs8 -topk8 -in -out -v2 // -passout pass:test // where is aes128 or aes256 +<<<<<<< HEAD String content = read("org/springframework/boot/web/server/pkcs8/" + file); List privateKeys = PemPrivateKeyParser.parse(content, "test"); assertThat(privateKeys).isNotEmpty(); PrivateKey privateKey = privateKeys.get(0); +======= + PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs8/" + file), + "test"); + assertThat(privateKey).isNotNull(); +>>>>>>> parent of 32e6ce210e1 (Allow PemPrivateKeyParser to parse multiple keys) assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo(algorithm); } @@ -251,18 +256,21 @@ void shouldNotParseEncryptedPkcs8NotUsingPbkdf2() { } @Test - void shouldNotParseEncryptedSec1() throws Exception { + void shouldNotParseEncryptedSec1() { // created with: // openssl ecparam -genkey -name prime256v1 | openssl ec -aes-128-cbc -out // prime256v1-aes-128-cbc.key - assertThat(PemPrivateKeyParser - .parse(read("org/springframework/boot/web/server/sec1/prime256v1-aes-128-cbc.key"), "test")).isEmpty(); + assertThatIllegalStateException() + .isThrownBy(() -> PemPrivateKeyParser + .parse(read("org/springframework/boot/web/server/sec1/prime256v1-aes-128-cbc.key"), "test")) + .withMessageContaining("Unrecognized private key format"); } @Test void shouldNotParseEncryptedPkcs1() throws Exception { // created with: // openssl genrsa -aes-256-cbc -out rsa-aes-256-cbc.key +<<<<<<< HEAD assertThat(PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs1/rsa-aes-256-cbc.key"), "test")) .isEmpty(); @@ -271,6 +279,12 @@ void shouldNotParseEncryptedPkcs1() throws Exception { private PrivateKey parse(String key) { List keys = PemPrivateKeyParser.parse(key); return (!ObjectUtils.isEmpty(keys)) ? keys.get(0) : null; +======= + assertThatIllegalStateException() + .isThrownBy(() -> PemPrivateKeyParser + .parse(read("org/springframework/boot/web/server/pkcs1/rsa-aes-256-cbc.key"), "test")) + .withMessageContaining("Unrecognized private key format"); +>>>>>>> parent of 32e6ce210e1 (Allow PemPrivateKeyParser to parse multiple keys) } private String read(String path) throws IOException { From 7bd9989614e99f7aa709df0139bf225bdd78b14c Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 23 Oct 2023 19:40:44 -0700 Subject: [PATCH 0683/1215] Fix merge conflict errors See gh-37999 --- .../ssl/pem/PemPrivateKeyParserTests.java | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java index 431dca2af4ca..e01584fb3caa 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java @@ -20,7 +20,6 @@ import java.nio.charset.StandardCharsets; import java.security.PrivateKey; import java.security.interfaces.ECPrivateKey; -import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -218,16 +217,9 @@ void shouldParseEncryptedPkcs8(String file, String algorithm) throws IOException // openssl pkcs8 -topk8 -in -out -v2 // -passout pass:test // where is aes128 or aes256 -<<<<<<< HEAD - String content = read("org/springframework/boot/web/server/pkcs8/" + file); - List privateKeys = PemPrivateKeyParser.parse(content, "test"); - assertThat(privateKeys).isNotEmpty(); - PrivateKey privateKey = privateKeys.get(0); -======= PrivateKey privateKey = PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs8/" + file), "test"); assertThat(privateKey).isNotNull(); ->>>>>>> parent of 32e6ce210e1 (Allow PemPrivateKeyParser to parse multiple keys) assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); assertThat(privateKey.getAlgorithm()).isEqualTo(algorithm); } @@ -267,24 +259,13 @@ void shouldNotParseEncryptedSec1() { } @Test - void shouldNotParseEncryptedPkcs1() throws Exception { + void shouldNotParseEncryptedPkcs1() { // created with: // openssl genrsa -aes-256-cbc -out rsa-aes-256-cbc.key -<<<<<<< HEAD - assertThat(PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs1/rsa-aes-256-cbc.key"), - "test")) - .isEmpty(); - } - - private PrivateKey parse(String key) { - List keys = PemPrivateKeyParser.parse(key); - return (!ObjectUtils.isEmpty(keys)) ? keys.get(0) : null; -======= assertThatIllegalStateException() .isThrownBy(() -> PemPrivateKeyParser .parse(read("org/springframework/boot/web/server/pkcs1/rsa-aes-256-cbc.key"), "test")) .withMessageContaining("Unrecognized private key format"); ->>>>>>> parent of 32e6ce210e1 (Allow PemPrivateKeyParser to parse multiple keys) } private String read(String path) throws IOException { From 4c3de96d74773e403e4032a3cfd9c630e0998ca1 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 24 Oct 2023 10:24:38 +0200 Subject: [PATCH 0684/1215] Fix links to Antora based JavaDoc and reference documentation This introduces two different attributes for, e.g. the Spring Framework version: the "normal" one, e.g. 6.1.0-SNAPSHOT, and the Antora one, e.g. 6.1. Spring Framework's reference documentation is still broken when using SNAPSHOT versions, see spring-framework/gh-31480. See gh-38000 --- .../build.gradle | 6 +++++- .../asciidoc/endpoints/integrationgraph.adoc | 2 +- .../spring-boot-docs/build.gradle | 16 +++++++++------- .../src/docs/asciidoc/actuator/metrics.adoc | 4 ++-- .../src/docs/asciidoc/attributes.adoc | 15 +++++++-------- .../src/docs/asciidoc/features/testing.adoc | 2 +- .../src/docs/asciidoc/howto/batch.adoc | 2 +- .../src/docs/asciidoc/web/spring-graphql.adoc | 10 +++++----- 8 files changed, 31 insertions(+), 26 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index 14d6c88a0b8b..7da8f6362edf 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -250,8 +250,12 @@ tasks.withType(org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask) { dependsOn dependencyVersions doFirst { def versionConstraints = dependencyVersions.versionConstraints + def toAntoraVersion = version -> { + String formatted = version.split("\\.").take(2).join('.') + return version.endsWith("-SNAPSHOT") ? formatted + "-SNAPSHOT" : formatted + } def integrationVersion = versionConstraints["org.springframework.integration:spring-integration-core"] - def integrationDocs = String.format("https://docs.spring.io/spring-integration/docs/%s/reference/html/", integrationVersion) + String integrationDocs = String.format("https://docs.spring.io/spring-integration/reference/%s", toAntoraVersion(integrationVersion)) attributes "spring-integration-docs": integrationDocs } dependsOn documentationTest diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/integrationgraph.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/integrationgraph.adoc index 5709ca935833..dad34a128c32 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/integrationgraph.adoc +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/asciidoc/endpoints/integrationgraph.adoc @@ -19,7 +19,7 @@ include::{snippets}/integrationgraph/graph/http-response.adoc[] [[integrationgraph.retrieving.response-structure]] === Response Structure The response contains all Spring Integration components used within the application, as well as the links between them. -More information about the structure can be found in the {spring-integration-docs}index-single.html#integration-graph[reference documentation]. +More information about the structure can be found in the {spring-integration-docs}/index.html#integration-graph[reference documentation]. diff --git a/spring-boot-project/spring-boot-docs/build.gradle b/spring-boot-project/spring-boot-docs/build.gradle index 5cbee491b503..a1a581c6b497 100644 --- a/spring-boot-project/spring-boot-docs/build.gradle +++ b/spring-boot-project/spring-boot-docs/build.gradle @@ -318,7 +318,7 @@ tasks.withType(org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask) { doFirst { def versionConstraints = dependencyVersions.versionConstraints def toAntoraVersion = version -> { - def formatted = version.split("\\.").take(2).join('.') + String formatted = version.split("\\.").take(2).join('.') return version.endsWith("-SNAPSHOT") ? formatted + "-SNAPSHOT" : formatted } attributes "hibernate-version": versionConstraints["org.hibernate.orm:hibernate-core"].split("\\.").take(2).join('.'), @@ -327,7 +327,8 @@ tasks.withType(org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask) { "lettuce-version": versionConstraints["io.lettuce:lettuce-core"], "native-build-tools-version": nativeBuildToolsVersion, "spring-amqp-version": versionConstraints["org.springframework.amqp:spring-amqp"], - "spring-batch-version": toAntoraVersion(versionConstraints["org.springframework.batch:spring-batch-core"]), + "spring-batch-version": versionConstraints["org.springframework.batch:spring-batch-core"], + "spring-batch-version-antora": toAntoraVersion(versionConstraints["org.springframework.batch:spring-batch-core"]), "spring-boot-version": project.version, "spring-data-commons-version": versionConstraints["org.springframework.data:spring-data-commons"], "spring-data-couchbase-version": versionConstraints["org.springframework.data:spring-data-couchbase"], @@ -338,13 +339,14 @@ tasks.withType(org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask) { "spring-data-neo4j-version": versionConstraints["org.springframework.data:spring-data-neo4j"], "spring-data-r2dbc-version": versionConstraints["org.springframework.data:spring-data-r2dbc"], "spring-data-rest-version": versionConstraints["org.springframework.data:spring-data-rest-core"], - "spring-framework-version": toAntoraVersion(versionConstraints["org.springframework:spring-core"]), - "spring-graphql-version": toAntoraVersion(versionConstraints["org.springframework.graphql:spring-graphql"]), - "spring-integration-version": toAntoraVersion(versionConstraints["org.springframework.integration:spring-integration-core"]), + "spring-framework-version": versionConstraints["org.springframework:spring-core"], + "spring-framework-version-antora": toAntoraVersion(versionConstraints["org.springframework:spring-core"]), + "spring-graphql-version-antora": toAntoraVersion(versionConstraints["org.springframework.graphql:spring-graphql"]), + "spring-integration-version-antora": toAntoraVersion(versionConstraints["org.springframework.integration:spring-integration-core"]), "spring-kafka-version": versionConstraints["org.springframework.kafka:spring-kafka"], "spring-pulsar-version": versionConstraints["org.springframework.pulsar:spring-pulsar"], - "spring-security-version": toAntoraVersion(versionConstraints["org.springframework.security:spring-security-core"]), - "spring-authorization-server-version": toAntoraVersion(versionConstraints["org.springframework.security:spring-security-oauth2-authorization-server"]), + "spring-security-version-antora": toAntoraVersion(versionConstraints["org.springframework.security:spring-security-core"]), + "spring-authorization-server-version-antora": toAntoraVersion(versionConstraints["org.springframework.security:spring-security-oauth2-authorization-server"]), "spring-webservices-version": versionConstraints["org.springframework.ws:spring-ws-core"], "tomcat-version": tomcatVersion.split("\\.").take(2).join('.'), "remote-spring-application-output": runRemoteSpringApplicationExample.outputs.files.singleFile, diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc index 2285fe4fe7fb..2dc886a9cc32 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc @@ -871,7 +871,7 @@ A `CacheMetricsRegistrar` bean is made available to make that process easier. [[actuator.metrics.supported.spring-batch]] ==== Spring Batch Metrics -See the {spring-batch-docs}monitoring-and-metrics.html[Spring Batch reference documentation]. +See the {spring-batch-docs}/monitoring-and-metrics.html[Spring Batch reference documentation]. @@ -959,7 +959,7 @@ Auto-configuration enables the instrumentation of all available RabbitMQ connect [[actuator.metrics.supported.spring-integration]] ==== Spring Integration Metrics -Spring Integration automatically provides {spring-integration-docs}metrics.html#micrometer-integration[Micrometer support] whenever a `MeterRegistry` bean is available. +Spring Integration automatically provides {spring-integration-docs}/metrics.html#micrometer-integration[Micrometer support] whenever a `MeterRegistry` bean is available. Metrics are published under the `spring.integration.` meter name. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc index 6862e2efcc88..9466a40e8085 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc @@ -55,8 +55,8 @@ :spring-boot-test-autoconfigure-module-api: {spring-boot-api}/org/springframework/boot/test/autoconfigure :spring-amqp-api: https://docs.spring.io/spring-amqp/docs/{spring-amqp-version}/api/org/springframework/amqp :spring-batch: https://spring.io/projects/spring-batch -:spring-batch-api: https://docs.spring.io/spring-batch/docs/{spring-batch-version}/api/org/springframework/batch -:spring-batch-docs: https://docs.spring.io/spring-batch/reference/{spring-batch-version}/ +:spring-batch-api: https://docs.spring.io/spring-batch/docs/{spring-batch-version}/org/springframework/batch +:spring-batch-docs: https://docs.spring.io/spring-batch/reference/{spring-batch-version-antora} :spring-data: https://spring.io/projects/spring-data :spring-data-cassandra: https://spring.io/projects/spring-data-cassandra :spring-data-commons-api: https://docs.spring.io/spring-data/commons/docs/{spring-data-commons-version}/api/org/springframework/data @@ -83,19 +83,18 @@ :spring-data-rest-api: https://docs.spring.io/spring-data/rest/docs/{spring-data-rest-version}/api/org/springframework/data/rest :spring-framework: https://spring.io/projects/spring-framework :spring-framework-api: https://docs.spring.io/spring-framework/docs/{spring-framework-version}/javadoc-api/org/springframework -:spring-framework-docs: https://docs.spring.io/spring-framework/reference/{spring-framework-version} +:spring-framework-docs: https://docs.spring.io/spring-framework/reference/{spring-framework-version-antora} :spring-graphql: https://spring.io/projects/spring-graphql -:spring-graphql-api: https://docs.spring.io/spring-graphql/docs/{spring-graphql-version}/api/ -:spring-graphql-docs: https://docs.spring.io/spring-graphql/reference/{spring-graphql-version}/ +:spring-graphql-docs: https://docs.spring.io/spring-graphql/reference/{spring-graphql-version-antora} :spring-integration: https://spring.io/projects/spring-integration -:spring-integration-docs: https://docs.spring.io/spring-integration/reference/{spring-integration-version}/ +:spring-integration-docs: https://docs.spring.io/spring-integration/reference/{spring-integration-version-antora} :spring-kafka-docs: https://docs.spring.io/spring-kafka/docs/{spring-kafka-version}/reference/ :spring-pulsar-docs: https://docs.spring.io/spring-pulsar/docs/{spring-pulsar-version}/reference/ :spring-restdocs: https://spring.io/projects/spring-restdocs :spring-security: https://spring.io/projects/spring-security -:spring-security-docs: https://docs.spring.io/spring-security/reference/{spring-security-version} +:spring-security-docs: https://docs.spring.io/spring-security/reference/{spring-security-version-antora} :spring-authorization-server: https://spring.io/projects/spring-authorization-server -:spring-authorization-server-docs: https://docs.spring.io/spring-authorization-server/reference/{spring-authorization-server-version}/ +:spring-authorization-server-docs: https://docs.spring.io/spring-authorization-server/reference/{spring-authorization-server-version-antora} :spring-session: https://spring.io/projects/spring-session :spring-webservices-docs: https://docs.spring.io/spring-ws/docs/{spring-webservices-version}/reference/html/ :ant-docs: https://ant.apache.org/manual diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc index ffa626e6e1a8..60a25852fd67 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc @@ -475,7 +475,7 @@ There are `GraphQlTester` variants and Spring Boot will auto-configure them depe * the `ExecutionGraphQlServiceTester` performs tests on the server side, without a client nor a transport * the `HttpGraphQlTester` performs tests with a client that connects to a server, with or without a live server -Spring Boot helps you to test your {spring-graphql-docs}#controllers[Spring GraphQL Controllers] with the `@GraphQlTest` annotation. +Spring Boot helps you to test your {spring-graphql-docs}/#controllers[Spring GraphQL Controllers] with the `@GraphQlTest` annotation. `@GraphQlTest` auto-configures the Spring GraphQL infrastructure, without any transport nor server being involved. This limits scanned beans to `@Controller`, `RuntimeWiringConfigurer`, `JsonComponent`, `Converter`, `GenericConverter`, `DataFetcherExceptionResolver`, `Instrumentation` and `GraphQlSourceBuilderCustomizer`. Regular `@Component` and `@ConfigurationProperties` beans are not scanned when the `@GraphQlTest` annotation is used. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc index f2113a069745..6edbcd2e2adf 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc @@ -59,4 +59,4 @@ This provides only one argument to the batch job: `someParameter=someValue`. === Storing the Job Repository Spring Batch requires a data store for the `Job` repository. If you use Spring Boot, you must use an actual database. -Note that it can be an in-memory database, see {spring-batch-docs}job.html#configuringJobRepository[Configuring a Job Repository]. +Note that it can be an in-memory database, see {spring-batch-docs}/job.html#configuringJobRepository[Configuring a Job Repository]. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/spring-graphql.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/spring-graphql.adoc index bde2f63e8ec4..fbdb9a41011c 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/spring-graphql.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/spring-graphql.adoc @@ -54,9 +54,9 @@ If you wish to not expose information about the schema, you can disable introspe === GraphQL RuntimeWiring The GraphQL Java `RuntimeWiring.Builder` can be used to register custom scalar types, directives, type resolvers, `DataFetcher`, and more. You can declare `RuntimeWiringConfigurer` beans in your Spring config to get access to the `RuntimeWiring.Builder`. -Spring Boot detects such beans and adds them to the {spring-graphql-docs}#execution-graphqlsource[GraphQlSource builder]. +Spring Boot detects such beans and adds them to the {spring-graphql-docs}/#execution-graphqlsource[GraphQlSource builder]. -Typically, however, applications will not implement `DataFetcher` directly and will instead create {spring-graphql-docs}#controllers[annotated controllers]. +Typically, however, applications will not implement `DataFetcher` directly and will instead create {spring-graphql-docs}/#controllers[annotated controllers]. Spring Boot will automatically detect `@Controller` classes with annotated handler methods and register those as ``DataFetcher``s. Here's a sample implementation for our greeting query with a `@Controller` class: @@ -67,7 +67,7 @@ include::code:GreetingController[] [[web.graphql.data-query]] === Querydsl and QueryByExample Repositories Support Spring Data offers support for both Querydsl and QueryByExample repositories. -Spring GraphQL can {spring-graphql-docs}#data[configure Querydsl and QueryByExample repositories as `DataFetcher`]. +Spring GraphQL can {spring-graphql-docs}/#data[configure Querydsl and QueryByExample repositories as `DataFetcher`]. Spring Data repositories annotated with `@GraphQlRepository` and extending one of: @@ -98,7 +98,7 @@ The GraphQL WebSocket endpoint is off by default. To enable it: * For a WebFlux application, no additional dependency is required * For both, the configprop:spring.graphql.websocket.path[] application property must be set -Spring GraphQL provides a {spring-graphql-docs}#web-interception[Web Interception] model. +Spring GraphQL provides a {spring-graphql-docs}/#web-interception[Web Interception] model. This is quite useful for retrieving information from an HTTP request header and set it in the GraphQL context or fetching information from the same context and writing it to a response header. With Spring Boot, you can declare a `WebInterceptor` bean to have it registered with the web transport. @@ -138,7 +138,7 @@ include::code:RSocketGraphQlClientExample[tag=request] [[web.graphql.exception-handling]] === Exception Handling Spring GraphQL enables applications to register one or more Spring `DataFetcherExceptionResolver` components that are invoked sequentially. -The Exception must be resolved to a list of `graphql.GraphQLError` objects, see {spring-graphql-docs}#execution-exceptions[Spring GraphQL exception handling documentation]. +The Exception must be resolved to a list of `graphql.GraphQLError` objects, see {spring-graphql-docs}/#execution-exceptions[Spring GraphQL exception handling documentation]. Spring Boot will automatically detect `DataFetcherExceptionResolver` beans and register them with the `GraphQlSource.Builder`. From 0950d4416a3a85b267791d158dcc21892e4d0542 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 24 Oct 2023 11:26:13 +0100 Subject: [PATCH 0685/1215] Pass in filter's name when adding to MockMvc Closes gh-38001 --- .../SpringBootMockMvcBuilderCustomizer.java | 4 +- ...ringBootMockMvcBuilderCustomizerTests.java | 38 +++++++++++++------ .../AbstractFilterRegistrationBean.java | 9 +++++ 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java index 357be7e93bde..fa4c8366fa30 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizer.java @@ -116,8 +116,8 @@ private void addFilters(ConfigurableMockMvcBuilder builder) { private void addFilter(ConfigurableMockMvcBuilder builder, AbstractFilterRegistrationBean registration) { Filter filter = registration.getFilter(); Collection urls = registration.getUrlPatterns(); - builder.addFilter(filter, registration.getInitParameters(), registration.determineDispatcherTypes(), - StringUtils.toStringArray(urls)); + builder.addFilter(filter, registration.getFilterName(), registration.getInitParameters(), + registration.determineDispatcherTypes(), StringUtils.toStringArray(urls)); } public void setAddFilters(boolean addFilters) { diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java index f3e7ffac61fd..207be549873c 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; @@ -66,15 +67,18 @@ void customizeShouldAddFilters() { DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(context); SpringBootMockMvcBuilderCustomizer customizer = new SpringBootMockMvcBuilderCustomizer(context); customizer.customize(builder); - FilterRegistrationBean registrationBean = (FilterRegistrationBean) context - .getBean("filterRegistrationBean"); - Filter testFilter = (Filter) context.getBean("testFilter"); - Filter otherTestFilter = registrationBean.getFilter(); + FilterRegistrationBean registrationBean = (FilterRegistrationBean) context.getBean("otherTestFilter"); + TestFilter testFilter = context.getBean("testFilter", TestFilter.class); + OtherTestFilter otherTestFilter = (OtherTestFilter) registrationBean.getFilter(); assertThat(builder).extracting("filters", as(InstanceOfAssertFactories.LIST)) - .extracting("delegate", "initParams", "dispatcherTypes") - .containsExactlyInAnyOrder(tuple(testFilter, Collections.emptyMap(), EnumSet.of(DispatcherType.REQUEST)), - tuple(otherTestFilter, Map.of("a", "alpha", "b", "bravo"), - EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR))); + .extracting("delegate", "dispatcherTypes") + .containsExactlyInAnyOrder(tuple(testFilter, EnumSet.of(DispatcherType.REQUEST)), + tuple(otherTestFilter, EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR))); + builder.build(); + assertThat(testFilter.filterName).isEqualTo("testFilter"); + assertThat(testFilter.initParams).isEmpty(); + assertThat(otherTestFilter.filterName).isEqualTo("otherTestFilter"); + assertThat(otherTestFilter.initParams).isEqualTo(Map.of("a", "alpha", "b", "bravo")); } @Test @@ -137,7 +141,7 @@ TestServlet testServlet() { static class FilterConfiguration { @Bean - FilterRegistrationBean filterRegistrationBean() { + FilterRegistrationBean otherTestFilter() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>( new OtherTestFilter()); filterRegistrationBean.setInitParameters(Map.of("a", "alpha", "b", "bravo")); @@ -158,9 +162,15 @@ static class TestServlet extends HttpServlet { static class TestFilter implements Filter { + private String filterName; + + private Map initParams = new HashMap<>(); + @Override public void init(FilterConfig filterConfig) { - + this.filterName = filterConfig.getFilterName(); + Collections.list(filterConfig.getInitParameterNames()) + .forEach((name) -> this.initParams.put(name, filterConfig.getInitParameter(name))); } @Override @@ -177,9 +187,15 @@ public void destroy() { static class OtherTestFilter implements Filter { + private String filterName; + + private Map initParams = new HashMap<>(); + @Override public void init(FilterConfig filterConfig) { - + this.filterName = filterConfig.getFilterName(); + Collections.list(filterConfig.getInitParameterNames()) + .forEach((name) -> this.initParams.put(name, filterConfig.getInitParameter(name))); } @Override diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/AbstractFilterRegistrationBean.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/AbstractFilterRegistrationBean.java index d7202385c54d..fa64a89724b0 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/AbstractFilterRegistrationBean.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/AbstractFilterRegistrationBean.java @@ -265,6 +265,15 @@ protected void configure(FilterRegistration.Dynamic registration) { */ public abstract T getFilter(); + /** + * Returns the filter name that will be registered. + * @return the filter name + * @since 3.2.0 + */ + public String getFilterName() { + return getOrDeduceName(getFilter()); + } + @Override public String toString() { StringBuilder builder = new StringBuilder(getOrDeduceName(this)); From 4e7c0737d44d5d6a15f5f6c736db3ad20be7fb20 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 24 Oct 2023 17:03:18 -0700 Subject: [PATCH 0686/1215] Fix PropertiesLauncher classpath detection without 'loader.path' set Update `PropertiesLauncher` to restore classpath detection logic applied when no `loader.path` property is set. Fixes gh-37992 --- .../boot/loader/launch/JarLauncher.java | 14 +++++---- .../loader/launch/PropertiesLauncher.java | 13 ++++++++- .../boot/loader/launch/WarLauncher.java | 14 +++++---- .../launch/PropertiesLauncherTests.java | 29 ++++++++++++++++++- 4 files changed, 58 insertions(+), 12 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java index ecabbc1fdf1d..3a6d1339ca11 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarLauncher.java @@ -38,11 +38,7 @@ protected JarLauncher(Archive archive) throws Exception { @Override protected boolean isIncludedOnClassPath(Archive.Entry entry) { - String name = entry.name(); - if (entry.isDirectory()) { - return name.equals("BOOT-INF/classes/"); - } - return name.startsWith("BOOT-INF/lib/"); + return isLibraryFileOrClassesDirectory(entry); } @Override @@ -50,6 +46,14 @@ protected String getEntryPathPrefix() { return "BOOT-INF/"; } + static boolean isLibraryFileOrClassesDirectory(Archive.Entry entry) { + String name = entry.name(); + if (entry.isDirectory()) { + return name.equals("BOOT-INF/classes/"); + } + return name.startsWith("BOOT-INF/lib/"); + } + public static void main(String[] args) throws Exception { new JarLauncher().launch(args); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java index 8b88484df48f..efa9d80c0b3f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/PropertiesLauncher.java @@ -140,7 +140,11 @@ public class PropertiesLauncher extends Launcher { private final Properties properties = new Properties(); public PropertiesLauncher() throws Exception { - this.archive = Archive.create(Launcher.class); + this(Archive.create(Launcher.class)); + } + + PropertiesLauncher(Archive archive) throws Exception { + this.archive = archive; this.homeDirectory = getHomeDirectory(); initializeProperties(); this.paths = getPaths(); @@ -464,6 +468,8 @@ protected Set getClassPathUrls() throws Exception { path = cleanupPath(handleUrl(path)); urls.addAll(getClassPathUrlsForPath(path)); } + urls.addAll(getClassPathUrlsForRoot()); + debug.log("Using class path URLs %s", urls); return urls; } @@ -531,6 +537,11 @@ private Set getClassPathUrlsForNested(String path) throws Exception { } } + private Set getClassPathUrlsForRoot() throws IOException { + debug.log("Adding classpath entries from root archive %s", this.archive); + return this.archive.getClassPathUrls(JarLauncher::isLibraryFileOrClassesDirectory); + } + private Predicate includeByPrefix(String prefix) { return (entry) -> (entry.isDirectory() && entry.name().equals(prefix)) || (isArchive(entry) && entry.name().startsWith(prefix)); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java index a74e63c4abcd..38318ba222c8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/WarLauncher.java @@ -37,11 +37,7 @@ protected WarLauncher(Archive archive) throws Exception { @Override public boolean isIncludedOnClassPath(Archive.Entry entry) { - String name = entry.name(); - if (entry.isDirectory()) { - return name.equals("WEB-INF/classes/"); - } - return name.startsWith("WEB-INF/lib/") || name.startsWith("WEB-INF/lib-provided/"); + return isLibraryFileOrClassesDirectory(entry); } @Override @@ -49,6 +45,14 @@ protected String getEntryPathPrefix() { return "WEB-INF/"; } + static boolean isLibraryFileOrClassesDirectory(Archive.Entry entry) { + String name = entry.name(); + if (entry.isDirectory()) { + return name.equals("WEB-INF/classes/"); + } + return name.startsWith("WEB-INF/lib/") || name.startsWith("WEB-INF/lib-provided/"); + } + public static void main(String[] args) throws Exception { new WarLauncher().launch(args); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/PropertiesLauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/PropertiesLauncherTests.java index 6e41ed6f912f..9bac00e9e74c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/PropertiesLauncherTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/PropertiesLauncherTests.java @@ -18,6 +18,7 @@ import java.io.File; import java.io.FileOutputStream; +import java.io.InputStream; import java.net.URL; import java.net.URLClassLoader; import java.time.Duration; @@ -26,7 +27,10 @@ import java.util.List; import java.util.Set; import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; import java.util.jar.Manifest; +import java.util.zip.ZipEntry; import org.assertj.core.api.Condition; import org.awaitility.Awaitility; @@ -309,8 +313,8 @@ void testArgsEnhanced() throws Exception { assertThat(Arrays.asList(this.launcher.getArgs("bar"))).hasToString("[foo, bar]"); } - @SuppressWarnings("unchecked") @Test + @SuppressWarnings("unchecked") void testLoadPathCustomizedUsingManifest() throws Exception { System.setProperty("loader.home", this.tempDir.getAbsolutePath()); Manifest manifest = new Manifest(); @@ -364,6 +368,29 @@ void loadResourceFromJarFile() throws Exception { assertThat(bytes).isNotEmpty(); } + @Test // gh-37992 + void classPathWithoutLoaderPathDefaultsToJarLauncherIncludes() throws Exception { + File file = new File(this.tempDir, "test.jar"); + try (JarOutputStream out = new JarOutputStream(new FileOutputStream(file))) { + try (JarFile in = new JarFile(new File("src/test/resources/jars/app.jar"))) { + out.putNextEntry(new ZipEntry("BOOT-INF/")); + out.putNextEntry(new ZipEntry("BOOT-INF/classes/")); + out.putNextEntry(new ZipEntry("BOOT-INF/classes/demo/")); + out.putNextEntry(new ZipEntry("BOOT-INF/classes/demo/Application.class")); + try (InputStream classIn = in.getInputStream(in.getEntry("demo/Application.class"))) { + classIn.transferTo(out); + } + out.closeEntry(); + } + } + Archive archive = new JarFileArchive(file); + System.setProperty("loader.main", "demo.Application"); + this.launcher = new PropertiesLauncher(archive); + this.launcher.launch(new String[0]); + waitFor("Hello World"); + + } + private void waitFor(String value) { Awaitility.waitAtMost(Duration.ofSeconds(5)).until(this.output::toString, containsString(value)); } From 0c66db7b1815cfea98e336bccaea85246ba4b4f0 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 25 Oct 2023 11:53:04 -0700 Subject: [PATCH 0687/1215] Refine container initialization and parallel startup logic Update `TestcontainersLifecycleBeanPostProcessor` to restore early container initialization logic and refine startup logic. Initial bean access now again triggers the creation all container beans. In addition the first access of a `Startable` bean now attempts to find and start all other `Startable` beans. Fixes gh-37989 --- ...tcontainersLifecycleBeanPostProcessor.java | 68 ++++++++++++------- 1 file changed, 45 insertions(+), 23 deletions(-) diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java index 7033b961a298..edafed5c7d01 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java @@ -20,6 +20,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import org.apache.commons.logging.Log; @@ -62,7 +63,9 @@ class TestcontainersLifecycleBeanPostProcessor implements DestructionAwareBeanPo private final TestcontainersStartup startup; - private volatile boolean containersInitialized = false; + private final AtomicBoolean startablesInitialized = new AtomicBoolean(); + + private final AtomicBoolean containersInitialized = new AtomicBoolean(); TestcontainersLifecycleBeanPostProcessor(ConfigurableListableBeanFactory beanFactory, TestcontainersStartup startup) { @@ -72,20 +75,53 @@ class TestcontainersLifecycleBeanPostProcessor implements DestructionAwareBeanPo @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (!this.containersInitialized && this.beanFactory.isConfigurationFrozen()) { + if (this.beanFactory.isConfigurationFrozen() && this.containersInitialized.compareAndSet(false, true)) { initializeContainers(); } + if (bean instanceof Startable startableBean) { + if (this.startablesInitialized.compareAndSet(false, true)) { + initializeStartables(startableBean, beanName); + } + else { + startableBean.start(); + } + } return bean; } + private void initializeStartables(Startable startableBean, String startableBeanName) { + List beanNames = new ArrayList<>( + List.of(this.beanFactory.getBeanNamesForType(Startable.class, false, false))); + beanNames.remove(startableBeanName); + List beans = getBeans(beanNames); + if (beans == null) { + this.startablesInitialized.set(false); + return; + } + beanNames.add(startableBeanName); + beans.add(startableBean); + start(beans); + if (!beanNames.isEmpty()) { + logger.debug(LogMessage.format("Initialized and started startable beans '%s'", beanNames)); + } + } + + private void start(List beans) { + Set startables = beans.stream() + .filter(Startable.class::isInstance) + .map(Startable.class::cast) + .collect(Collectors.toCollection(LinkedHashSet::new)); + this.startup.start(startables); + } + private void initializeContainers() { - Set beanNames = new LinkedHashSet<>(); - beanNames.addAll(List.of(this.beanFactory.getBeanNamesForType(ContainerState.class, false, false))); - beanNames.addAll(List.of(this.beanFactory.getBeanNamesForType(Startable.class, false, false))); - initializeContainers(beanNames); + List beanNames = List.of(this.beanFactory.getBeanNamesForType(ContainerState.class, false, false)); + if (getBeans(beanNames) == null) { + this.containersInitialized.set(false); + } } - private void initializeContainers(Set beanNames) { + private List getBeans(List beanNames) { List beans = new ArrayList<>(beanNames.size()); for (String beanName : beanNames) { try { @@ -93,26 +129,12 @@ private void initializeContainers(Set beanNames) { } catch (BeanCreationException ex) { if (ex.contains(BeanCurrentlyInCreationException.class)) { - return; + return null; } throw ex; } } - if (!this.containersInitialized) { - this.containersInitialized = true; - if (!beanNames.isEmpty()) { - logger.debug(LogMessage.format("Initialized container beans '%s'", beanNames)); - } - start(beans); - } - } - - private void start(List beans) { - Set startables = beans.stream() - .filter(Startable.class::isInstance) - .map(Startable.class::cast) - .collect(Collectors.toCollection(LinkedHashSet::new)); - this.startup.start(startables); + return beans; } @Override From b35c4d64971b1cb61765fa801384ecdc0c7f317f Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 25 Oct 2023 21:20:42 -0700 Subject: [PATCH 0688/1215] Open loader jar URLs by default using `runtimeVersion` Update `UrlJarFileFactory` so that `runtimeVersion` is used by default instead of `baseVersion`. Prior to this commit we tried to mirror the JDK handler on look for a `#runtime` fragment. This unfortunately doesn't work with the URLs produced by `URLClassPath`. This commit also fixes a bug in `NestedJarFile` where we didn't return the correct result from `hasEntry`. Fixes gh-38050 --- .../org/springframework/boot/loader/jar/NestedJarFile.java | 2 +- .../boot/loader/net/protocol/jar/UrlJarFileFactory.java | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java index 2fe35c46c169..d711b9137a52 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java @@ -230,7 +230,7 @@ public boolean hasEntry(String name) { } ZipContent.Entry entry = getVersionedContentEntry(name); if (entry != null) { - return false; + return true; } synchronized (this) { ensureOpen(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java index 7a45caea98a2..35ba9283ca94 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java @@ -58,7 +58,12 @@ JarFile createJarFile(URL jarFileUrl, Consumer closeAction) throws IOEx } private Runtime.Version getVersion(URL url) { - return "runtime".equals(url.getRef()) ? JarFile.runtimeVersion() : JarFile.baseVersion(); + // The standard JDK handler uses #runtime to indicate that the runtime version + // should be used. This unfortunately doesn't work for us as + // jdk.internal.loaderURLClassPath only adds the runtime fragment when the URL + // is using the internal JDK handler. We need to flip the default to use + // the runtime version. See gh-38050 + return "base".equals(url.getRef()) ? JarFile.baseVersion() : JarFile.runtimeVersion(); } private boolean isLocalFileUrl(URL url) { From 01e2f70c7339db01958c234ef96fd24d1f1b98b2 Mon Sep 17 00:00:00 2001 From: DevSeoRex Date: Wed, 25 Oct 2023 21:58:02 +0900 Subject: [PATCH 0689/1215] Extract "server.ports" hardcoding into a constant See gh-38029 --- .../RSocketPortInfoApplicationContextInitializer.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/context/RSocketPortInfoApplicationContextInitializer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/context/RSocketPortInfoApplicationContextInitializer.java index da497d9ab1d6..1e160726e1a3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/context/RSocketPortInfoApplicationContextInitializer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/context/RSocketPortInfoApplicationContextInitializer.java @@ -54,6 +54,7 @@ public void initialize(ConfigurableApplicationContext applicationContext) { private static class Listener implements ApplicationListener { private static final String PROPERTY_NAME = "local.rsocket.server.port"; + private static final String SERVER_PORTS = "server.ports"; private final ConfigurableApplicationContext applicationContext; @@ -79,9 +80,9 @@ private void setPortProperty(ApplicationContext context, int port) { private void setPortProperty(ConfigurableEnvironment environment, int port) { MutablePropertySources sources = environment.getPropertySources(); - PropertySource source = sources.get("server.ports"); + PropertySource source = sources.get(SERVER_PORTS); if (source == null) { - source = new MapPropertySource("server.ports", new HashMap<>()); + source = new MapPropertySource(SERVER_PORTS, new HashMap<>()); sources.addFirst(source); } setPortProperty(port, source); From 8095c2a94b720ebb712bb3eb5c1f71f4c733a3d4 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 26 Oct 2023 14:35:02 +0200 Subject: [PATCH 0690/1215] Polish "Extract "server.ports" hardcoding into a constant" See gh-38029 --- .../context/RSocketPortInfoApplicationContextInitializer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/context/RSocketPortInfoApplicationContextInitializer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/context/RSocketPortInfoApplicationContextInitializer.java index 1e160726e1a3..7a3fed4a02e9 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/context/RSocketPortInfoApplicationContextInitializer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/context/RSocketPortInfoApplicationContextInitializer.java @@ -54,6 +54,7 @@ public void initialize(ConfigurableApplicationContext applicationContext) { private static class Listener implements ApplicationListener { private static final String PROPERTY_NAME = "local.rsocket.server.port"; + private static final String SERVER_PORTS = "server.ports"; private final ConfigurableApplicationContext applicationContext; From 5ff4a961b131a32c3b6417a4ebd0e55fe8d1a504 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 26 Oct 2023 15:18:02 +0200 Subject: [PATCH 0691/1215] Polish 0fbb1f7890adcc904782de2efb434c34dfe9531c See gh-38029 --- .../RSocketPortInfoApplicationContextInitializer.java | 6 +++--- .../ServerPortInfoApplicationContextInitializer.java | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/context/RSocketPortInfoApplicationContextInitializer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/context/RSocketPortInfoApplicationContextInitializer.java index 7a3fed4a02e9..dd94ba58204a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/context/RSocketPortInfoApplicationContextInitializer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/context/RSocketPortInfoApplicationContextInitializer.java @@ -55,7 +55,7 @@ private static class Listener implements ApplicationListener source = sources.get(SERVER_PORTS); + PropertySource source = sources.get(PROPERTY_SOURCE_NAME); if (source == null) { - source = new MapPropertySource(SERVER_PORTS, new HashMap<>()); + source = new MapPropertySource(PROPERTY_SOURCE_NAME, new HashMap<>()); sources.addFirst(source); } setPortProperty(port, source); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/context/ServerPortInfoApplicationContextInitializer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/context/ServerPortInfoApplicationContextInitializer.java index 7e3fafe9f410..2ec73ab57a12 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/context/ServerPortInfoApplicationContextInitializer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/context/ServerPortInfoApplicationContextInitializer.java @@ -52,6 +52,8 @@ public class ServerPortInfoApplicationContextInitializer implements ApplicationContextInitializer, ApplicationListener { + private static final String PROPERTY_SOURCE_NAME = "server.ports"; + @Override public void initialize(ConfigurableApplicationContext applicationContext) { applicationContext.addApplicationListener(this); @@ -80,9 +82,9 @@ private void setPortProperty(ApplicationContext context, String propertyName, in @SuppressWarnings("unchecked") private void setPortProperty(ConfigurableEnvironment environment, String propertyName, int port) { MutablePropertySources sources = environment.getPropertySources(); - PropertySource source = sources.get("server.ports"); + PropertySource source = sources.get(PROPERTY_SOURCE_NAME); if (source == null) { - source = new MapPropertySource("server.ports", new HashMap<>()); + source = new MapPropertySource(PROPERTY_SOURCE_NAME, new HashMap<>()); sources.addFirst(source); } ((Map) source.getSource()).put(propertyName, port); From bba323ba5fad3668f85b34ee543291ab3ea21100 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 26 Oct 2023 13:17:45 -0700 Subject: [PATCH 0692/1215] Copy zip data descriptor records when creating virtual zip data The zip specification states that when 'bit 3' of the general purpose flags is set then a data descriptor record must be present. Prior to this commit, our `VirtualZipDataBlock` ignored such records and would create invalid data. Although the generated data would work for zip parsers that read the central directory records, it causes problems with streaming reader implementations such as `JarInputStream`. This commit updates the code so that it now copies the data descriptor records. It support both blocks that have a signature and those that don't. It also updates the generation logic to correctly deal with any extra data bytes present after the local file header record. Fixes gh-38063 --- .../boot/loader/zip/ByteArrayDataBlock.java | 6 +- .../boot/loader/zip/VirtualZipDataBlock.java | 30 +++-- .../loader/zip/ZipDataDescriptorRecord.java | 120 ++++++++++++++++++ .../loader/zip/ZipLocalFileHeaderRecord.java | 1 + .../loader/zip/VirtualZipDataBlockTests.java | 40 ++++++ .../zip/ZipDataDescriptorRecordTests.java | 111 ++++++++++++++++ 6 files changed, 296 insertions(+), 12 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipDataDescriptorRecord.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipDataDescriptorRecordTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ByteArrayDataBlock.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ByteArrayDataBlock.java index 577cb2dc3b29..d1a4f7fcf982 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ByteArrayDataBlock.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ByteArrayDataBlock.java @@ -24,7 +24,7 @@ * * @author Phillip Webb */ -class ByteArrayDataBlock implements DataBlock { +class ByteArrayDataBlock implements CloseableDataBlock { private final byte[] bytes; @@ -53,4 +53,8 @@ private int read(ByteBuffer dst, int pos) { return length; } + @Override + public void close() throws IOException { + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualZipDataBlock.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualZipDataBlock.java index 21021da25e53..6ba095c74193 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualZipDataBlock.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualZipDataBlock.java @@ -30,7 +30,7 @@ */ class VirtualZipDataBlock extends VirtualDataBlock implements CloseableDataBlock { - private final FileChannelDataBlock data; + private final CloseableDataBlock data; /** * Create a new {@link VirtualZipDataBlock} for the given entries. @@ -40,7 +40,7 @@ class VirtualZipDataBlock extends VirtualDataBlock implements CloseableDataBlock * @param centralRecordPositions the record positions in the data block. * @throws IOException on I/O error */ - VirtualZipDataBlock(FileChannelDataBlock data, NameOffsetLookups nameOffsetLookups, + VirtualZipDataBlock(CloseableDataBlock data, NameOffsetLookups nameOffsetLookups, ZipCentralDirectoryFileHeaderRecord[] centralRecords, long[] centralRecordPositions) throws IOException { this.data = data; List parts = new ArrayList<>(); @@ -54,12 +54,14 @@ class VirtualZipDataBlock extends VirtualDataBlock implements CloseableDataBlock DataBlock name = new DataPart( centralRecordPos + ZipCentralDirectoryFileHeaderRecord.FILE_NAME_OFFSET + nameOffset, (centralRecord.fileNameLength() & 0xFFFF) - nameOffset); - ZipLocalFileHeaderRecord localRecord = ZipLocalFileHeaderRecord.load(this.data, - centralRecord.offsetToLocalHeader()); - DataBlock content = new DataPart(centralRecord.offsetToLocalHeader() + localRecord.size(), - centralRecord.compressedSize()); + long localRecordPos = centralRecord.offsetToLocalHeader() & 0xFFFFFFFF; + ZipLocalFileHeaderRecord localRecord = ZipLocalFileHeaderRecord.load(this.data, localRecordPos); + DataBlock content = new DataPart(localRecordPos + localRecord.size(), centralRecord.compressedSize()); + boolean hasDescriptorRecord = ZipDataDescriptorRecord.isPresentBasedOnFlag(centralRecord); + ZipDataDescriptorRecord dataDescriptorRecord = (!hasDescriptorRecord) ? null + : ZipDataDescriptorRecord.load(data, localRecordPos + localRecord.size() + content.size()); sizeOfCentralDirectory += addToCentral(centralParts, centralRecord, centralRecordPos, name, (int) offset); - offset += addToLocal(parts, localRecord, name, content); + offset += addToLocal(parts, centralRecord, localRecord, dataDescriptorRecord, name, content); } parts.addAll(centralParts); ZipEndOfCentralDirectoryRecord eocd = new ZipEndOfCentralDirectoryRecord((short) centralRecords.length, @@ -83,14 +85,20 @@ private long addToCentral(List parts, ZipCentralDirectoryFileHeaderRe return record.size(); } - private long addToLocal(List parts, ZipLocalFileHeaderRecord originalRecord, DataBlock name, + private long addToLocal(List parts, ZipCentralDirectoryFileHeaderRecord centralRecord, + ZipLocalFileHeaderRecord originalRecord, ZipDataDescriptorRecord dataDescriptorRecord, DataBlock name, DataBlock content) throws IOException { - ZipLocalFileHeaderRecord record = originalRecord.withExtraFieldLength((short) 0) - .withFileNameLength((short) (name.size() & 0xFFFF)); + ZipLocalFileHeaderRecord record = originalRecord.withFileNameLength((short) (name.size() & 0xFFFF)); + long originalRecordPos = centralRecord.offsetToLocalHeader() & 0xFFFFFFFF; + int extraFieldLength = originalRecord.extraFieldLength() & 0xFFFF; parts.add(new ByteArrayDataBlock(record.asByteArray())); parts.add(name); + parts.add(new DataPart(originalRecordPos + originalRecord.size() - extraFieldLength, extraFieldLength)); parts.add(content); - return record.size() + content.size(); + if (dataDescriptorRecord != null) { + parts.add(new ByteArrayDataBlock(dataDescriptorRecord.asByteArray())); + } + return record.size() + content.size() + ((dataDescriptorRecord != null) ? dataDescriptorRecord.size() : 0); } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipDataDescriptorRecord.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipDataDescriptorRecord.java new file mode 100644 index 000000000000..af3a85027ec8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipDataDescriptorRecord.java @@ -0,0 +1,120 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import org.springframework.boot.loader.log.DebugLogger; + +/** + * A ZIP File "Data Descriptor" record. + * + * @param includeSignature if the signature bytes are written or not (see note in spec) + * @param crc32 the CRC32 checksum + * @param compressedSize the size of the entry when compressed + * @param uncompressedSize the size of the entry when uncompressed + * @author Phillip Webb + * @see Chapter + * 4.3.9 of the Zip File Format Specification + */ +record ZipDataDescriptorRecord(boolean includeSignature, int crc32, int compressedSize, int uncompressedSize) { + + private static final DebugLogger debug = DebugLogger.get(ZipDataDescriptorRecord.class); + + private static final int SIGNATURE = 0x08074b50; + + private static final int DATA_SIZE = 12; + + private static final int SIGNATURE_SIZE = 4; + + long size() { + return (!includeSignature()) ? DATA_SIZE : DATA_SIZE + SIGNATURE_SIZE; + } + + /** + * Return the contents of this record as a byte array suitable for writing to a zip. + * @return the record as a byte array + */ + byte[] asByteArray() { + ByteBuffer buffer = ByteBuffer.allocate((int) size()); + buffer.order(ByteOrder.LITTLE_ENDIAN); + if (this.includeSignature) { + buffer.putInt(SIGNATURE); + } + buffer.putInt(this.crc32); + buffer.putInt(this.compressedSize); + buffer.putInt(this.uncompressedSize); + return buffer.array(); + } + + /** + * Load the {@link ZipDataDescriptorRecord} from the given data block. + * @param dataBlock the source data block + * @param pos the position of the record + * @return a new {@link ZipLocalFileHeaderRecord} instance + * @throws IOException on I/O error + */ + static ZipDataDescriptorRecord load(DataBlock dataBlock, long pos) throws IOException { + debug.log("Loading ZipDataDescriptorRecord from position %s", pos); + ByteBuffer buffer = ByteBuffer.allocate(SIGNATURE_SIZE + DATA_SIZE); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.limit(SIGNATURE_SIZE); + dataBlock.readFully(buffer, pos); + buffer.rewind(); + int signatureOrCrc = buffer.getInt(); + boolean hasSignature = (signatureOrCrc == SIGNATURE); + buffer.rewind(); + buffer.limit((!hasSignature) ? DATA_SIZE - SIGNATURE_SIZE : DATA_SIZE); + dataBlock.readFully(buffer, pos + SIGNATURE_SIZE); + buffer.rewind(); + return new ZipDataDescriptorRecord(hasSignature, (!hasSignature) ? signatureOrCrc : buffer.getInt(), + buffer.getInt(), buffer.getInt()); + } + + /** + * Return if the {@link ZipDataDescriptorRecord} is present based on the general + * purpose bit flag in the given {@link ZipLocalFileHeaderRecord}. + * @param localRecord the local record to check + * @return if the bit flag is set + */ + static boolean isPresentBasedOnFlag(ZipLocalFileHeaderRecord localRecord) { + return isPresentBasedOnFlag(localRecord.generalPurposeBitFlag()); + } + + /** + * Return if the {@link ZipDataDescriptorRecord} is present based on the general + * purpose bit flag in the given {@link ZipCentralDirectoryFileHeaderRecord}. + * @param centralRecord the central record to check + * @return if the bit flag is set + */ + static boolean isPresentBasedOnFlag(ZipCentralDirectoryFileHeaderRecord centralRecord) { + return isPresentBasedOnFlag(centralRecord.generalPurposeBitFlag()); + } + + /** + * Return if the {@link ZipDataDescriptorRecord} is present based on the given general + * purpose bit flag. + * @param generalPurposeBitFlag the general purpose bit flag to check + * @return if the bit flag is set + */ + static boolean isPresentBasedOnFlag(int generalPurposeBitFlag) { + return (generalPurposeBitFlag & 0b0000_1000) != 0; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipLocalFileHeaderRecord.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipLocalFileHeaderRecord.java index 8d77ca585aec..daed69afb9b7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipLocalFileHeaderRecord.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipLocalFileHeaderRecord.java @@ -121,4 +121,5 @@ static ZipLocalFileHeaderRecord load(DataBlock dataBlock, long pos) throws IOExc buffer.getShort(), buffer.getInt(), buffer.getInt(), buffer.getInt(), buffer.getShort(), buffer.getShort()); } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/VirtualZipDataBlockTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/VirtualZipDataBlockTests.java index 42ea979673ca..33f93bfb0f02 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/VirtualZipDataBlockTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/VirtualZipDataBlockTests.java @@ -25,6 +25,8 @@ import java.nio.file.NoSuchFileException; import java.util.ArrayList; import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -95,4 +97,42 @@ void createContainsValidZipContent() throws IOException { } } + @Test // gh-38063 + void createWithDescriptorRecordContainsValidZipContent() throws Exception { + try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(this.file))) { + ZipEntry entry = new ZipEntry("META-INF/"); + entry.setMethod(ZipEntry.DEFLATED); + zip.putNextEntry(entry); + zip.write(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 }); + zip.closeEntry(); + } + byte[] bytes = Files.readAllBytes(this.file.toPath()); + CloseableDataBlock data = new ByteArrayDataBlock(bytes); + List centralRecords = new ArrayList<>(); + List centralRecordPositions = new ArrayList<>(); + ZipEndOfCentralDirectoryRecord eocd = ZipEndOfCentralDirectoryRecord.load(data).endOfCentralDirectoryRecord(); + long pos = eocd.offsetToStartOfCentralDirectory(); + for (int i = 0; i < eocd.totalNumberOfCentralDirectoryEntries(); i++) { + ZipCentralDirectoryFileHeaderRecord centralRecord = ZipCentralDirectoryFileHeaderRecord.load(data, pos); + centralRecords.add(centralRecord); + centralRecordPositions.add(pos); + pos += centralRecord.size(); + } + NameOffsetLookups nameOffsetLookups = new NameOffsetLookups(0, centralRecords.size()); + for (int i = 0; i < centralRecords.size(); i++) { + nameOffsetLookups.enable(i, true); + } + nameOffsetLookups.enable(0, true); + File outputFile = new File(this.tempDir, "out.jar"); + try (VirtualZipDataBlock block = new VirtualZipDataBlock(data, nameOffsetLookups, + centralRecords.toArray(ZipCentralDirectoryFileHeaderRecord[]::new), + centralRecordPositions.stream().mapToLong(Long::longValue).toArray())) { + try (FileOutputStream out = new FileOutputStream(outputFile)) { + block.asInputStream().transferTo(out); + } + } + byte[] virtualBytes = Files.readAllBytes(outputFile.toPath()); + assertThat(bytes).isEqualTo(virtualBytes); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipDataDescriptorRecordTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipDataDescriptorRecordTests.java new file mode 100644 index 000000000000..2af772eaccfc --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipDataDescriptorRecordTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ZipDataDescriptorRecord}. + * + * @author Phillip Webb + */ +class ZipDataDescriptorRecordTests { + + private static final short S0 = 0; + + @Test + void loadWhenHasSignatureLoadsData() throws Exception { + DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { // + 0x50, 0x4b, 0x07, 0x08, // + 0x01, 0x00, 0x00, 0x00, // + 0x02, 0x00, 0x00, 0x00, // + 0x03, 0x00, 0x00, 0x00 }); // + ZipDataDescriptorRecord record = ZipDataDescriptorRecord.load(dataBlock, 0); + assertThat(record.includeSignature()).isTrue(); + assertThat(record.crc32()).isEqualTo(1); + assertThat(record.compressedSize()).isEqualTo(2); + assertThat(record.uncompressedSize()).isEqualTo(3); + } + + @Test + void loadWhenHasNoSignatureLoadsData() throws Exception { + DataBlock dataBlock = new ByteArrayDataBlock(new byte[] { // + 0x01, 0x00, 0x00, 0x00, // + 0x02, 0x00, 0x00, 0x00, // + 0x03, 0x00, 0x00, 0x00 }); // + ZipDataDescriptorRecord record = ZipDataDescriptorRecord.load(dataBlock, 0); + assertThat(record.includeSignature()).isFalse(); + assertThat(record.crc32()).isEqualTo(1); + assertThat(record.compressedSize()).isEqualTo(2); + assertThat(record.uncompressedSize()).isEqualTo(3); + } + + @Test + void sizeWhenIncludeSignatureReturnsSize() { + ZipDataDescriptorRecord record = new ZipDataDescriptorRecord(true, 0, 0, 0); + assertThat(record.size()).isEqualTo(16); + } + + @Test + void sizeWhenNotIncludeSignatureReturnsSize() { + ZipDataDescriptorRecord record = new ZipDataDescriptorRecord(false, 0, 0, 0); + assertThat(record.size()).isEqualTo(12); + } + + @Test + void asByteArrayWhenIncludeSignatureReturnsByteArray() throws Exception { + byte[] bytes = new byte[] { // + 0x50, 0x4b, 0x07, 0x08, // + 0x01, 0x00, 0x00, 0x00, // + 0x02, 0x00, 0x00, 0x00, // + 0x03, 0x00, 0x00, 0x00 }; // + ZipDataDescriptorRecord record = ZipDataDescriptorRecord.load(new ByteArrayDataBlock(bytes), 0); + assertThat(record.asByteArray()).isEqualTo(bytes); + } + + @Test + void asByteArrayWhenNotIncludeSignatureReturnsByteArray() throws Exception { + byte[] bytes = new byte[] { // + 0x01, 0x00, 0x00, 0x00, // + 0x02, 0x00, 0x00, 0x00, // + 0x03, 0x00, 0x00, 0x00 }; // + ZipDataDescriptorRecord record = ZipDataDescriptorRecord.load(new ByteArrayDataBlock(bytes), 0); + assertThat(record.asByteArray()).isEqualTo(bytes); + } + + @Test + void isPresentBasedOnFlagWhenPresentReturnsTrue() { + testIsPresentBasedOnFlag((short) 0x8, true); + } + + @Test + void isPresentBasedOnFlagWhenNotPresentReturnsFalse() { + testIsPresentBasedOnFlag((short) 0x0, false); + } + + private void testIsPresentBasedOnFlag(short flag, boolean expected) { + ZipCentralDirectoryFileHeaderRecord centralRecord = new ZipCentralDirectoryFileHeaderRecord(S0, S0, flag, S0, + S0, S0, S0, S0, S0, S0, S0, S0, S0, S0, S0, S0); + ZipLocalFileHeaderRecord localRecord = new ZipLocalFileHeaderRecord(S0, flag, S0, S0, S0, S0, S0, S0, S0, S0); + assertThat(ZipDataDescriptorRecord.isPresentBasedOnFlag(flag)).isEqualTo(expected); + assertThat(ZipDataDescriptorRecord.isPresentBasedOnFlag(centralRecord)).isEqualTo(expected); + assertThat(ZipDataDescriptorRecord.isPresentBasedOnFlag(localRecord)).isEqualTo(expected); + } + +} From 4af9ed4d1d41653a89ee3a7d40db6e27d9ffd330 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 26 Oct 2023 18:58:15 -0700 Subject: [PATCH 0693/1215] Fix Tomcat TldScanner issues by returning raw zip data for nested jars Update JarUrlConnection so that the full raw zip data is returned from nested jars when no entry name is specified. This update allows Tomcat's `WarURLConnection` to work with our nested connections since they can parse the returned raw zip data. Fixes gh-38047 --- .../boot/loader/jar/NestedJarFile.java | 31 +++++++++++++++++++ .../net/protocol/jar/JarUrlConnection.java | 13 ++++++-- .../net/protocol/jar/UrlJarFileFactory.java | 8 ++--- .../protocol/jar/JarUrlConnectionTests.java | 17 ++++++++-- 4 files changed, 60 insertions(+), 9 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java index d711b9137a52..bddd274e22b9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java @@ -17,6 +17,7 @@ package org.springframework.boot.loader.jar; import java.io.File; +import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; @@ -143,6 +144,13 @@ public NestedJarFile(File file, String nestedEntryName, Runtime.Version version) this.version = (version != null) ? version.feature() : baseVersion().feature(); } + public InputStream getRawZipDataInputStream() throws IOException { + RawZipDataInputStream inputStream = new RawZipDataInputStream( + this.resources.zipContent().openRawZipData().asInputStream()); + this.resources.addInputStream(inputStream); + return inputStream; + } + @Override public Manifest getManifest() throws IOException { try { @@ -799,4 +807,27 @@ public void close() throws IOException { } + /** + * {@link InputStream} for raw zip data. + */ + private class RawZipDataInputStream extends FilterInputStream { + + private volatile boolean closed; + + protected RawZipDataInputStream(InputStream in) { + super(in); + } + + @Override + public void close() throws IOException { + if (this.closed) { + return; + } + this.closed = true; + super.close(); + NestedJarFile.this.resources.removeInputStream(this); + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java index dc51bfe4ebc5..c9a7475a1f44 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java @@ -175,11 +175,12 @@ public InputStream getInputStream() throws IOException { if (this.notFound != null) { throwFileNotFound(); } - if (this.entryName == null) { + URL jarFileURL = getJarFileURL(); + if (this.entryName == null && !UrlJarFileFactory.isNestedUrl(jarFileURL)) { throw new IOException("no entry name specified"); } - if (!getUseCaches() && Optimizations.isEnabled(false)) { - JarFile cached = jarFiles.getCached(getJarFileURL()); + if (!getUseCaches() && Optimizations.isEnabled(false) && this.entryName != null) { + JarFile cached = jarFiles.getCached(jarFileURL); if (cached != null) { if (cached.getEntry(this.entryName) != null) { return emptyInputStream; @@ -188,6 +189,12 @@ public InputStream getInputStream() throws IOException { } connect(); if (this.jarEntry == null) { + if (this.jarFile instanceof NestedJarFile nestedJarFile) { + // In order to work with Tomcat's TLD scanning and WarURLConnection we + // return the raw zip data rather than failing because there is no entry. + // See gh-38047 for details. + return nestedJarFile.getRawZipDataInputStream(); + } throwFileNotFound(); } return new ConnectionInputStream(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java index 35ba9283ca94..b35bc2435c45 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java @@ -80,10 +80,6 @@ private JarFile createJarFileForLocalFile(URL url, Runtime.Version version, Cons return new UrlJarFile(new File(path), version, closeAction); } - private boolean isNestedUrl(URL url) { - return url.getProtocol().equalsIgnoreCase("nested"); - } - private JarFile createJarFileForNested(URL url, Runtime.Version version, Consumer closeAction) throws IOException { NestedLocation location = NestedLocation.fromUrl(url); @@ -120,4 +116,8 @@ private void deleteIfPossible(Path local, Throwable cause) { } } + static boolean isNestedUrl(URL url) { + return url.getProtocol().equalsIgnoreCase("nested"); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java index 5d7ccf616b5a..d2445b48e99d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java @@ -22,6 +22,7 @@ import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.net.URL; import java.net.URLConnection; import java.nio.charset.StandardCharsets; @@ -237,8 +238,8 @@ void getPermissionReturnJarConnectionPermission() throws IOException { } @Test - void getInputStreamWhenHasNoEntryThrowsException() throws Exception { - JarUrlConnection connection = JarUrlConnection.open(this.url); + void getInputStreamWhenNotNestedAndHasNoEntryThrowsException() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file)); assertThatIOException().isThrownBy(() -> connection.getInputStream()).withMessage("no entry name specified"); } @@ -271,6 +272,18 @@ void getInputStreamWhenNoEntryAndNotOptimzedThrowsException() throws Exception { .withMessageContaining("JAR entry missing.dat not found in"); } + @Test // gh-38047 + void getInputStreamWhenNoEntryAndNestedReturnsFullJarInputStream() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar")); + File outFile = new File(this.temp, "out.zip"); + try (OutputStream out = new FileOutputStream(outFile)) { + connection.getInputStream().transferTo(out); + } + try (JarFile outJar = new JarFile(outFile)) { + assertThat(outJar.getEntry("3.dat")).isNotNull(); + } + } + @Test void getInputStreamReturnsInputStream() throws IOException { JarUrlConnection connection = JarUrlConnection.open(JarUrl.create(this.file, "nested.jar", "3.dat")); From beb49e19330c12c8824b39c74144f16a6e4f74fb Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 26 Oct 2023 22:26:14 -0700 Subject: [PATCH 0694/1215] Add tests for DataBlockInputStream and fix implementation oddities Fix issues with `DataBlockInputStream` including the fact that remain bytes were not tracked correctly. Also add some tests and fix a few other unusual details with the implementation. Closes gh-38066 --- .../boot/loader/zip/DataBlock.java | 3 +- .../boot/loader/zip/DataBlockInputStream.java | 38 ++--- .../loader/zip/DataBlockInputStreamTests.java | 137 ++++++++++++++++++ .../boot/loader/zip/DataBlockTests.java | 2 +- 4 files changed, 156 insertions(+), 24 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/DataBlockInputStreamTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/DataBlock.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/DataBlock.java index 7475b67173b2..b37cad6a82d1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/DataBlock.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/DataBlock.java @@ -73,8 +73,9 @@ default void readFully(ByteBuffer dst, long pos) throws IOException { /** * Return this {@link DataBlock} as an {@link InputStream}. * @return an {@link InputStream} to read the data block content + * @throws IOException on IO error */ - default InputStream asInputStream() { + default InputStream asInputStream() throws IOException { return new DataBlockInputStream(this); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/DataBlockInputStream.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/DataBlockInputStream.java index a05ae60f3e44..3f9b0275bed4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/DataBlockInputStream.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/DataBlockInputStream.java @@ -20,7 +20,6 @@ import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; -import java.util.zip.ZipException; /** * {@link InputStream} backed by a {@link DataBlock}. @@ -35,10 +34,11 @@ class DataBlockInputStream extends InputStream { private long remaining; - private volatile boolean closing; + private volatile boolean closed; - DataBlockInputStream(DataBlock dataBlock) { + DataBlockInputStream(DataBlock dataBlock) throws IOException { this.dataBlock = dataBlock; + this.remaining = dataBlock.size(); } @Override @@ -49,7 +49,6 @@ public int read() throws IOException { @Override public int read(byte[] b, int off, int len) throws IOException { - int result; ensureOpen(); ByteBuffer dst = ByteBuffer.wrap(b, off, len); int count = this.dataBlock.read(dst, this.pos); @@ -57,23 +56,15 @@ public int read(byte[] b, int off, int len) throws IOException { this.pos += count; this.remaining -= count; } - result = count; - if (this.remaining == 0) { - close(); - } - return result; + return count; } @Override public long skip(long n) throws IOException { - long result; - result = (n > 0) ? maxForwardSkip(n) : maxBackwardSkip(n); - this.pos += result; - this.remaining -= result; - if (this.remaining == 0) { - close(); - } - return result; + long count = (n > 0) ? maxForwardSkip(n) : maxBackwardSkip(n); + this.pos += count; + this.remaining -= count; + return count; } private long maxForwardSkip(long n) { @@ -87,21 +78,24 @@ private long maxBackwardSkip(long n) { @Override public int available() { + if (this.closed) { + return 0; + } return (this.remaining < Integer.MAX_VALUE) ? (int) this.remaining : Integer.MAX_VALUE; } - private void ensureOpen() throws ZipException { - if (this.closing) { - throw new ZipException("InputStream closed"); + private void ensureOpen() throws IOException { + if (this.closed) { + throw new IOException("InputStream closed"); } } @Override public void close() throws IOException { - if (this.closing) { + if (this.closed) { return; } - this.closing = true; + this.closed = true; if (this.dataBlock instanceof Closeable closeable) { closeable.close(); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/DataBlockInputStreamTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/DataBlockInputStreamTests.java new file mode 100644 index 000000000000..2bfcf4011ce1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/DataBlockInputStreamTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import java.io.IOException; +import java.io.InputStream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIOException; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; + +/** + * Tests for {@link DataBlockInputStream}. + * + * @author Phillip Webb + */ +class DataBlockInputStreamTests { + + private ByteArrayDataBlock dataBlock; + + private InputStream inputStream; + + @BeforeEach + void setup() throws Exception { + this.dataBlock = new ByteArrayDataBlock(new byte[] { 0, 1, 2 }); + this.inputStream = this.dataBlock.asInputStream(); + } + + @Test + void readSingleByteReadsByte() throws Exception { + assertThat(this.inputStream.read()).isEqualTo(0); + assertThat(this.inputStream.read()).isEqualTo(1); + assertThat(this.inputStream.read()).isEqualTo(2); + assertThat(this.inputStream.read()).isEqualTo(-1); + } + + @Test + void readByteArrayWhenNotOpenThrowsException() throws Exception { + byte[] bytes = new byte[10]; + this.inputStream.close(); + assertThatIOException().isThrownBy(() -> this.inputStream.read(bytes)).withMessage("InputStream closed"); + } + + @Test + void readByteArrayWhenReadingMultipleTimesReadsBytes() throws Exception { + byte[] bytes = new byte[3]; + assertThat(this.inputStream.read(bytes, 0, 2)).isEqualTo(2); + assertThat(this.inputStream.read(bytes, 2, 1)).isEqualTo(1); + assertThat(bytes).containsExactly(0, 1, 2); + } + + @Test + void readByteArrayWhenReadingMoreThanAvailableReadsRemainingBytes() throws Exception { + byte[] bytes = new byte[5]; + assertThat(this.inputStream.read(bytes, 0, 5)).isEqualTo(3); + assertThat(bytes).containsExactly(0, 1, 2, 0, 0); + } + + @Test + void skipSkipsBytes() throws Exception { + assertThat(this.inputStream.skip(2)).isEqualTo(2); + assertThat(this.inputStream.read()).isEqualTo(2); + assertThat(this.inputStream.read()).isEqualTo(-1); + } + + @Test + void skipWhenSkippingMoreThanRemainingSkipsBytes() throws Exception { + assertThat(this.inputStream.skip(100)).isEqualTo(3); + assertThat(this.inputStream.read()).isEqualTo(-1); + } + + @Test + void skipBackwardsSkipsBytes() throws IOException { + assertThat(this.inputStream.skip(2)).isEqualTo(2); + assertThat(this.inputStream.skip(-1)).isEqualTo(-1); + assertThat(this.inputStream.read()).isEqualTo(1); + } + + @Test + void skipBackwardsPastBeginingSkipsBytes() throws Exception { + assertThat(this.inputStream.skip(1)).isEqualTo(1); + assertThat(this.inputStream.skip(-100)).isEqualTo(-1); + assertThat(this.inputStream.read()).isEqualTo(0); + } + + @Test + void availableReturnsRemainingBytes() throws IOException { + assertThat(this.inputStream.available()).isEqualTo(3); + this.inputStream.read(); + assertThat(this.inputStream.available()).isEqualTo(2); + this.inputStream.skip(1); + assertThat(this.inputStream.available()).isEqualTo(1); + } + + @Test + void availableWhenClosedReturnsZero() throws IOException { + this.inputStream.close(); + assertThat(this.inputStream.available()).isZero(); + } + + @Test + void closeClosesDataBlock() throws Exception { + this.dataBlock = spy(new ByteArrayDataBlock(new byte[] { 0, 1, 2 })); + this.inputStream = this.dataBlock.asInputStream(); + this.inputStream.close(); + then(this.dataBlock).should().close(); + } + + @Test + void closeMultipleTimesClosesDataBlockOnce() throws Exception { + this.dataBlock = spy(new ByteArrayDataBlock(new byte[] { 0, 1, 2 })); + this.inputStream = this.dataBlock.asInputStream(); + this.inputStream.close(); + this.inputStream.close(); + then(this.dataBlock).should(times(1)).close(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/DataBlockTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/DataBlockTests.java index f238059a3400..eed800f981b3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/DataBlockTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/DataBlockTests.java @@ -68,7 +68,7 @@ void readFullyWhenReadReturnsNegativeResultThrowsException() throws Exception { } @Test - void asInputStreamReturnsDataBlockInputStream() { + void asInputStreamReturnsDataBlockInputStream() throws Exception { DataBlock dataBlock = mock(DataBlock.class, withSettings().defaultAnswer(CALLS_REAL_METHODS)); assertThat(dataBlock.asInputStream()).isInstanceOf(DataBlockInputStream.class); } From 1b6431c219afc2f14fdebc5d7425a4bc4af77a5a Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Sun, 29 Oct 2023 19:34:39 +0900 Subject: [PATCH 0695/1215] Fix shouldStopKeepAliveThreadIfContextIsClosed() See gh-38103 --- .../org/springframework/boot/SpringApplicationTests.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java index 92eefe0e3fe8..877433d23b28 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java @@ -1425,8 +1425,9 @@ void shouldStopKeepAliveThreadIfContextIsClosed() { this.context.close(); Awaitility.await() .atMost(Duration.ofSeconds(30)) - .untilAsserted(() -> assertThat(getCurrentThreads()) - .filteredOn((thread) -> thread.getName().equals("keep-alive"))); + .untilAsserted( + () -> assertThat(getCurrentThreads()).filteredOn((thread) -> thread.getName().equals("keep-alive")) + .isEmpty()); } private ArgumentMatcher isAvailabilityChangeEventWithState( From 5765f9410cf9d3b265050fd3307bc9140b124243 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 30 Oct 2023 18:16:07 +0000 Subject: [PATCH 0696/1215] Upgrade to Liquibase 4.24.0 Closes gh-38120 --- .../liquibase/LiquibaseAutoConfigurationTests.java | 6 +++++- spring-boot-project/spring-boot-dependencies/build.gradle | 6 +----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java index 7d1f68526f4b..2e0be6c31b52 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java @@ -266,8 +266,12 @@ void overrideDropFirst() { @Test void overrideClearChecksums() { + String jdbcUrl = "jdbc:hsqldb:mem:liquibase" + UUID.randomUUID(); + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.liquibase.url:" + jdbcUrl) + .run((context) -> assertThat(context).hasNotFailed()); this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) - .withPropertyValues("spring.liquibase.clear-checksums:true") + .withPropertyValues("spring.liquibase.clear-checksums:true", "spring.liquibase.url:" + jdbcUrl) .run(assertLiquibase((liquibase) -> assertThat(liquibase.isClearCheckSums()).isTrue())); } diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 99cad246324a..2ec3c3e8624d 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -823,11 +823,7 @@ bom { ] } } - library("Liquibase", "4.23.0") { - prohibit { - versionRange "4.23.1" - because "it contains a regression (https://github.com/liquibase/liquibase/issues/4684)" - } + library("Liquibase", "4.24.0") { group("org.liquibase") { modules = [ "liquibase-cdi", From d2325d111091122478200436a8b9e9dc98eeec59 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 30 Oct 2023 18:16:12 +0000 Subject: [PATCH 0697/1215] Upgrade to Oracle Database 23.3.0.23.09 Closes gh-38121 --- .../jdbc/OracleUcpDataSourceConfigurationTests.java | 5 +++-- spring-boot-project/spring-boot-dependencies/build.gradle | 5 +---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpDataSourceConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpDataSourceConfigurationTests.java index 234374540943..971ab21d37a5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpDataSourceConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpDataSourceConfigurationTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.jdbc; import java.sql.Connection; +import java.time.Duration; import javax.sql.DataSource; @@ -82,10 +83,10 @@ void testDataSourceDefaultsPreserved() { this.contextRunner.run((context) -> { PoolDataSourceImpl ds = context.getBean(PoolDataSourceImpl.class); assertThat(ds.getInitialPoolSize()).isZero(); - assertThat(ds.getMinPoolSize()).isZero(); + assertThat(ds.getMinPoolSize()).isEqualTo(1); assertThat(ds.getMaxPoolSize()).isEqualTo(Integer.MAX_VALUE); assertThat(ds.getInactiveConnectionTimeout()).isZero(); - assertThat(ds.getConnectionWaitTimeout()).isEqualTo(3); + assertThat(ds.getConnectionWaitDuration()).isEqualTo(Duration.ofSeconds(3)); assertThat(ds.getTimeToLiveConnectionTimeout()).isZero(); assertThat(ds.getAbandonedConnectionTimeout()).isZero(); assertThat(ds.getTimeoutCheckInterval()).isEqualTo(30); diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 2ec3c3e8624d..d3fda6d3b63f 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1093,10 +1093,7 @@ bom { ] } } - library("Oracle Database", "21.9.0.0") { - prohibit { - versionRange "23.2.0.0" - } + library("Oracle Database", "23.3.0.23.09") { group("com.oracle.database.jdbc") { imports = [ "ojdbc-bom" From 9b7d8cacc36a940b6ce290f586fd76ffe85a570f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 08:34:02 +0000 Subject: [PATCH 0698/1215] Update upgrade policy for the RC phase --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d3fda6d3b63f..f2f4d981459a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -9,7 +9,7 @@ description = "Spring Boot Dependencies" bom { effectiveBomArtifact() upgrade { - policy = "any" + policy = "same-minor-version" gitHub { issueLabels = ["type: dependency-upgrade"] } From 00987feb8101310ecfabf7a6d9b88dd1837acc23 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 08:39:41 +0000 Subject: [PATCH 0699/1215] Upgrade to ActiveMQ 5.18.3 Closes gh-38126 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f2f4d981459a..9fb269512c57 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -14,7 +14,7 @@ bom { issueLabels = ["type: dependency-upgrade"] } } - library("ActiveMQ", "5.18.2") { + library("ActiveMQ", "5.18.3") { group("org.apache.activemq") { modules = [ "activemq-amqp", From 90d6b53a27fa086ad498bbb5c3d5c30d045f63fc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 08:39:46 +0000 Subject: [PATCH 0700/1215] Upgrade to Artemis 2.31.2 Closes gh-38127 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 9fb269512c57..5b21379042e3 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -65,7 +65,7 @@ bom { ] } } - library("Artemis", "2.31.0") { + library("Artemis", "2.31.2") { group("org.apache.activemq") { modules = [ "artemis-amqp-protocol", From f7ccd00d7923ab57f94c0242d60003d66693895e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 08:39:51 +0000 Subject: [PATCH 0701/1215] Upgrade to Glassfish JAXB 4.0.4 Closes gh-38128 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5b21379042e3..4ce62b509e14 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -309,7 +309,7 @@ bom { ] } } - library("Glassfish JAXB", "4.0.3") { + library("Glassfish JAXB", "4.0.4") { group("org.glassfish.jaxb") { imports = [ "jaxb-bom" From 7ee570a383b8f8b3c74587683e1b1627fc7302e2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 08:39:56 +0000 Subject: [PATCH 0702/1215] Upgrade to Hazelcast 5.3.5 Closes gh-38129 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 4ce62b509e14..5368cba0447b 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -364,7 +364,7 @@ bom { ] } } - library("Hazelcast", "5.3.2") { + library("Hazelcast", "5.3.5") { group("com.hazelcast") { modules = [ "hazelcast", From e0a50e17846cc17a2510debbd9bfa32341d9236c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 08:40:00 +0000 Subject: [PATCH 0703/1215] Upgrade to Jakarta Json 2.1.3 Closes gh-38130 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5368cba0447b..00598eedfd94 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -502,7 +502,7 @@ bom { ] } } - library("Jakarta Json", "2.1.2") { + library("Jakarta Json", "2.1.3") { group("jakarta.json") { modules = [ "jakarta.json-api" From fd1fdafe31fa31dfbe369c06a6886df1190672ac Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 08:40:05 +0000 Subject: [PATCH 0704/1215] Upgrade to Jakarta XML SOAP 3.0.1 Closes gh-38131 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 00598eedfd94..ce049e9b43cc 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -587,7 +587,7 @@ bom { ] } } - library("Jakarta XML SOAP", "3.0.0") { + library("Jakarta XML SOAP", "3.0.1") { group("jakarta.xml.soap") { modules = [ "jakarta.xml.soap-api" From 7ee35a8378bf35dff2156f796da5ff8c92d4b4ef Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 08:40:10 +0000 Subject: [PATCH 0705/1215] Upgrade to Jakarta XML WS 4.0.1 Closes gh-38132 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index ce049e9b43cc..4fedd61a0292 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -594,7 +594,7 @@ bom { ] } } - library("Jakarta XML WS", "4.0.0") { + library("Jakarta XML WS", "4.0.1") { group("jakarta.xml.ws") { modules = [ "jakarta.xml.ws-api" From 6796be09394e77a2499b949d92ea417d18eb2e9d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 08:40:15 +0000 Subject: [PATCH 0706/1215] Upgrade to Jedis 5.0.2 Closes gh-38133 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 4fedd61a0292..ee6c0f7ebae1 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -652,7 +652,7 @@ bom { ] } } - library("Jedis", "5.0.1") { + library("Jedis", "5.0.2") { group("redis.clients") { modules = [ "jedis" From 72450bfe95528a8a48edafc71a0a6fea57a4b2d7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 08:40:20 +0000 Subject: [PATCH 0707/1215] Upgrade to Jetty Reactive HTTPClient 4.0.1 Closes gh-38134 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index ee6c0f7ebae1..634eda55ca56 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -666,7 +666,7 @@ bom { ] } } - library("Jetty Reactive HTTPClient", "4.0.0") { + library("Jetty Reactive HTTPClient", "4.0.1") { group("org.eclipse.jetty") { modules = [ "jetty-reactive-httpclient" From f699e9b77da071f74ae4b8d1bdd2c4738d66f29d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 08:40:24 +0000 Subject: [PATCH 0708/1215] Upgrade to Jetty 12.0.3 Closes gh-38135 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 634eda55ca56..7aeb11c8b42b 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -673,7 +673,7 @@ bom { ] } } - library("Jetty", "12.0.2") { + library("Jetty", "12.0.3") { group("org.eclipse.jetty.ee10") { imports = [ "jetty-ee10-bom" From 20a6b7a9bc026a9b62235fd6b3ed4d08a790894e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 08:40:29 +0000 Subject: [PATCH 0709/1215] Upgrade to Kotlin 1.9.20 Closes gh-38136 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9d3858acc5a1..443565e4f461 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ commonsCodecVersion=1.16.0 hamcrestVersion=2.2 jacksonVersion=2.15.3 junitJupiterVersion=5.10.0 -kotlinVersion=1.9.20-RC +kotlinVersion=1.9.20 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.27 springFrameworkVersion=6.1.0-SNAPSHOT From 58be0ddf36cc4c2313f4ab94a7a038776210e1c5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 08:40:34 +0000 Subject: [PATCH 0710/1215] Upgrade to Log4j2 2.21.1 Closes gh-38137 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 7aeb11c8b42b..9fa74c8bc11b 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -834,7 +834,7 @@ bom { ] } } - library("Log4j2", "2.21.0") { + library("Log4j2", "2.21.1") { group("org.apache.logging.log4j") { imports = [ "log4j-bom" From 37a8fc320622df51e0daa3bff90175556ef26c1c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 08:40:38 +0000 Subject: [PATCH 0711/1215] Upgrade to Maven Clean Plugin 3.3.2 Closes gh-38138 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 9fa74c8bc11b..3f9428bb1d04 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -878,7 +878,7 @@ bom { ] } } - library("Maven Clean Plugin", "3.3.1") { + library("Maven Clean Plugin", "3.3.2") { group("org.apache.maven.plugins") { plugins = [ "maven-clean-plugin" From 053edc04a563dc66493f87c7654f44d3c5b2527b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 08:40:43 +0000 Subject: [PATCH 0712/1215] Upgrade to Maven Dependency Plugin 3.6.1 Closes gh-38139 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 3f9428bb1d04..138e3337377a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -892,7 +892,7 @@ bom { ] } } - library("Maven Dependency Plugin", "3.6.0") { + library("Maven Dependency Plugin", "3.6.1") { group("org.apache.maven.plugins") { plugins = [ "maven-dependency-plugin" From 719545d9ab3422ffa8a4d86131dca898d2496449 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 08:40:48 +0000 Subject: [PATCH 0713/1215] Upgrade to MSSQL JDBC 12.4.2.jre11 Closes gh-38140 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 138e3337377a..da0884aa4459 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1031,7 +1031,7 @@ bom { ] } } - library("MSSQL JDBC", "12.4.1.jre11") { + library("MSSQL JDBC", "12.4.2.jre11") { prohibit { endsWith([".jre8", "-preview"]) because "we use the non-preview .jre11 version" From 2d67957cbba9c1a49334c5ead008f46b64a2b83b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 08:40:52 +0000 Subject: [PATCH 0714/1215] Upgrade to Native Build Tools Plugin 0.9.28 Closes gh-38141 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 443565e4f461..fcd518d75261 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ jacksonVersion=2.15.3 junitJupiterVersion=5.10.0 kotlinVersion=1.9.20 mavenVersion=3.9.4 -nativeBuildToolsVersion=0.9.27 +nativeBuildToolsVersion=0.9.28 springFrameworkVersion=6.1.0-SNAPSHOT tomcatVersion=10.1.15 From 8a2c0f18acdbf519c13946bfcede78f87d458c92 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 08:40:57 +0000 Subject: [PATCH 0715/1215] Upgrade to Pulsar 3.1.1 Closes gh-38142 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index da0884aa4459..09b24641466c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1128,7 +1128,7 @@ bom { ] } } - library("Pulsar", "3.1.0") { + library("Pulsar", "3.1.1") { group("org.apache.pulsar") { modules = [ "bouncy-castle-bc", From 8a925b68582150706a3409445797cf89c76c7650 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 08:41:02 +0000 Subject: [PATCH 0716/1215] Upgrade to SAAJ Impl 3.0.3 Closes gh-38143 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 09b24641466c..2c5533e5eac7 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1434,7 +1434,7 @@ bom { ] } } - library("SAAJ Impl", "3.0.2") { + library("SAAJ Impl", "3.0.3") { group("com.sun.xml.messaging.saaj") { modules = [ "saaj-impl" From e0169e7cf1695ce5e5496feba01a1351831ff5f4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 08:41:02 +0000 Subject: [PATCH 0717/1215] Upgrade to Spring Framework 6.1.0-RC2 Closes gh-37995 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index fcd518d75261..4bcfd9ba55db 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ junitJupiterVersion=5.10.0 kotlinVersion=1.9.20 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.28 -springFrameworkVersion=6.1.0-SNAPSHOT +springFrameworkVersion=6.1.0-RC2 tomcatVersion=10.1.15 kotlin.stdlib.default.dependency=false From 1ef66d2e39b68e9c50daeff57d49779e817b0522 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 08:41:07 +0000 Subject: [PATCH 0718/1215] Upgrade to Spring WS 4.0.7 Closes gh-38144 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 2c5533e5eac7..562f347cc992 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1619,7 +1619,7 @@ bom { ] } } - library("Spring WS", "4.0.6") { + library("Spring WS", "4.0.7") { considerSnapshots() group("org.springframework.ws") { imports = [ From 890a3e72ac0d38f5bc6dde2a120d9bc6260db1af Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 18:29:08 +0000 Subject: [PATCH 0719/1215] Repair file channel when it's closed by interruption When an interrupted that calls FileChannel.read, the channel is closed and the read fails with a ClosedByInterruptException. The closure of the channel makes it unusable by other threads. To allow other threads to read from the data block, this commit recreates the FileChannel when a read fails on an interrupted thread with a ClosedByInterruptException. The exception is then rethrown to continue the thread's interruption. Closes gh-38154 --- .../boot/loader/zip/FileChannelDataBlock.java | 21 ++++++++++++++-- .../loader/zip/FileChannelDataBlockTests.java | 24 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/FileChannelDataBlock.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/FileChannelDataBlock.java index 1824281ede34..0346a87d4d0e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/FileChannelDataBlock.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/FileChannelDataBlock.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.channels.ClosedByInterruptException; import java.nio.channels.ClosedChannelException; import java.nio.channels.FileChannel; import java.nio.file.Files; @@ -179,7 +180,13 @@ int read(ByteBuffer dst, long position) throws IOException { synchronized (this.lock) { if (position < this.bufferPosition || position >= this.bufferPosition + this.bufferSize) { this.buffer.clear(); - this.bufferSize = this.fileChannel.read(this.buffer, position); + try { + this.bufferSize = this.fileChannel.read(this.buffer, position); + } + catch (ClosedByInterruptException ex) { + repairFileChannel(); + throw ex; + } this.bufferPosition = position; } if (this.bufferSize <= 0) { @@ -193,6 +200,16 @@ int read(ByteBuffer dst, long position) throws IOException { } } + private void repairFileChannel() throws IOException { + if (tracker != null) { + tracker.closedFileChannel(this.path, this.fileChannel); + } + this.fileChannel = FileChannel.open(this.path, StandardOpenOption.READ); + if (tracker != null) { + tracker.openedFileChannel(this.path, this.fileChannel); + } + } + void open() throws IOException { synchronized (this.lock) { if (this.referenceCount == 0) { @@ -231,7 +248,7 @@ void close() throws IOException { void ensureOpen(Supplier exceptionSupplier) throws E { synchronized (this.lock) { - if (this.referenceCount == 0) { + if (this.referenceCount == 0 || !this.fileChannel.isOpen()) { throw exceptionSupplier.get(); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockTests.java index 9beb4aa314d0..df015ff68d55 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockTests.java @@ -19,9 +19,11 @@ import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.channels.ClosedByInterruptException; import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.Path; +import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -74,6 +76,28 @@ void readReadsFile() throws IOException { } } + @Test + void readReadsFileWhenAnotherThreadHasBeenInterrupted() throws IOException, InterruptedException { + try (FileChannelDataBlock block = createAndOpenBlock()) { + ByteBuffer buffer = ByteBuffer.allocate(CONTENT.length); + AtomicReference failure = new AtomicReference<>(); + Thread thread = new Thread(() -> { + Thread.currentThread().interrupt(); + try { + block.read(ByteBuffer.allocate(CONTENT.length), 0); + } + catch (IOException ex) { + failure.set(ex); + } + }); + thread.start(); + thread.join(); + assertThat(failure.get()).isInstanceOf(ClosedByInterruptException.class); + assertThat(block.read(buffer, 0)).isEqualTo(6); + assertThat(buffer.array()).containsExactly(CONTENT); + } + } + @Test void readDoesNotReadPastEndOfFile() throws IOException { try (FileChannelDataBlock block = createAndOpenBlock()) { From 8bf847e5490fab4571f6ea5ff33670d25ed2f3b2 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 27 Oct 2023 22:51:36 -0700 Subject: [PATCH 0720/1215] Rename keyAlias parameter to alias Rename the keyAlais parameter to alias since it may be used as either the key alias or the certificate alias. Also clarify the javadoc for keyPassword. Closes gh-38099 --- .../boot/ssl/pem/PemSslStoreBundle.java | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java index c1db6b4d9ae2..becea0dbf169 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java @@ -41,7 +41,7 @@ */ public class PemSslStoreBundle implements SslStoreBundle { - private static final String DEFAULT_KEY_ALIAS = "ssl"; + private static final String DEFAULT_ALIAS = "ssl"; private final KeyStore keyStore; @@ -60,40 +60,39 @@ public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails * Create a new {@link PemSslStoreBundle} instance. * @param keyStoreDetails the key store details * @param trustStoreDetails the trust store details - * @param keyAlias the key alias to use or {@code null} to use a default alias + * @param alias the alias to use or {@code null} to use a default alias */ - public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, - String keyAlias) { - this(keyStoreDetails, trustStoreDetails, keyAlias, null); + public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String alias) { + this(keyStoreDetails, trustStoreDetails, alias, null); } /** * Create a new {@link PemSslStoreBundle} instance. * @param keyStoreDetails the key store details * @param trustStoreDetails the trust store details - * @param keyAlias the key alias to use or {@code null} to use a default alias - * @param keyPassword the password to use for the key + * @param alias the alias to use or {@code null} to use a default alias + * @param keyPassword the password to protect the key (if one is added) * @since 3.2.0 */ - public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String keyAlias, + public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String alias, String keyPassword) { - this(keyStoreDetails, trustStoreDetails, keyAlias, keyPassword, false); + this(keyStoreDetails, trustStoreDetails, alias, keyPassword, false); } /** * Create a new {@link PemSslStoreBundle} instance. * @param keyStoreDetails the key store details * @param trustStoreDetails the trust store details - * @param keyAlias the key alias to use or {@code null} to use a default alias - * @param keyPassword the password to use for the key + * @param alias the key alias to use or {@code null} to use a default alias + * @param keyPassword the password to protect the key (if one is added) * @param verifyKeys whether to verify that the private key matches the public key * @since 3.2.0 */ - public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String keyAlias, + public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String alias, String keyPassword, boolean verifyKeys) { - this.keyStore = createKeyStore("key", keyStoreDetails, (keyAlias != null) ? keyAlias : DEFAULT_KEY_ALIAS, - keyPassword, verifyKeys); - this.trustStore = createKeyStore("trust", trustStoreDetails, (keyAlias != null) ? keyAlias : DEFAULT_KEY_ALIAS, + this.keyStore = createKeyStore("key", keyStoreDetails, (alias != null) ? alias : DEFAULT_ALIAS, keyPassword, + verifyKeys); + this.trustStore = createKeyStore("trust", trustStoreDetails, (alias != null) ? alias : DEFAULT_ALIAS, keyPassword, verifyKeys); } @@ -112,7 +111,7 @@ public KeyStore getTrustStore() { return this.trustStore; } - private static KeyStore createKeyStore(String name, PemSslStoreDetails details, String keyAlias, String keyPassword, + private static KeyStore createKeyStore(String name, PemSslStoreDetails details, String alias, String keyPassword, boolean verifyKeys) { if (details == null || details.isEmpty()) { return null; @@ -126,10 +125,10 @@ private static KeyStore createKeyStore(String name, PemSslStoreDetails details, if (verifyKeys) { verifyKeys(privateKey, certificates); } - addPrivateKey(store, privateKey, keyAlias, keyPassword, certificates); + addPrivateKey(store, privateKey, alias, keyPassword, certificates); } else { - addCertificates(store, certificates, keyAlias); + addCertificates(store, certificates, alias); } return store; } From 2c6fca8df7ec67e1e7dd44c517a68cebbb261cc1 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 30 Oct 2023 16:04:58 -0700 Subject: [PATCH 0721/1215] Allow alias and password to be configured on a per PEM store basis Closes gh-38124 --- .../ssl/PropertiesSslBundle.java | 9 ++- .../boot/ssl/pem/PemSslStoreBundle.java | 42 ++++------ .../boot/ssl/pem/PemSslStoreDetails.java | 80 +++++++++++++++++-- .../boot/web/server/WebServerSslBundle.java | 8 +- .../boot/ssl/pem/PemSslStoreBundleTests.java | 31 ++++--- 5 files changed, 121 insertions(+), 49 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java index a1c8e9522f95..96188861a123 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java @@ -107,10 +107,11 @@ public static SslBundle get(JksSslBundleProperties properties) { } private static SslStoreBundle asSslStoreBundle(PemSslBundleProperties properties) { - PemSslStoreDetails keyStoreDetails = asStoreDetails(properties.getKeystore()); - PemSslStoreDetails trustStoreDetails = asStoreDetails(properties.getTruststore()); - return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, properties.getKey().getAlias(), null, - properties.isVerifyKeys()); + PemSslStoreDetails keyStoreDetails = asStoreDetails(properties.getKeystore()) + .withAlias(properties.getKey().getAlias()); + PemSslStoreDetails trustStoreDetails = asStoreDetails(properties.getTruststore()) + .withAlias(properties.getKey().getAlias()); + return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, properties.isVerifyKeys()); } private static PemSslStoreDetails asStoreDetails(PemSslBundleProperties.Store properties) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java index becea0dbf169..0346b5395bd1 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java @@ -26,7 +26,6 @@ import java.util.List; import org.springframework.boot.ssl.SslStoreBundle; -import org.springframework.boot.ssl.pem.KeyVerifier.Result; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -61,39 +60,31 @@ public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails * @param keyStoreDetails the key store details * @param trustStoreDetails the trust store details * @param alias the alias to use or {@code null} to use a default alias + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of + * {@link PemSslStoreDetails#alias()} in the {@code keyStoreDetails} and + * {@code trustStoreDetails} */ + @Deprecated(since = "3.2.0", forRemoval = true) public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String alias) { - this(keyStoreDetails, trustStoreDetails, alias, null); + this(keyStoreDetails, trustStoreDetails, alias, false); } /** * Create a new {@link PemSslStoreBundle} instance. * @param keyStoreDetails the key store details * @param trustStoreDetails the trust store details - * @param alias the alias to use or {@code null} to use a default alias - * @param keyPassword the password to protect the key (if one is added) + * @param verifyKeys whether to verify that the private key matches the public key * @since 3.2.0 */ - public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String alias, - String keyPassword) { - this(keyStoreDetails, trustStoreDetails, alias, keyPassword, false); + public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, + boolean verifyKeys) { + this(keyStoreDetails, trustStoreDetails, null, verifyKeys); } - /** - * Create a new {@link PemSslStoreBundle} instance. - * @param keyStoreDetails the key store details - * @param trustStoreDetails the trust store details - * @param alias the key alias to use or {@code null} to use a default alias - * @param keyPassword the password to protect the key (if one is added) - * @param verifyKeys whether to verify that the private key matches the public key - * @since 3.2.0 - */ - public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String alias, - String keyPassword, boolean verifyKeys) { - this.keyStore = createKeyStore("key", keyStoreDetails, (alias != null) ? alias : DEFAULT_ALIAS, keyPassword, - verifyKeys); - this.trustStore = createKeyStore("trust", trustStoreDetails, (alias != null) ? alias : DEFAULT_ALIAS, - keyPassword, verifyKeys); + private PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String alias, + boolean verifyKeys) { + this.keyStore = createKeyStore("key", keyStoreDetails, alias, verifyKeys); + this.trustStore = createKeyStore("trust", trustStoreDetails, alias, verifyKeys); } @Override @@ -111,13 +102,14 @@ public KeyStore getTrustStore() { return this.trustStore; } - private static KeyStore createKeyStore(String name, PemSslStoreDetails details, String alias, String keyPassword, - boolean verifyKeys) { + private static KeyStore createKeyStore(String name, PemSslStoreDetails details, String alias, boolean verifyKeys) { if (details == null || details.isEmpty()) { return null; } try { Assert.notNull(details.certificate(), "Certificate content must not be null"); + alias = (details.alias() != null) ? details.alias() : alias; + alias = (alias != null) ? alias : DEFAULT_ALIAS; KeyStore store = createKeyStore(details); X509Certificate[] certificates = loadCertificates(details); PrivateKey privateKey = loadPrivateKey(details); @@ -125,7 +117,7 @@ private static KeyStore createKeyStore(String name, PemSslStoreDetails details, if (verifyKeys) { verifyKeys(privateKey, certificates); } - addPrivateKey(store, privateKey, alias, keyPassword, certificates); + addPrivateKey(store, privateKey, alias, details.password(), certificates); } else { addCertificates(store, certificates, alias); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java index 81d68eb69594..cdc580f4ce44 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java @@ -26,6 +26,10 @@ * * @param type the key store type, for example {@code JKS} or {@code PKCS11}. A * {@code null} value will use {@link KeyStore#getDefaultType()}). + * @param alias the alias used when setting entries in the {@link KeyStore} + * @param password the password used + * {@link KeyStore#setKeyEntry(String, java.security.Key, char[], java.security.cert.Certificate[]) + * setting key entries} in the {@link KeyStore} * @param certificate the certificate content (either the PEM content itself or something * that can be loaded by {@link ResourceUtils#getURL}) * @param privateKey the private key content (either the PEM content itself or something @@ -35,28 +39,94 @@ * @author Phillip Webb * @since 3.1.0 */ -public record PemSslStoreDetails(String type, String certificate, String privateKey, String privateKeyPassword) { +public record PemSslStoreDetails(String type, String alias, String password, String certificate, String privateKey, + String privateKeyPassword) { + /** + * Create a new {@link PemSslStoreDetails} instance. + * @param type the key store type, for example {@code JKS} or {@code PKCS11}. A + * {@code null} value will use {@link KeyStore#getDefaultType()}). + * @param alias the alias used when setting entries in the {@link KeyStore} + * @param password the password used + * {@link KeyStore#setKeyEntry(String, java.security.Key, char[], java.security.cert.Certificate[]) + * setting key entries} in the {@link KeyStore} + * @param certificate the certificate content (either the PEM content itself or + * something that can be loaded by {@link ResourceUtils#getURL}) + * @param privateKey the private key content (either the PEM content itself or + * something that can be loaded by {@link ResourceUtils#getURL}) + * @param privateKeyPassword a password used to decrypt an encrypted private key + * @since 3.2.0 + */ + public PemSslStoreDetails { + } + + /** + * Create a new {@link PemSslStoreDetails} instance. + * @param type the key store type, for example {@code JKS} or {@code PKCS11}. A + * {@code null} value will use {@link KeyStore#getDefaultType()}). + * @param certificate the certificate content (either the PEM content itself or + * something that can be loaded by {@link ResourceUtils#getURL}) + * @param privateKey the private key content (either the PEM content itself or + * something that can be loaded by {@link ResourceUtils#getURL}) + * @param privateKeyPassword a password used to decrypt an encrypted private key + */ + public PemSslStoreDetails(String type, String certificate, String privateKey, String privateKeyPassword) { + this(type, null, null, certificate, privateKey, null); + } + + /** + * Create a new {@link PemSslStoreDetails} instance. + * @param type the key store type, for example {@code JKS} or {@code PKCS11}. A + * {@code null} value will use {@link KeyStore#getDefaultType()}). + * @param certificate the certificate content (either the PEM content itself or + * something that can be loaded by {@link ResourceUtils#getURL}) + * @param privateKey the private key content (either the PEM content itself or + * something that can be loaded by {@link ResourceUtils#getURL}) + */ public PemSslStoreDetails(String type, String certificate, String privateKey) { this(type, certificate, privateKey, null); } + /** + * Return a new {@link PemSslStoreDetails} instance with a new alias. + * @param alias the new alias + * @return a new {@link PemSslStoreDetails} instance + * @since 3.2.0 + */ + public PemSslStoreDetails withAlias(String alias) { + return new PemSslStoreDetails(this.type, alias, this.password, this.certificate, this.privateKey, + this.privateKeyPassword); + } + + /** + * Return a new {@link PemSslStoreDetails} instance with a new password. + * @param password the new password + * @return a new {@link PemSslStoreDetails} instance + * @since 3.2.0 + */ + public PemSslStoreDetails withPassword(String password) { + return new PemSslStoreDetails(this.type, this.alias, password, this.certificate, this.privateKey, + this.privateKeyPassword); + } + /** * Return a new {@link PemSslStoreDetails} instance with a new private key. * @param privateKey the new private key * @return a new {@link PemSslStoreDetails} instance */ public PemSslStoreDetails withPrivateKey(String privateKey) { - return new PemSslStoreDetails(this.type, this.certificate, privateKey, this.privateKeyPassword); + return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificate, privateKey, + this.privateKeyPassword); } /** * Return a new {@link PemSslStoreDetails} instance with a new private key password. - * @param password the new private key password + * @param privateKeyPassword the new private key password * @return a new {@link PemSslStoreDetails} instance */ - public PemSslStoreDetails withPrivateKeyPassword(String password) { - return new PemSslStoreDetails(this.type, this.certificate, this.privateKey, password); + public PemSslStoreDetails withPrivateKeyPassword(String privateKeyPassword) { + return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificate, this.privateKey, + privateKeyPassword); } boolean isEmpty() { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java index e722830aa498..c9f989bfae02 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java @@ -62,10 +62,12 @@ private WebServerSslBundle(SslStoreBundle stores, String keyPassword, Ssl ssl) { private static SslStoreBundle createPemStoreBundle(Ssl ssl) { PemSslStoreDetails keyStoreDetails = new PemSslStoreDetails(ssl.getKeyStoreType(), ssl.getCertificate(), - ssl.getCertificatePrivateKey()); + ssl.getCertificatePrivateKey()) + .withAlias(ssl.getKeyAlias()); PemSslStoreDetails trustStoreDetails = new PemSslStoreDetails(ssl.getTrustStoreType(), - ssl.getTrustCertificate(), ssl.getTrustCertificatePrivateKey()); - return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, ssl.getKeyAlias()); + ssl.getTrustCertificate(), ssl.getTrustCertificatePrivateKey()) + .withAlias(ssl.getKeyAlias()); + return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails); } private static SslStoreBundle createJksStoreBundle(Ssl ssl) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java index 84c0f408cd6f..6414382683b8 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java @@ -166,6 +166,7 @@ void whenHasEmbeddedKeyStoreDetailsAndTrustStoreDetails() { } @Test + @SuppressWarnings("removal") void whenHasKeyStoreDetailsAndTrustStoreDetailsAndAlias() { PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem") .withPrivateKey("classpath:test-key.pem"); @@ -190,21 +191,26 @@ void whenHasStoreType() { @Test void whenHasKeyStoreDetailsAndTrustStoreDetailsAndKeyPassword() { PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem") - .withPrivateKey("classpath:test-key.pem"); + .withPrivateKey("classpath:test-key.pem") + .withAlias("ksa") + .withPassword("kss"); PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem") - .withPrivateKey("classpath:test-key.pem"); - PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, "test-alias", "keysecret"); - assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias", "keysecret".toCharArray())); - assertThat(bundle.getTrustStore()) - .satisfies(storeContainingCertAndKey("test-alias", "keysecret".toCharArray())); + .withPrivateKey("classpath:test-key.pem") + .withAlias("tsa") + .withPassword("tss"); + PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails); + assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("ksa", "kss".toCharArray())); + assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("tsa", "tss".toCharArray())); } @Test void shouldVerifyKeysIfEnabled() { PemSslStoreDetails keyStoreDetails = PemSslStoreDetails .forCertificate("classpath:org/springframework/boot/ssl/pem/key1.crt") - .withPrivateKey("classpath:org/springframework/boot/ssl/pem/key1.pem"); - PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, null, "test-alias", "keysecret", true); + .withPrivateKey("classpath:org/springframework/boot/ssl/pem/key1.pem") + .withAlias("test-alias") + .withPassword("keysecret"); + PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, null, true); assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias", "keysecret".toCharArray())); } @@ -212,8 +218,10 @@ void shouldVerifyKeysIfEnabled() { void shouldVerifyKeysIfEnabledAndCertificateChainIsUsed() { PemSslStoreDetails keyStoreDetails = PemSslStoreDetails .forCertificate("classpath:org/springframework/boot/ssl/pem/key2-chain.crt") - .withPrivateKey("classpath:org/springframework/boot/ssl/pem/key2.pem"); - PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, null, "test-alias", "keysecret", true); + .withPrivateKey("classpath:org/springframework/boot/ssl/pem/key2.pem") + .withAlias("test-alias") + .withPassword("keysecret"); + PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, null, true); assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias", "keysecret".toCharArray())); } @@ -222,8 +230,7 @@ void shouldFailIfVerifyKeysIsEnabledAndKeysDontMatch() { PemSslStoreDetails keyStoreDetails = PemSslStoreDetails .forCertificate("classpath:org/springframework/boot/ssl/pem/key2.crt") .withPrivateKey("classpath:org/springframework/boot/ssl/pem/key1.pem"); - assertThatIllegalStateException() - .isThrownBy(() -> new PemSslStoreBundle(keyStoreDetails, null, null, null, true)) + assertThatIllegalStateException().isThrownBy(() -> new PemSslStoreBundle(keyStoreDetails, null, true)) .withMessageContaining("Private key matches none of the certificates"); } From 2b39ec6f602787d684343bc204e99255856f56a8 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 27 Oct 2023 17:09:30 -0700 Subject: [PATCH 0722/1215] Introduce a public `PemContent` class Update `PemContent` so that it now holds PEM data and is public. This update is required so that in the future we can make use of our PEM parsing code in spring-boot-autoconfigure. Closes gh-38174 --- .../boot/ssl/pem/PemCertificateParser.java | 4 + .../boot/ssl/pem/PemContent.java | 110 +++++++++++++++--- .../boot/ssl/pem/PemPrivateKeyParser.java | 2 +- .../boot/ssl/pem/PemSslStoreBundle.java | 6 +- .../boot/ssl/pem/PemContentTests.java | 99 +++++++++++++++- .../ssl/pem/PemPrivateKeyParserTests.java | 19 +-- 6 files changed, 199 insertions(+), 41 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemCertificateParser.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemCertificateParser.java index 58cf1ac4447d..8e07b0740bc3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemCertificateParser.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemCertificateParser.java @@ -27,6 +27,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + /** * Parser for X.509 certificates in PEM format. * @@ -58,6 +61,7 @@ static List parse(String text) { CertificateFactory factory = getCertificateFactory(); List certs = new ArrayList<>(); readCertificates(text, factory, certs::add); + Assert.state(!CollectionUtils.isEmpty(certs), "Missing certificates or unrecognized format"); return List.copyOf(certs); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java index 9d704598365e..280e7094df20 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java @@ -17,26 +17,32 @@ package org.springframework.boot.ssl.pem; import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; +import java.io.InputStream; +import java.io.UncheckedIOException; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.List; import java.util.Objects; import java.util.regex.Pattern; -import org.springframework.util.FileCopyUtils; +import org.springframework.util.Assert; import org.springframework.util.ResourceUtils; +import org.springframework.util.StreamUtils; /** - * Utility to load PEM content. + * PEM encoded content that can provide {@link X509Certificate certificates} and + * {@link PrivateKey private keys}. * * @author Scott Frederick * @author Phillip Webb + * @since 3.2.0 */ -final class PemContent { +public final class PemContent { private static final Pattern PEM_HEADER = Pattern.compile("-+BEGIN\\s+[^-]*-+", Pattern.CASE_INSENSITIVE); @@ -48,11 +54,32 @@ private PemContent(String text) { this.text = text; } - List getCertificates() { + /** + * Parse and return all {@link X509Certificate certificates} from the PEM content. + * Most PEM files either contain a single certificate or a certificate chain. + * @return the certificates + * @throws IllegalStateException if no certificates could be loaded + */ + public List getCertificates() { return PemCertificateParser.parse(this.text); } - PrivateKey getPrivateKeys(String password) { + /** + * Parse and return the {@link PrivateKey private keys} from the PEM content. + * @return the private keys + * @throws IllegalStateException if no private key could be loaded + */ + public PrivateKey getPrivateKey() { + return getPrivateKey(null); + } + + /** + * Parse and return the {@link PrivateKey private keys} from the PEM content or + * {@code null} if there is no private key. + * @param password the password to decrypt the private keys or {@code null} + * @return the private keys + */ + public PrivateKey getPrivateKey(String password) { return PemPrivateKeyParser.parse(this.text, password); } @@ -77,27 +104,74 @@ public String toString() { return this.text; } - static PemContent load(String content) { + /** + * Load {@link PemContent} from the given content (either the PEM content itself or + * something that can be loaded by {@link ResourceUtils#getURL}). + * @param content the content to load + * @return a new {@link PemContent} instance + * @throws IOException on IO error + */ + static PemContent load(String content) throws IOException { if (content == null) { return null; } - if (isPemContent(content)) { + if (isPresentInText(content)) { return new PemContent(content); } try { - URL url = ResourceUtils.getURL(content); - try (Reader reader = new InputStreamReader(url.openStream(), StandardCharsets.UTF_8)) { - return new PemContent(FileCopyUtils.copyToString(reader)); - } + return load(ResourceUtils.getURL(content)); + } + catch (IOException | UncheckedIOException ex) { + throw new IOException("Error reading certificate or key from file '%s'".formatted(content), ex); + } + } + + /** + * Load {@link PemContent} from the given {@link URL}. + * @param url the URL to load content from + * @return the loaded PEM content + * @throws IOException on IO error + */ + public static PemContent load(URL url) throws IOException { + Assert.notNull(url, "Url must not be null"); + try (InputStream in = url.openStream()) { + return load(in); } - catch (IOException ex) { - throw new IllegalStateException( - "Error reading certificate or key from file '" + content + "':" + ex.getMessage(), ex); + } + + /** + * Load {@link PemContent} from the given {@link Path}. + * @param path a path to load the content from + * @return the loaded PEM content + * @throws IOException on IO error + */ + public static PemContent load(Path path) throws IOException { + Assert.notNull(path, "Path must not be null"); + try (InputStream in = Files.newInputStream(path, StandardOpenOption.READ)) { + return load(in); } } - private static boolean isPemContent(String content) { - return content != null && PEM_HEADER.matcher(content).find() && PEM_FOOTER.matcher(content).find(); + private static PemContent load(InputStream in) throws IOException { + return of(StreamUtils.copyToString(in, StandardCharsets.UTF_8)); + } + + /** + * Return a new {@link PemContent} instance containing the given text. + * @param text the text containing PEM encoded content + * @return a new {@link PemContent} instance + */ + public static PemContent of(String text) { + return (text != null) ? new PemContent(text) : null; + } + + /** + * Return if PEM content is present in the given text. + * @param text the text to check + * @return if the text includes PEM encoded content. + */ + public static boolean isPresentInText(String text) { + return text != null && PEM_HEADER.matcher(text).find() && PEM_FOOTER.matcher(text).find(); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java index 068ae51f6296..dbc5ca697275 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java @@ -194,11 +194,11 @@ static PrivateKey parse(String text, String password) { return privateKey; } } - throw new IllegalStateException("Unrecognized private key format"); } catch (Exception ex) { throw new IllegalStateException("Error loading private key file: " + ex.getMessage(), ex); } + throw new IllegalStateException("Missing private key or unrecognized format"); } /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java index 0346b5395bd1..ac565343b738 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java @@ -141,15 +141,15 @@ private static void verifyKeys(PrivateKey privateKey, X509Certificate[] certific throw new IllegalStateException("Private key matches none of the certificates"); } - private static PrivateKey loadPrivateKey(PemSslStoreDetails details) { + private static PrivateKey loadPrivateKey(PemSslStoreDetails details) throws IOException { PemContent pemContent = PemContent.load(details.privateKey()); if (pemContent == null) { return null; } - return pemContent.getPrivateKeys(details.privateKeyPassword()); + return pemContent.getPrivateKey(details.privateKeyPassword()); } - private static X509Certificate[] loadCertificates(PemSslStoreDetails details) { + private static X509Certificate[] loadCertificates(PemSslStoreDetails details) throws IOException { PemContent pemContent = PemContent.load(details.certificate()); List certificates = pemContent.getCertificates(); Assert.state(!CollectionUtils.isEmpty(certificates), "Loaded certificates are empty"); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java index 6a8ddedb5d5f..e4318afe663c 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java @@ -18,12 +18,17 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link PemContent}. @@ -33,12 +38,61 @@ class PemContentTests { @Test - void loadWhenContentIsNullReturnsNull() { - assertThat(PemContent.load(null)).isNull(); + void getCertificateWhenNoCertificatesThrowsException() { + PemContent content = PemContent.of(""); + assertThatIllegalStateException().isThrownBy(content::getCertificates) + .withMessage("Missing certificates or unrecognized format"); } @Test - void loadWhenContentIsPemContentReturnsContent() { + void getCertificateReturnsCertificates() throws Exception { + PemContent content = PemContent.load(getClass().getResource("/test-cert-chain.pem")); + List certificates = content.getCertificates(); + assertThat(certificates).isNotNull(); + assertThat(certificates).hasSize(2); + assertThat(certificates.get(0).getType()).isEqualTo("X.509"); + assertThat(certificates.get(1).getType()).isEqualTo("X.509"); + } + + @Test + void getPrivateKeyWhenNoKeyThrowsException() { + PemContent content = PemContent.of(""); + assertThatIllegalStateException().isThrownBy(content::getPrivateKey) + .withMessage("Missing private key or unrecognized format"); + } + + @Test + void getPrivateKeyReturnsPrivateKey() throws Exception { + PemContent content = PemContent + .load(getClass().getResource("/org/springframework/boot/web/server/pkcs8/dsa.key")); + PrivateKey privateKey = content.getPrivateKey(); + assertThat(privateKey).isNotNull(); + assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); + assertThat(privateKey.getAlgorithm()).isEqualTo("DSA"); + } + + @Test + void equalsAndHashCode() { + PemContent c1 = PemContent.of("aaa"); + PemContent c2 = PemContent.of("aaa"); + PemContent c3 = PemContent.of("bbb"); + assertThat(c1.hashCode()).isEqualTo(c2.hashCode()); + assertThat(c1).isEqualTo(c1).isEqualTo(c2).isNotEqualTo(c3); + } + + @Test + void toStringReturnsString() { + PemContent content = PemContent.of("test"); + assertThat(content).hasToString("test"); + } + + @Test + void loadWithStringWhenContentIsNullReturnsNull() throws Exception { + assertThat(PemContent.load((String) null)).isNull(); + } + + @Test + void loadWithStringWhenContentIsPemContentReturnsContent() throws Exception { String content = """ -----BEGIN CERTIFICATE----- MIICpDCCAYwCCQCDOqHKPjAhCTANBgkqhkiG9w0BAQUFADAUMRIwEAYDVQQDDAls @@ -61,17 +115,52 @@ void loadWhenContentIsPemContentReturnsContent() { } @Test - void loadWhenClasspathLocationReturnsContent() throws IOException { + void loadWithStringWhenClasspathLocationReturnsContent() throws IOException { String actual = PemContent.load("classpath:test-cert.pem").toString(); String expected = new ClassPathResource("test-cert.pem").getContentAsString(StandardCharsets.UTF_8); assertThat(actual).isEqualTo(expected); } @Test - void loadWhenFileLocationReturnsContent() throws IOException { + void loadWithStringWhenFileLocationReturnsContent() throws IOException { String actual = PemContent.load("src/test/resources/test-cert.pem").toString(); String expected = new ClassPathResource("test-cert.pem").getContentAsString(StandardCharsets.UTF_8); assertThat(actual).isEqualTo(expected); } + @Test + void loadWithUrlReturnsContent() throws Exception { + ClassPathResource resource = new ClassPathResource("test-cert.pem"); + String expected = resource.getContentAsString(StandardCharsets.UTF_8); + String actual = PemContent.load(resource.getURL()).toString(); + assertThat(actual).isEqualTo(expected); + } + + @Test + void loadWithPathReturnsContent() throws IOException { + Path path = Path.of("src/test/resources/test-cert.pem"); + String actual = PemContent.load(path).toString(); + String expected = new ClassPathResource("test-cert.pem").getContentAsString(StandardCharsets.UTF_8); + assertThat(actual).isEqualTo(expected); + } + + @Test + void ofWhenNullReturnsNull() { + assertThat(PemContent.of(null)).isNull(); + } + + @Test + void ofReturnsContent() { + assertThat(PemContent.of("test")).hasToString("test"); + } + + @Test + void hashCodeAndEquals() { + PemContent a = PemContent.of("1"); + PemContent b = PemContent.of("1"); + PemContent c = PemContent.of("2"); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + assertThat(a).isEqualTo(a).isEqualTo(b).isNotEqualTo(c); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java index e01584fb3caa..22ceb5455b43 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemPrivateKeyParserTests.java @@ -77,10 +77,7 @@ void shouldParseTraditionalPkcs1(String file, String algorithm) throws IOExcepti void shouldNotParseUnsupportedTraditionalPkcs1(String file) { assertThatIllegalStateException() .isThrownBy(() -> PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs1/" + file))) - .withMessageContaining("Error loading private key file") - .withCauseInstanceOf(IllegalStateException.class) - .havingCause() - .withMessageContaining("Unrecognized private key format"); + .withMessageContaining("Missing private key or unrecognized format"); } @ParameterizedTest @@ -120,10 +117,7 @@ void shouldParseEcPkcs8(String file, String curveName, String oid) throws IOExce void shouldNotParseUnsupportedEcPkcs8(String file) { assertThatIllegalStateException() .isThrownBy(() -> PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/pkcs8/" + file))) - .withMessageContaining("Error loading private key file") - .withCauseInstanceOf(IllegalStateException.class) - .havingCause() - .withMessageContaining("Unrecognized private key format"); + .withMessageContaining("Missing private key or unrecognized format"); } @ParameterizedTest @@ -191,10 +185,7 @@ void shouldParseEcSec1(String file, String curveName, String oid) throws IOExcep void shouldNotParseUnsupportedEcSec1(String file) { assertThatIllegalStateException() .isThrownBy(() -> PemPrivateKeyParser.parse(read("org/springframework/boot/web/server/sec1/" + file))) - .withMessageContaining("Error loading private key file") - .withCauseInstanceOf(IllegalStateException.class) - .havingCause() - .withMessageContaining("Unrecognized private key format"); + .withMessageContaining("Missing private key or unrecognized format"); } @Test @@ -255,7 +246,7 @@ void shouldNotParseEncryptedSec1() { assertThatIllegalStateException() .isThrownBy(() -> PemPrivateKeyParser .parse(read("org/springframework/boot/web/server/sec1/prime256v1-aes-128-cbc.key"), "test")) - .withMessageContaining("Unrecognized private key format"); + .withMessageContaining("Missing private key or unrecognized format"); } @Test @@ -265,7 +256,7 @@ void shouldNotParseEncryptedPkcs1() { assertThatIllegalStateException() .isThrownBy(() -> PemPrivateKeyParser .parse(read("org/springframework/boot/web/server/pkcs1/rsa-aes-256-cbc.key"), "test")) - .withMessageContaining("Unrecognized private key format"); + .withMessageContaining("Missing private key or unrecognized format"); } private String read(String path) throws IOException { From 5e5d2265f5143850db0d348a1d3432c0a8126d32 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 30 Oct 2023 19:44:37 -0700 Subject: [PATCH 0723/1215] Introduce `PemSslStore` as an alternative to `PemSslStoreDetails`. Add a `PemSslStore` interface that can be used as an alternative to `PemSslStoreDetails` when PEM content has already been loaded and parsed. Closes gh-38175 --- .../boot/ssl/pem/LoadedPemSslStore.java | 88 +++++++++ .../boot/ssl/pem/PemSslStore.java | 172 ++++++++++++++++++ .../boot/ssl/pem/PemSslStoreBundle.java | 95 +++++----- .../boot/ssl/pem/PemSslStoreDetails.java | 52 ++++-- .../boot/ssl/pem/PemSslStoreBundleTests.java | 37 ++-- .../boot/ssl/pem/PemSslStoreTests.java | 78 ++++++++ 6 files changed, 453 insertions(+), 69 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/LoadedPemSslStore.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreTests.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/LoadedPemSslStore.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/LoadedPemSslStore.java new file mode 100644 index 000000000000..07f457a1b1f8 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/LoadedPemSslStore.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2023 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.ssl.pem; + +import java.io.IOException; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.List; + +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * {@link PemSslStore} loaded from {@link PemSslStoreDetails}. + * + * @author Phillip Webb + * @see PemSslStore#load(PemSslStoreDetails) + */ +final class LoadedPemSslStore implements PemSslStore { + + private final PemSslStoreDetails details; + + private final List certificates; + + private final PrivateKey privateKey; + + LoadedPemSslStore(PemSslStoreDetails details) throws IOException { + Assert.notNull(details, "Details must not be null"); + this.details = details; + this.certificates = loadCertificates(details); + this.privateKey = loadPrivateKey(details); + } + + private static List loadCertificates(PemSslStoreDetails details) throws IOException { + PemContent pemContent = PemContent.load(details.certificates()); + if (pemContent == null) { + return null; + } + List certificates = pemContent.getCertificates(); + Assert.state(!CollectionUtils.isEmpty(certificates), "Loaded certificates are empty"); + return certificates; + } + + private static PrivateKey loadPrivateKey(PemSslStoreDetails details) throws IOException { + PemContent pemContent = PemContent.load(details.privateKey()); + return (pemContent != null) ? pemContent.getPrivateKey(details.privateKeyPassword()) : null; + } + + @Override + public String type() { + return this.details.type(); + } + + @Override + public String alias() { + return this.details.alias(); + } + + @Override + public String password() { + return this.details.password(); + } + + @Override + public List certificates() { + return this.certificates; + } + + @Override + public PrivateKey privateKey() { + return this.privateKey; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java new file mode 100644 index 000000000000..7eb3ce7b6757 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java @@ -0,0 +1,172 @@ +/* + * Copyright 2012-2023 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.ssl.pem; + +import java.io.IOException; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.List; + +import org.springframework.util.Assert; + +/** + * An individual trust or key store that has been loaded from PEM content. + * + * @author Phillip Webb + * @since 3.2.0 + * @see PemSslStoreDetails + * @see PemContent + */ +public interface PemSslStore { + + /** + * The key store type, for example {@code JKS} or {@code PKCS11}. A {@code null} value + * will use {@link KeyStore#getDefaultType()}). + * @return the key store type + */ + String type(); + + /** + * The alias used when setting entries in the {@link KeyStore}. + * @return the alias + */ + String alias(); + + /** + * the password used + * {@link KeyStore#setKeyEntry(String, java.security.Key, char[], java.security.cert.Certificate[]) + * setting key entries} in the {@link KeyStore}. + * @return the password + */ + String password(); + + /** + * The certificates for this store. When a {@link #privateKey() private key} is + * present the returned value is treated as a certificate chain, otherwise it is + * treated a list of certificates that should all be registered. + * @return the X509 certificates + */ + List certificates(); + + /** + * The private key for this store or {@code null}. + * @return the private key + */ + PrivateKey privateKey(); + + /** + * Return a new {@link PemSslStore} instance with a new alias. + * @param alias the new alias + * @return a new {@link PemSslStore} instance + */ + default PemSslStore withAlias(String alias) { + return of(type(), alias, password(), certificates(), privateKey()); + } + + /** + * Return a new {@link PemSslStore} instance with a new password. + * @param password the new password + * @return a new {@link PemSslStore} instance + */ + default PemSslStore withPassword(String password) { + return of(type(), alias(), password, certificates(), privateKey()); + } + + /** + * Return a {@link PemSslStore} instance loaded using the given + * {@link PemSslStoreDetails}. + * @param details the PEM store details + * @return a loaded {@link PemSslStore} or {@code null}. + * @throws IOException on IO error + */ + static PemSslStore load(PemSslStoreDetails details) throws IOException { + if (details == null || details.isEmpty()) { + return null; + } + return new LoadedPemSslStore(details); + } + + /** + * Factory method that can be used to create a new {@link PemSslStore} with the given + * values. + * @param type the key store type + * @param certificates the certificates for this store + * @param privateKey the private key + * @return a new {@link PemSslStore} instance + */ + static PemSslStore of(String type, List certificates, PrivateKey privateKey) { + return of(type, null, null, certificates, privateKey); + } + + /** + * Factory method that can be used to create a new {@link PemSslStore} with the given + * values. + * @param certificates the certificates for this store + * @param privateKey the private key + * @return a new {@link PemSslStore} instance + */ + static PemSslStore of(List certificates, PrivateKey privateKey) { + return of(null, null, null, certificates, privateKey); + } + + /** + * Factory method that can be used to create a new {@link PemSslStore} with the given + * values. + * @param type the key store type + * @param alias the alias used when setting entries in the {@link KeyStore} + * @param password the password used + * {@link KeyStore#setKeyEntry(String, java.security.Key, char[], java.security.cert.Certificate[]) + * setting key entries} in the {@link KeyStore} + * @param certificates the certificates for this store + * @param privateKey the private key + * @return a new {@link PemSslStore} instance + */ + static PemSslStore of(String type, String alias, String password, List certificates, + PrivateKey privateKey) { + Assert.notEmpty(certificates, "Certificates must not be empty"); + return new PemSslStore() { + + @Override + public String type() { + return type; + } + + @Override + public String alias() { + return alias; + } + + @Override + public String password() { + return password; + } + + @Override + public List certificates() { + return certificates; + } + + @Override + public PrivateKey privateKey() { + return privateKey; + } + + }; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java index ac565343b738..b5ad0b5b46ea 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java @@ -17,6 +17,7 @@ package org.springframework.boot.ssl.pem; import java.io.IOException; +import java.io.UncheckedIOException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; @@ -27,7 +28,6 @@ import org.springframework.boot.ssl.SslStoreBundle; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** @@ -52,7 +52,7 @@ public class PemSslStoreBundle implements SslStoreBundle { * @param trustStoreDetails the trust store details */ public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails) { - this(keyStoreDetails, trustStoreDetails, null); + this(keyStoreDetails, trustStoreDetails, null, false); } /** @@ -83,8 +83,26 @@ public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails private PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String alias, boolean verifyKeys) { - this.keyStore = createKeyStore("key", keyStoreDetails, alias, verifyKeys); - this.trustStore = createKeyStore("trust", trustStoreDetails, alias, verifyKeys); + try { + this.keyStore = createKeyStore("key", PemSslStore.load(keyStoreDetails), alias, verifyKeys); + this.trustStore = createKeyStore("trust", PemSslStore.load(trustStoreDetails), alias, verifyKeys); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + /** + * Create a new {@link PemSslStoreBundle} instance. + * @param pemKeyStore the PEM key store + * @param pemTrustStore the PEM trust store + * @param alias the alias to use or {@code null} to use a default alias + * @param verifyKeys whether to verify that the private key matches the public key + * @since 3.2.0 + */ + public PemSslStoreBundle(PemSslStore pemKeyStore, PemSslStore pemTrustStore, String alias, boolean verifyKeys) { + this.keyStore = createKeyStore("key", pemKeyStore, alias, verifyKeys); + this.trustStore = createKeyStore("trust", pemTrustStore, alias, verifyKeys); } @Override @@ -102,22 +120,22 @@ public KeyStore getTrustStore() { return this.trustStore; } - private static KeyStore createKeyStore(String name, PemSslStoreDetails details, String alias, boolean verifyKeys) { - if (details == null || details.isEmpty()) { + private static KeyStore createKeyStore(String name, PemSslStore pemSslStore, String alias, boolean verifyKeys) { + if (pemSslStore == null) { return null; } try { - Assert.notNull(details.certificate(), "Certificate content must not be null"); - alias = (details.alias() != null) ? details.alias() : alias; + Assert.notEmpty(pemSslStore.certificates(), "Certificates must not be empty"); + alias = (pemSslStore.alias() != null) ? pemSslStore.alias() : alias; alias = (alias != null) ? alias : DEFAULT_ALIAS; - KeyStore store = createKeyStore(details); - X509Certificate[] certificates = loadCertificates(details); - PrivateKey privateKey = loadPrivateKey(details); + KeyStore store = createKeyStore(pemSslStore.type()); + List certificates = pemSslStore.certificates(); + PrivateKey privateKey = pemSslStore.privateKey(); if (privateKey != null) { if (verifyKeys) { verifyKeys(privateKey, certificates); } - addPrivateKey(store, privateKey, alias, details.password(), certificates); + addPrivateKey(store, privateKey, alias, pemSslStore.password(), certificates); } else { addCertificates(store, certificates, alias); @@ -129,50 +147,37 @@ private static KeyStore createKeyStore(String name, PemSslStoreDetails details, } } - private static void verifyKeys(PrivateKey privateKey, X509Certificate[] certificates) { + private static KeyStore createKeyStore(String type) + throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { + KeyStore store = KeyStore.getInstance(StringUtils.hasText(type) ? type : KeyStore.getDefaultType()); + store.load(null); + return store; + } + + private static void verifyKeys(PrivateKey privateKey, List certificateChain) { KeyVerifier keyVerifier = new KeyVerifier(); // Key should match one of the certificates - for (X509Certificate certificate : certificates) { - Result result = keyVerifier.matches(privateKey, certificate.getPublicKey()); - if (result == Result.YES) { + for (X509Certificate certificate : certificateChain) { + KeyVerifier.Result result = keyVerifier.matches(privateKey, certificate.getPublicKey()); + if (result == KeyVerifier.Result.YES) { return; } } - throw new IllegalStateException("Private key matches none of the certificates"); - } - - private static PrivateKey loadPrivateKey(PemSslStoreDetails details) throws IOException { - PemContent pemContent = PemContent.load(details.privateKey()); - if (pemContent == null) { - return null; - } - return pemContent.getPrivateKey(details.privateKeyPassword()); - } - - private static X509Certificate[] loadCertificates(PemSslStoreDetails details) throws IOException { - PemContent pemContent = PemContent.load(details.certificate()); - List certificates = pemContent.getCertificates(); - Assert.state(!CollectionUtils.isEmpty(certificates), "Loaded certificates are empty"); - return certificates.toArray(X509Certificate[]::new); - } - - private static KeyStore createKeyStore(PemSslStoreDetails details) - throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { - String type = StringUtils.hasText(details.type()) ? details.type() : KeyStore.getDefaultType(); - KeyStore store = KeyStore.getInstance(type); - store.load(null); - return store; + throw new IllegalStateException("Private key matches none of the certificates in the chain"); } private static void addPrivateKey(KeyStore keyStore, PrivateKey privateKey, String alias, String keyPassword, - X509Certificate[] certificates) throws KeyStoreException { - keyStore.setKeyEntry(alias, privateKey, (keyPassword != null) ? keyPassword.toCharArray() : null, certificates); + List certificateChain) throws KeyStoreException { + keyStore.setKeyEntry(alias, privateKey, (keyPassword != null) ? keyPassword.toCharArray() : null, + certificateChain.toArray(X509Certificate[]::new)); } - private static void addCertificates(KeyStore keyStore, X509Certificate[] certificates, String alias) + private static void addCertificates(KeyStore keyStore, List certificates, String alias) throws KeyStoreException { - for (int index = 0; index < certificates.length; index++) { - keyStore.setCertificateEntry(alias + "-" + index, certificates[index]); + for (int index = 0; index < certificates.size(); index++) { + String entryAlias = alias + ((certificates.size() == 1) ? "" : "-" + index); + X509Certificate certificate = certificates.get(index); + keyStore.setCertificateEntry(entryAlias, certificate); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java index cdc580f4ce44..57d6d5db60ce 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java @@ -30,16 +30,19 @@ * @param password the password used * {@link KeyStore#setKeyEntry(String, java.security.Key, char[], java.security.cert.Certificate[]) * setting key entries} in the {@link KeyStore} - * @param certificate the certificate content (either the PEM content itself or something - * that can be loaded by {@link ResourceUtils#getURL}) + * @param certificates the certificates content (either the PEM content itself or + * something that can be loaded by {@link ResourceUtils#getURL}). When a + * {@link #privateKey() private key} is present this value is treated as a certificate + * chain, otherwise it is treated a list of certificates that should all be registered. * @param privateKey the private key content (either the PEM content itself or something * that can be loaded by {@link ResourceUtils#getURL}) * @param privateKeyPassword a password used to decrypt an encrypted private key * @author Scott Frederick * @author Phillip Webb * @since 3.1.0 + * @see PemSslStore#load(PemSslStoreDetails) */ -public record PemSslStoreDetails(String type, String alias, String password, String certificate, String privateKey, +public record PemSslStoreDetails(String type, String alias, String password, String certificates, String privateKey, String privateKeyPassword) { /** @@ -50,7 +53,7 @@ public record PemSslStoreDetails(String type, String alias, String password, Str * @param password the password used * {@link KeyStore#setKeyEntry(String, java.security.Key, char[], java.security.cert.Certificate[]) * setting key entries} in the {@link KeyStore} - * @param certificate the certificate content (either the PEM content itself or + * @param certificates the certificate content (either the PEM content itself or * something that can be loaded by {@link ResourceUtils#getURL}) * @param privateKey the private key content (either the PEM content itself or * something that can be loaded by {@link ResourceUtils#getURL}) @@ -87,6 +90,16 @@ public PemSslStoreDetails(String type, String certificate, String privateKey) { this(type, certificate, privateKey, null); } + /** + * Return the certificate content. + * @return the certificate content + * @deprecated since 3.2.0 for removal in 3.4.0 in favor of {@link #certificates()} + */ + @Deprecated(since = "3.2.0", forRemoval = true) + public String certificate() { + return certificates(); + } + /** * Return a new {@link PemSslStoreDetails} instance with a new alias. * @param alias the new alias @@ -94,7 +107,7 @@ public PemSslStoreDetails(String type, String certificate, String privateKey) { * @since 3.2.0 */ public PemSslStoreDetails withAlias(String alias) { - return new PemSslStoreDetails(this.type, alias, this.password, this.certificate, this.privateKey, + return new PemSslStoreDetails(this.type, alias, this.password, this.certificates, this.privateKey, this.privateKeyPassword); } @@ -105,7 +118,7 @@ public PemSslStoreDetails withAlias(String alias) { * @since 3.2.0 */ public PemSslStoreDetails withPassword(String password) { - return new PemSslStoreDetails(this.type, this.alias, password, this.certificate, this.privateKey, + return new PemSslStoreDetails(this.type, this.alias, password, this.certificates, this.privateKey, this.privateKeyPassword); } @@ -115,7 +128,7 @@ public PemSslStoreDetails withPassword(String password) { * @return a new {@link PemSslStoreDetails} instance */ public PemSslStoreDetails withPrivateKey(String privateKey) { - return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificate, privateKey, + return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificates, privateKey, this.privateKeyPassword); } @@ -125,12 +138,12 @@ public PemSslStoreDetails withPrivateKey(String privateKey) { * @return a new {@link PemSslStoreDetails} instance */ public PemSslStoreDetails withPrivateKeyPassword(String privateKeyPassword) { - return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificate, this.privateKey, + return new PemSslStoreDetails(this.type, this.alias, this.password, this.certificates, this.privateKey, privateKeyPassword); } boolean isEmpty() { - return isEmpty(this.type) && isEmpty(this.certificate) && isEmpty(this.privateKey); + return isEmpty(this.type) && isEmpty(this.certificates) && isEmpty(this.privateKey); } private boolean isEmpty(String value) { @@ -139,12 +152,27 @@ private boolean isEmpty(String value) { /** * Factory method to create a new {@link PemSslStoreDetails} instance for the given - * certificate. - * @param certificate the certificate + * certificate. Note: This method doesn't actually check if the provided value + * only contains a single certificate. It is functionally equivalent to + * {@link #forCertificates(String)}. + * @param certificate the certificate content (either the PEM content itself or + * something that can be loaded by {@link ResourceUtils#getURL}) * @return a new {@link PemSslStoreDetails} instance. */ public static PemSslStoreDetails forCertificate(String certificate) { - return new PemSslStoreDetails(null, certificate, null); + return forCertificates(certificate); + } + + /** + * Factory method to create a new {@link PemSslStoreDetails} instance for the given + * certificates. + * @param certificates the certificates content (either the PEM content itself or + * something that can be loaded by {@link ResourceUtils#getURL}) + * @return a new {@link PemSslStoreDetails} instance. + * @since 3.2.0 + */ + public static PemSslStoreDetails forCertificates(String certificates) { + return new PemSslStoreDetails(null, certificates, null); } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java index 6414382683b8..c6759036d357 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java @@ -17,6 +17,9 @@ package org.springframework.boot.ssl.pem; import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.List; import java.util.function.Consumer; import org.junit.jupiter.api.Test; @@ -94,7 +97,7 @@ class PemSslStoreBundleTests { private static final char[] EMPTY_KEY_PASSWORD = new char[] {}; @Test - void whenNullStores() { + void createWithDetailsWhenNullStores() { PemSslStoreDetails keyStoreDetails = null; PemSslStoreDetails trustStoreDetails = null; PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails); @@ -104,7 +107,7 @@ void whenNullStores() { } @Test - void whenStoresHaveNoValues() { + void createWithDetailsWhenStoresHaveNoValues() { PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate(null); PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate(null); PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails); @@ -114,7 +117,7 @@ void whenStoresHaveNoValues() { } @Test - void whenHasKeyStoreDetailsCertAndKey() { + void createWithDetailsWhenHasKeyStoreDetailsCertAndKey() { PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem") .withPrivateKey("classpath:test-key.pem"); PemSslStoreDetails trustStoreDetails = null; @@ -124,7 +127,7 @@ void whenHasKeyStoreDetailsCertAndKey() { } @Test - void whenHasKeyStoreDetailsCertAndEncryptedKey() { + void createWithDetailsWhenHasKeyStoreDetailsCertAndEncryptedKey() { PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem") .withPrivateKey("classpath:ssl/pkcs8/key-rsa-encrypted.pem") .withPrivateKeyPassword("test"); @@ -135,17 +138,17 @@ void whenHasKeyStoreDetailsCertAndEncryptedKey() { } @Test - void whenHasKeyStoreDetailsAndTrustStoreDetailsWithoutKey() { + void createWithDetailsWhenHasKeyStoreDetailsAndTrustStoreDetailsWithoutKey() { PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem") .withPrivateKey("classpath:test-key.pem"); PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem"); PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, trustStoreDetails); assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("ssl")); - assertThat(bundle.getTrustStore()).satisfies(storeContainingCert("ssl-0")); + assertThat(bundle.getTrustStore()).satisfies(storeContainingCert("ssl")); } @Test - void whenHasKeyStoreDetailsAndTrustStoreDetails() { + void createWithDetailsWhenHasKeyStoreDetailsAndTrustStoreDetails() { PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem") .withPrivateKey("classpath:test-key.pem"); PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem") @@ -156,7 +159,7 @@ void whenHasKeyStoreDetailsAndTrustStoreDetails() { } @Test - void whenHasEmbeddedKeyStoreDetailsAndTrustStoreDetails() { + void createWithDetailsWhenHasEmbeddedKeyStoreDetailsAndTrustStoreDetails() { PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate(CERTIFICATE).withPrivateKey(PRIVATE_KEY); PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate(CERTIFICATE) .withPrivateKey(PRIVATE_KEY); @@ -167,7 +170,7 @@ void whenHasEmbeddedKeyStoreDetailsAndTrustStoreDetails() { @Test @SuppressWarnings("removal") - void whenHasKeyStoreDetailsAndTrustStoreDetailsAndAlias() { + void createWithDetailsWhenHasKeyStoreDetailsAndTrustStoreDetailsAndAlias() { PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem") .withPrivateKey("classpath:test-key.pem"); PemSslStoreDetails trustStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem") @@ -178,7 +181,7 @@ void whenHasKeyStoreDetailsAndTrustStoreDetailsAndAlias() { } @Test - void whenHasStoreType() { + void createWithDetailsWhenHasStoreType() { PemSslStoreDetails keyStoreDetails = new PemSslStoreDetails("PKCS12", "classpath:test-cert.pem", "classpath:test-key.pem"); PemSslStoreDetails trustStoreDetails = new PemSslStoreDetails("PKCS12", "classpath:test-cert.pem", @@ -189,7 +192,7 @@ void whenHasStoreType() { } @Test - void whenHasKeyStoreDetailsAndTrustStoreDetailsAndKeyPassword() { + void createWithDetailsWhenHasKeyStoreDetailsAndTrustStoreDetailsAndKeyPassword() { PemSslStoreDetails keyStoreDetails = PemSslStoreDetails.forCertificate("classpath:test-cert.pem") .withPrivateKey("classpath:test-key.pem") .withAlias("ksa") @@ -217,7 +220,7 @@ void shouldVerifyKeysIfEnabled() { @Test void shouldVerifyKeysIfEnabledAndCertificateChainIsUsed() { PemSslStoreDetails keyStoreDetails = PemSslStoreDetails - .forCertificate("classpath:org/springframework/boot/ssl/pem/key2-chain.crt") + .forCertificates("classpath:org/springframework/boot/ssl/pem/key2-chain.crt") .withPrivateKey("classpath:org/springframework/boot/ssl/pem/key2.pem") .withAlias("test-alias") .withPassword("keysecret"); @@ -234,6 +237,16 @@ void shouldFailIfVerifyKeysIsEnabledAndKeysDontMatch() { .withMessageContaining("Private key matches none of the certificates"); } + @Test + void createWithPemSslStoreCreatesInstance() { + List certificates = PemContent.of(CERTIFICATE).getCertificates(); + PrivateKey privateKey = PemContent.of(PRIVATE_KEY).getPrivateKey(); + PemSslStore pemSslStore = PemSslStore.of(certificates, privateKey); + PemSslStoreBundle bundle = new PemSslStoreBundle(pemSslStore, pemSslStore, null, false); + assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("ssl")); + assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("ssl")); + } + private Consumer storeContainingCert(String keyAlias) { return storeContainingCert(KeyStore.getDefaultType(), keyAlias); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreTests.java new file mode 100644 index 000000000000..9e4ca402b14d --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2023 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.ssl.pem; + +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link PemSslStore}. + * + * @author Phillip Webb + */ +class PemSslStoreTests { + + @Test + void withAliasReturnsStoreWithNewAlias() { + List certificates = List.of(mock(X509Certificate.class)); + PrivateKey privateKey = mock(PrivateKey.class); + PemSslStore store = PemSslStore.of("type", "alias", "secret", certificates, privateKey); + assertThat(store.withAlias("newalias").alias()).isEqualTo("newalias"); + } + + @Test + void withPasswordReturnsStoreWithNewPassword() { + List certificates = List.of(mock(X509Certificate.class)); + PrivateKey privateKey = mock(PrivateKey.class); + PemSslStore store = PemSslStore.of("type", "alias", "secret", certificates, privateKey); + assertThat(store.withPassword("newsecret").password()).isEqualTo("newsecret"); + } + + @Test + void ofWhenNullCertificatesThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> PemSslStore.of(null, null, null, null, null)) + .withMessage("Certificates must not be empty"); + } + + @Test + void ofWhenEmptyCertificatesThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> PemSslStore.of(null, null, null, Collections.emptyList(), null)) + .withMessage("Certificates must not be empty"); + } + + @Test + void ofReturnsPemSslStore() { + List certificates = List.of(mock(X509Certificate.class)); + PrivateKey privateKey = mock(PrivateKey.class); + PemSslStore store = PemSslStore.of("type", "alias", "password", certificates, privateKey); + assertThat(store.type()).isEqualTo("type"); + assertThat(store.alias()).isEqualTo("alias"); + assertThat(store.password()).isEqualTo("password"); + assertThat(store.certificates()).isEqualTo(certificates); + assertThat(store.privateKey()).isEqualTo(privateKey); + } + +} From 1b61bc1f2094e38e3c0c3491688d38ea3d6af7f9 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 27 Oct 2023 21:07:24 -0700 Subject: [PATCH 0724/1215] Move PEM verification to spring-boot-autoconfigure Move `KeyVerifier` to spring-boot-autoconfigure to reduce the public API required in `PemSslStoreBundle`. This commit also moves the verify property so that is can be set per store. Closes gh-38173 --- .../boot/autoconfigure/ssl}/KeyVerifier.java | 2 +- .../ssl/PemSslBundleProperties.java | 26 ++++----- .../ssl/PropertiesSslBundle.java | 44 ++++++++++++--- .../autoconfigure/ssl}/KeyVerifierTests.java | 4 +- .../ssl/PropertiesSslBundleTests.java | 48 +++++++++++++++++ .../boot/autoconfigure/ssl}/key1.crt | 0 .../boot/autoconfigure/ssl}/key1.pem | 0 .../boot/autoconfigure/ssl}/key2-chain.crt | 0 .../boot/autoconfigure/ssl}/key2.crt | 0 .../boot/autoconfigure/ssl}/key2.pem | 0 .../boot/ssl/pem/PemSslStoreBundle.java | 53 +++++-------------- .../boot/ssl/pem/PemSslStoreBundleTests.java | 34 +----------- 12 files changed, 113 insertions(+), 98 deletions(-) rename spring-boot-project/{spring-boot/src/main/java/org/springframework/boot/ssl/pem => spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl}/KeyVerifier.java (98%) rename spring-boot-project/{spring-boot/src/test/java/org/springframework/boot/ssl/pem => spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl}/KeyVerifierTests.java (96%) rename spring-boot-project/{spring-boot/src/test/resources/org/springframework/boot/ssl/pem => spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/ssl}/key1.crt (100%) rename spring-boot-project/{spring-boot/src/test/resources/org/springframework/boot/ssl/pem => spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/ssl}/key1.pem (100%) rename spring-boot-project/{spring-boot/src/test/resources/org/springframework/boot/ssl/pem => spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/ssl}/key2-chain.crt (100%) rename spring-boot-project/{spring-boot/src/test/resources/org/springframework/boot/ssl/pem => spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/ssl}/key2.crt (100%) rename spring-boot-project/{spring-boot/src/test/resources/org/springframework/boot/ssl/pem => spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/ssl}/key2.pem (100%) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/KeyVerifier.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/KeyVerifier.java similarity index 98% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/KeyVerifier.java rename to spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/KeyVerifier.java index 6b79c0a305e9..65d68134e06a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/KeyVerifier.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/KeyVerifier.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.ssl.pem; +package org.springframework.boot.autoconfigure.ssl; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemSslBundleProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemSslBundleProperties.java index d98939350f96..7870c6b05925 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemSslBundleProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemSslBundleProperties.java @@ -39,11 +39,6 @@ public class PemSslBundleProperties extends SslBundleProperties { */ private final Store truststore = new Store(); - /** - * Whether to verify that the private key matches the public key. - */ - private boolean verifyKeys; - public Store getKeystore() { return this.keystore; } @@ -52,14 +47,6 @@ public Store getTruststore() { return this.truststore; } - public boolean isVerifyKeys() { - return this.verifyKeys; - } - - public void setVerifyKeys(boolean verifyKeys) { - this.verifyKeys = verifyKeys; - } - /** * Store properties. */ @@ -85,6 +72,11 @@ public static class Store { */ private String privateKeyPassword; + /** + * Whether to verify that the private key matches the public key. + */ + private boolean verifyKeys; + public String getType() { return this.type; } @@ -117,6 +109,14 @@ public void setPrivateKeyPassword(String privateKeyPassword) { this.privateKeyPassword = privateKeyPassword; } + public boolean isVerifyKeys() { + return this.verifyKeys; + } + + public void setVerifyKeys(boolean verifyKeys) { + this.verifyKeys = verifyKeys; + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java index 96188861a123..12a1847b24c5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java @@ -16,6 +16,10 @@ package org.springframework.boot.autoconfigure.ssl; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.security.cert.X509Certificate; + import org.springframework.boot.autoconfigure.ssl.SslBundleProperties.Key; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundleKey; @@ -24,6 +28,7 @@ import org.springframework.boot.ssl.SslStoreBundle; import org.springframework.boot.ssl.jks.JksSslStoreBundle; import org.springframework.boot.ssl.jks.JksSslStoreDetails; +import org.springframework.boot.ssl.pem.PemSslStore; import org.springframework.boot.ssl.pem.PemSslStoreBundle; import org.springframework.boot.ssl.pem.PemSslStoreDetails; @@ -107,16 +112,39 @@ public static SslBundle get(JksSslBundleProperties properties) { } private static SslStoreBundle asSslStoreBundle(PemSslBundleProperties properties) { - PemSslStoreDetails keyStoreDetails = asStoreDetails(properties.getKeystore()) - .withAlias(properties.getKey().getAlias()); - PemSslStoreDetails trustStoreDetails = asStoreDetails(properties.getTruststore()) - .withAlias(properties.getKey().getAlias()); - return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails, properties.isVerifyKeys()); + PemSslStore keyStore = asPemSslStore(properties.getKeystore(), properties.getKey().getAlias()); + PemSslStore trustStore = asPemSslStore(properties.getTruststore(), properties.getKey().getAlias()); + return new PemSslStoreBundle(keyStore, trustStore); + } + + private static PemSslStore asPemSslStore(PemSslBundleProperties.Store properties, String alias) { + try { + PemSslStoreDetails details = asStoreDetails(properties, alias); + PemSslStore pemSslStore = PemSslStore.load(details); + if (properties.isVerifyKeys()) { + verifyPemSslStoreKeys(pemSslStore); + } + return pemSslStore; + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private static void verifyPemSslStoreKeys(PemSslStore pemSslStore) { + KeyVerifier keyVerifier = new KeyVerifier(); + for (X509Certificate certificate : pemSslStore.certificates()) { + KeyVerifier.Result result = keyVerifier.matches(pemSslStore.privateKey(), certificate.getPublicKey()); + if (result == KeyVerifier.Result.YES) { + return; + } + } + throw new IllegalStateException("Private key matches none of the certificates in the chain"); } - private static PemSslStoreDetails asStoreDetails(PemSslBundleProperties.Store properties) { - return new PemSslStoreDetails(properties.getType(), properties.getCertificate(), properties.getPrivateKey(), - properties.getPrivateKeyPassword()); + private static PemSslStoreDetails asStoreDetails(PemSslBundleProperties.Store properties, String alias) { + return new PemSslStoreDetails(properties.getType(), alias, null, properties.getCertificate(), + properties.getPrivateKey(), properties.getPrivateKeyPassword()); } private static SslStoreBundle asSslStoreBundle(JksSslBundleProperties properties) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/KeyVerifierTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/KeyVerifierTests.java similarity index 96% rename from spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/KeyVerifierTests.java rename to spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/KeyVerifierTests.java index 4f681ce77f92..269b824a6782 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/KeyVerifierTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/KeyVerifierTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.ssl.pem; +package org.springframework.boot.autoconfigure.ssl; import java.security.InvalidAlgorithmParameterException; import java.security.KeyPair; @@ -33,7 +33,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.boot.ssl.pem.KeyVerifier.Result; +import org.springframework.boot.autoconfigure.ssl.KeyVerifier.Result; import static org.assertj.core.api.Assertions.assertThat; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundleTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundleTests.java index 1b745328310a..6143a7919d34 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundleTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundleTests.java @@ -20,12 +20,15 @@ import java.security.KeyStore; import java.security.cert.Certificate; import java.util.Set; +import java.util.function.Consumer; import org.junit.jupiter.api.Test; import org.springframework.boot.ssl.SslBundle; +import org.springframework.util.function.ThrowingConsumer; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link PropertiesSslBundle}. @@ -35,6 +38,8 @@ */ class PropertiesSslBundleTests { + private static final char[] EMPTY_KEY_PASSWORD = new char[] {}; + @Test void pemPropertiesAreMappedToSslBundle() throws Exception { PemSslBundleProperties properties = new PemSslBundleProperties(); @@ -99,4 +104,47 @@ void jksPropertiesAreMappedToSslBundle() { assertThat(trustStore.getProvider().getName()).isEqualTo("SUN"); } + @Test + void getWithPemSslBundlePropertiesWhenVerifyKeyStoreAgainstSingleCertificateWithMatchCreatesBundle() { + PemSslBundleProperties properties = new PemSslBundleProperties(); + properties.getKeystore().setCertificate("classpath:org/springframework/boot/autoconfigure/ssl/key1.crt"); + properties.getKeystore().setPrivateKey("classpath:org/springframework/boot/autoconfigure/ssl/key1.pem"); + properties.getKeystore().setVerifyKeys(true); + properties.getKey().setAlias("test-alias"); + SslBundle bundle = PropertiesSslBundle.get(properties); + assertThat(bundle.getStores().getKeyStore()).satisfies(storeContainingCertAndKey("test-alias")); + } + + @Test + void getWithPemSslBundlePropertiesWhenVerifyKeyStoreAgainstCertificateChainWithMatchCreatesBundle() { + PemSslBundleProperties properties = new PemSslBundleProperties(); + properties.getKeystore().setCertificate("classpath:org/springframework/boot/autoconfigure/ssl/key2-chain.crt"); + properties.getKeystore().setPrivateKey("classpath:org/springframework/boot/autoconfigure/ssl/key2.pem"); + properties.getKeystore().setVerifyKeys(true); + properties.getKey().setAlias("test-alias"); + SslBundle bundle = PropertiesSslBundle.get(properties); + assertThat(bundle.getStores().getKeyStore()).satisfies(storeContainingCertAndKey("test-alias")); + } + + @Test + void getWithPemSslBundlePropertiesWhenVerifyKeyStoreWithNoMatchThrowsException() { + PemSslBundleProperties properties = new PemSslBundleProperties(); + properties.getKeystore().setCertificate("classpath:org/springframework/boot/autoconfigure/ssl/key2.crt"); + properties.getKeystore().setPrivateKey("classpath:org/springframework/boot/autoconfigure/ssl/key1.pem"); + properties.getKeystore().setVerifyKeys(true); + properties.getKey().setAlias("test-alias"); + assertThatIllegalStateException().isThrownBy(() -> PropertiesSslBundle.get(properties)) + .withMessageContaining("Private key matches none of the certificates"); + } + + private Consumer storeContainingCertAndKey(String keyAlias) { + return ThrowingConsumer.of((keyStore) -> { + assertThat(keyStore).isNotNull(); + assertThat(keyStore.getType()).isEqualTo(KeyStore.getDefaultType()); + assertThat(keyStore.containsAlias(keyAlias)).isTrue(); + assertThat(keyStore.getCertificate(keyAlias)).isNotNull(); + assertThat(keyStore.getKey(keyAlias, EMPTY_KEY_PASSWORD)).isNotNull(); + }); + } + } diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key1.crt b/spring-boot-project/spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/ssl/key1.crt similarity index 100% rename from spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key1.crt rename to spring-boot-project/spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/ssl/key1.crt diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key1.pem b/spring-boot-project/spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/ssl/key1.pem similarity index 100% rename from spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key1.pem rename to spring-boot-project/spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/ssl/key1.pem diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key2-chain.crt b/spring-boot-project/spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/ssl/key2-chain.crt similarity index 100% rename from spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key2-chain.crt rename to spring-boot-project/spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/ssl/key2-chain.crt diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key2.crt b/spring-boot-project/spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/ssl/key2.crt similarity index 100% rename from spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key2.crt rename to spring-boot-project/spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/ssl/key2.crt diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key2.pem b/spring-boot-project/spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/ssl/key2.pem similarity index 100% rename from spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/ssl/pem/key2.pem rename to spring-boot-project/spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/ssl/key2.pem diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java index b5ad0b5b46ea..441d9cfb3f89 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java @@ -51,8 +51,9 @@ public class PemSslStoreBundle implements SslStoreBundle { * @param keyStoreDetails the key store details * @param trustStoreDetails the trust store details */ + @SuppressWarnings("removal") public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails) { - this(keyStoreDetails, trustStoreDetails, null, false); + this(keyStoreDetails, trustStoreDetails, null); } /** @@ -66,26 +67,9 @@ public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails */ @Deprecated(since = "3.2.0", forRemoval = true) public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String alias) { - this(keyStoreDetails, trustStoreDetails, alias, false); - } - - /** - * Create a new {@link PemSslStoreBundle} instance. - * @param keyStoreDetails the key store details - * @param trustStoreDetails the trust store details - * @param verifyKeys whether to verify that the private key matches the public key - * @since 3.2.0 - */ - public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, - boolean verifyKeys) { - this(keyStoreDetails, trustStoreDetails, null, verifyKeys); - } - - private PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String alias, - boolean verifyKeys) { try { - this.keyStore = createKeyStore("key", PemSslStore.load(keyStoreDetails), alias, verifyKeys); - this.trustStore = createKeyStore("trust", PemSslStore.load(trustStoreDetails), alias, verifyKeys); + this.keyStore = createKeyStore("key", PemSslStore.load(keyStoreDetails), alias); + this.trustStore = createKeyStore("trust", PemSslStore.load(trustStoreDetails), alias); } catch (IOException ex) { throw new UncheckedIOException(ex); @@ -96,13 +80,15 @@ private PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails * Create a new {@link PemSslStoreBundle} instance. * @param pemKeyStore the PEM key store * @param pemTrustStore the PEM trust store - * @param alias the alias to use or {@code null} to use a default alias - * @param verifyKeys whether to verify that the private key matches the public key * @since 3.2.0 */ - public PemSslStoreBundle(PemSslStore pemKeyStore, PemSslStore pemTrustStore, String alias, boolean verifyKeys) { - this.keyStore = createKeyStore("key", pemKeyStore, alias, verifyKeys); - this.trustStore = createKeyStore("trust", pemTrustStore, alias, verifyKeys); + public PemSslStoreBundle(PemSslStore pemKeyStore, PemSslStore pemTrustStore) { + this(pemKeyStore, pemTrustStore, null); + } + + private PemSslStoreBundle(PemSslStore pemKeyStore, PemSslStore pemTrustStore, String alias) { + this.keyStore = createKeyStore("key", pemKeyStore, alias); + this.trustStore = createKeyStore("trust", pemTrustStore, alias); } @Override @@ -120,7 +106,7 @@ public KeyStore getTrustStore() { return this.trustStore; } - private static KeyStore createKeyStore(String name, PemSslStore pemSslStore, String alias, boolean verifyKeys) { + private static KeyStore createKeyStore(String name, PemSslStore pemSslStore, String alias) { if (pemSslStore == null) { return null; } @@ -132,9 +118,6 @@ private static KeyStore createKeyStore(String name, PemSslStore pemSslStore, Str List certificates = pemSslStore.certificates(); PrivateKey privateKey = pemSslStore.privateKey(); if (privateKey != null) { - if (verifyKeys) { - verifyKeys(privateKey, certificates); - } addPrivateKey(store, privateKey, alias, pemSslStore.password(), certificates); } else { @@ -154,18 +137,6 @@ private static KeyStore createKeyStore(String type) return store; } - private static void verifyKeys(PrivateKey privateKey, List certificateChain) { - KeyVerifier keyVerifier = new KeyVerifier(); - // Key should match one of the certificates - for (X509Certificate certificate : certificateChain) { - KeyVerifier.Result result = keyVerifier.matches(privateKey, certificate.getPublicKey()); - if (result == KeyVerifier.Result.YES) { - return; - } - } - throw new IllegalStateException("Private key matches none of the certificates in the chain"); - } - private static void addPrivateKey(KeyStore keyStore, PrivateKey privateKey, String alias, String keyPassword, List certificateChain) throws KeyStoreException { keyStore.setKeyEntry(alias, privateKey, (keyPassword != null) ? keyPassword.toCharArray() : null, diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java index c6759036d357..b34901931062 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemSslStoreBundleTests.java @@ -27,7 +27,6 @@ import org.springframework.util.function.ThrowingConsumer; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link PemSslStoreBundle}. @@ -206,43 +205,12 @@ void createWithDetailsWhenHasKeyStoreDetailsAndTrustStoreDetailsAndKeyPassword() assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("tsa", "tss".toCharArray())); } - @Test - void shouldVerifyKeysIfEnabled() { - PemSslStoreDetails keyStoreDetails = PemSslStoreDetails - .forCertificate("classpath:org/springframework/boot/ssl/pem/key1.crt") - .withPrivateKey("classpath:org/springframework/boot/ssl/pem/key1.pem") - .withAlias("test-alias") - .withPassword("keysecret"); - PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, null, true); - assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias", "keysecret".toCharArray())); - } - - @Test - void shouldVerifyKeysIfEnabledAndCertificateChainIsUsed() { - PemSslStoreDetails keyStoreDetails = PemSslStoreDetails - .forCertificates("classpath:org/springframework/boot/ssl/pem/key2-chain.crt") - .withPrivateKey("classpath:org/springframework/boot/ssl/pem/key2.pem") - .withAlias("test-alias") - .withPassword("keysecret"); - PemSslStoreBundle bundle = new PemSslStoreBundle(keyStoreDetails, null, true); - assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("test-alias", "keysecret".toCharArray())); - } - - @Test - void shouldFailIfVerifyKeysIsEnabledAndKeysDontMatch() { - PemSslStoreDetails keyStoreDetails = PemSslStoreDetails - .forCertificate("classpath:org/springframework/boot/ssl/pem/key2.crt") - .withPrivateKey("classpath:org/springframework/boot/ssl/pem/key1.pem"); - assertThatIllegalStateException().isThrownBy(() -> new PemSslStoreBundle(keyStoreDetails, null, true)) - .withMessageContaining("Private key matches none of the certificates"); - } - @Test void createWithPemSslStoreCreatesInstance() { List certificates = PemContent.of(CERTIFICATE).getCertificates(); PrivateKey privateKey = PemContent.of(PRIVATE_KEY).getPrivateKey(); PemSslStore pemSslStore = PemSslStore.of(certificates, privateKey); - PemSslStoreBundle bundle = new PemSslStoreBundle(pemSslStore, pemSslStore, null, false); + PemSslStoreBundle bundle = new PemSslStoreBundle(pemSslStore, pemSslStore); assertThat(bundle.getKeyStore()).satisfies(storeContainingCertAndKey("ssl")); assertThat(bundle.getTrustStore()).satisfies(storeContainingCertAndKey("ssl")); } From 5dc5c2a4bc6ec96a73df4ba4fa4309f9855115e2 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 30 Oct 2023 17:16:06 -0700 Subject: [PATCH 0725/1215] Rename `KeyVerifier` to `CertificateMatcher` Rename `KeyVerifier` to `CertificateMatcher` and refactor some of the internals. This commit also adds test helper classes to help simplify some of the tests. See gh-38173 --- .../autoconfigure/ssl/CertificateMatcher.java | 113 ++++++++++++++++ .../boot/autoconfigure/ssl/KeyVerifier.java | 104 --------------- .../ssl/PropertiesSslBundle.java | 17 +-- .../ssl/CertificateMatcherTests.java | 61 +++++++++ .../ssl/CertificateMatchingTest.java | 41 ++++++ .../ssl/CertificateMatchingTestSource.java | 125 ++++++++++++++++++ .../autoconfigure/ssl/KeyVerifierTests.java | 90 ------------- src/checkstyle/checkstyle-suppressions.xml | 1 + 8 files changed, 345 insertions(+), 207 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/CertificateMatcher.java delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/KeyVerifier.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/CertificateMatcherTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/CertificateMatchingTest.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/CertificateMatchingTestSource.java delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/KeyVerifierTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/CertificateMatcher.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/CertificateMatcher.java new file mode 100644 index 000000000000..343305fd2894 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/CertificateMatcher.java @@ -0,0 +1,113 @@ +/* + * Copyright 2012-2023 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.ssl; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.Certificate; +import java.util.List; +import java.util.Objects; + +/** + * Helper used to match certificates against a {@link PrivateKey}. + * + * @author Moritz Halbritter + * @author Phillip Webb + */ +class CertificateMatcher { + + private static final byte[] DATA = new byte[256]; + static { + for (int i = 0; i < DATA.length; i++) { + DATA[i] = (byte) i; + } + } + + private final PrivateKey privateKey; + + private final Signature signature; + + private final byte[] generatedSignature; + + CertificateMatcher(PrivateKey privateKey) { + this.privateKey = privateKey; + this.signature = createSignature(privateKey); + this.generatedSignature = sign(this.signature, privateKey); + } + + private Signature createSignature(PrivateKey privateKey) { + try { + String algorithm = getSignatureAlgorithm(this.privateKey); + return (algorithm != null) ? Signature.getInstance(algorithm) : null; + } + catch (NoSuchAlgorithmException ex) { + return null; + } + } + + private static String getSignatureAlgorithm(PrivateKey privateKey) { + // https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#signature-algorithms + // https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#keypairgenerator-algorithms + return switch (privateKey.getAlgorithm()) { + case "RSA" -> "SHA256withRSA"; + case "DSA" -> "SHA256withDSA"; + case "EC" -> "SHA256withECDSA"; + case "EdDSA" -> "EdDSA"; + default -> null; + }; + } + + boolean matchesAny(List certificates) { + return (this.generatedSignature != null) && certificates.stream().anyMatch(this::matches); + } + + boolean matches(Certificate certificate) { + return matches(certificate.getPublicKey()); + } + + private boolean matches(PublicKey publicKey) { + return (this.generatedSignature != null) + && Objects.equals(this.privateKey.getAlgorithm(), publicKey.getAlgorithm()) && verify(publicKey); + } + + private boolean verify(PublicKey publicKey) { + try { + this.signature.initVerify(publicKey); + this.signature.update(DATA); + return this.signature.verify(this.generatedSignature); + } + catch (InvalidKeyException | SignatureException ex) { + return false; + } + } + + private static byte[] sign(Signature signature, PrivateKey privateKey) { + try { + signature.initSign(privateKey); + signature.update(DATA); + return signature.sign(); + } + catch (InvalidKeyException | SignatureException ex) { + return null; + } + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/KeyVerifier.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/KeyVerifier.java deleted file mode 100644 index 65d68134e06a..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/KeyVerifier.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2012-2023 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.ssl; - -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.Signature; -import java.security.SignatureException; - -/** - * Performs checks on keys, e.g., if a public key and a private key belong together. - * - * @author Moritz Halbritter - */ -class KeyVerifier { - - private static final byte[] DATA = "Just some piece of data which gets signed".getBytes(StandardCharsets.UTF_8); - - /** - * Checks if the given private key belongs to the given public key. - * @param privateKey the private key - * @param publicKey the public key - * @return whether the keys belong together - */ - Result matches(PrivateKey privateKey, PublicKey publicKey) { - try { - if (!privateKey.getAlgorithm().equals(publicKey.getAlgorithm())) { - // Keys are of different type - return Result.NO; - } - String algorithm = getSignatureAlgorithm(privateKey.getAlgorithm()); - if (algorithm == null) { - return Result.UNKNOWN; - } - byte[] signature = createSignature(privateKey, algorithm); - return verifySignature(publicKey, algorithm, signature); - } - catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException ex) { - return Result.UNKNOWN; - } - } - - private static byte[] createSignature(PrivateKey privateKey, String algorithm) - throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { - Signature signer = Signature.getInstance(algorithm); - signer.initSign(privateKey); - signer.update(DATA); - return signer.sign(); - } - - private static Result verifySignature(PublicKey publicKey, String algorithm, byte[] signature) - throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { - Signature verifier = Signature.getInstance(algorithm); - verifier.initVerify(publicKey); - verifier.update(DATA); - try { - if (verifier.verify(signature)) { - return Result.YES; - } - else { - return Result.NO; - } - } - catch (SignatureException ex) { - return Result.NO; - } - } - - private static String getSignatureAlgorithm(String keyAlgorithm) { - // https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#signature-algorithms - // https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#keypairgenerator-algorithms - return switch (keyAlgorithm) { - case "RSA" -> "SHA256withRSA"; - case "DSA" -> "SHA256withDSA"; - case "EC" -> "SHA256withECDSA"; - case "EdDSA" -> "EdDSA"; - default -> null; - }; - } - - enum Result { - - YES, NO, UNKNOWN - - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java index 12a1847b24c5..184c31b17192 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java @@ -18,7 +18,6 @@ import java.io.IOException; import java.io.UncheckedIOException; -import java.security.cert.X509Certificate; import org.springframework.boot.autoconfigure.ssl.SslBundleProperties.Key; import org.springframework.boot.ssl.SslBundle; @@ -31,6 +30,7 @@ import org.springframework.boot.ssl.pem.PemSslStore; import org.springframework.boot.ssl.pem.PemSslStoreBundle; import org.springframework.boot.ssl.pem.PemSslStoreDetails; +import org.springframework.util.Assert; /** * {@link SslBundle} backed by {@link JksSslBundleProperties} or @@ -122,7 +122,9 @@ private static PemSslStore asPemSslStore(PemSslBundleProperties.Store properties PemSslStoreDetails details = asStoreDetails(properties, alias); PemSslStore pemSslStore = PemSslStore.load(details); if (properties.isVerifyKeys()) { - verifyPemSslStoreKeys(pemSslStore); + CertificateMatcher certificateMatcher = new CertificateMatcher(pemSslStore.privateKey()); + Assert.state(certificateMatcher.matchesAny(pemSslStore.certificates()), + "Private key matches none of the certificates in the chain"); } return pemSslStore; } @@ -131,17 +133,6 @@ private static PemSslStore asPemSslStore(PemSslBundleProperties.Store properties } } - private static void verifyPemSslStoreKeys(PemSslStore pemSslStore) { - KeyVerifier keyVerifier = new KeyVerifier(); - for (X509Certificate certificate : pemSslStore.certificates()) { - KeyVerifier.Result result = keyVerifier.matches(pemSslStore.privateKey(), certificate.getPublicKey()); - if (result == KeyVerifier.Result.YES) { - return; - } - } - throw new IllegalStateException("Private key matches none of the certificates in the chain"); - } - private static PemSslStoreDetails asStoreDetails(PemSslBundleProperties.Store properties, String alias) { return new PemSslStoreDetails(properties.getType(), alias, null, properties.getCertificate(), properties.getPrivateKey(), properties.getPrivateKeyPassword()); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/CertificateMatcherTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/CertificateMatcherTests.java new file mode 100644 index 000000000000..c1516bdf6358 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/CertificateMatcherTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2023 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.ssl; + +import java.security.cert.Certificate; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CertificateMatcher}. + * + * @author Moritz Halbritter + * @author Phillip Webb + */ +class CertificateMatcherTests { + + @CertificateMatchingTest + void matchesWhenMatchReturnsTrue(CertificateMatchingTestSource source) { + CertificateMatcher matcher = new CertificateMatcher(source.privateKey()); + assertThat(matcher.matches(source.matchingCertificate())).isTrue(); + } + + @CertificateMatchingTest + void matchesWhenNoMatchReturnsFalse(CertificateMatchingTestSource source) { + CertificateMatcher matcher = new CertificateMatcher(source.privateKey()); + for (Certificate nonMatchingCertificate : source.nonMatchingCertificates()) { + assertThat(matcher.matches(nonMatchingCertificate)).isFalse(); + } + } + + @CertificateMatchingTest + void matchesAnyWhenNoneMatchReturnsFalse(CertificateMatchingTestSource source) { + CertificateMatcher matcher = new CertificateMatcher(source.privateKey()); + assertThat(matcher.matchesAny(source.nonMatchingCertificates())).isFalse(); + } + + @CertificateMatchingTest + void matchesAnyWhenOneMatchesReturnsTrue(CertificateMatchingTestSource source) { + CertificateMatcher matcher = new CertificateMatcher(source.privateKey()); + List certificates = new ArrayList<>(source.nonMatchingCertificates()); + certificates.add(source.matchingCertificate()); + assertThat(matcher.matchesAny(certificates)).isTrue(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/CertificateMatchingTest.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/CertificateMatchingTest.java new file mode 100644 index 000000000000..fcf5e39d6534 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/CertificateMatchingTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2023 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.ssl; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Annotation for a {@code ParameterizedTest @ParameterizedTest} with a + * {@link CertificateMatchingTestSource} parameter. + * + * @author Phillip Webb + */ +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ParameterizedTest(name = "{0}") +@MethodSource("org.springframework.boot.autoconfigure.ssl.CertificateMatchingTestSource#create") +public @interface CertificateMatchingTest { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/CertificateMatchingTestSource.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/CertificateMatchingTestSource.java new file mode 100644 index 000000000000..e04f5651fa0a --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/CertificateMatchingTestSource.java @@ -0,0 +1,125 @@ +/* + * Copyright 2012-2023 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.ssl; + +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.NamedParameterSpec; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Source used with {@link CertificateMatchingTest @CertificateMatchingTest} annotated + * tests that provides access to useful test material. + * + * @param algorithm the algorithm + * @param privateKey the private key to use for matching + * @param matchingCertificate a certificate that matches the private key + * @param nonMatchingCertificates a list of certificate that do not match the private key + * @param nonMatchingPrivateKeys a list of private keys that do not match the certificate + * @author Moritz Halbritter + * @author Phillip Webb + */ +record CertificateMatchingTestSource(CertificateMatchingTestSource.Algorithm algorithm, PrivateKey privateKey, + X509Certificate matchingCertificate, List nonMatchingCertificates, + List nonMatchingPrivateKeys) { + + private static final List ALGORITHMS; + static { + List algorithms = new ArrayList<>(); + Stream.of("RSA", "DSA", "ed25519", "ed448").map(Algorithm::of).forEach(algorithms::add); + Stream.of("secp256r1", "secp521r1").map(Algorithm::ec).forEach(algorithms::add); + ALGORITHMS = List.copyOf(algorithms); + } + + CertificateMatchingTestSource(Algorithm algorithm, KeyPair matchingKeyPair, List nonMatchingKeyPairs) { + this(algorithm, matchingKeyPair.getPrivate(), asCertificate(matchingKeyPair), + nonMatchingKeyPairs.stream().map(CertificateMatchingTestSource::asCertificate).toList(), + nonMatchingKeyPairs.stream().map(KeyPair::getPrivate).toList()); + } + + private static X509Certificate asCertificate(KeyPair keyPair) { + X509Certificate certificate = mock(X509Certificate.class); + given(certificate.getPublicKey()).willReturn(keyPair.getPublic()); + return certificate; + } + + @Override + public String toString() { + return this.algorithm.toString(); + } + + static List create() + throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + Map keyPairs = new LinkedHashMap<>(); + for (Algorithm algorithm : ALGORITHMS) { + keyPairs.put(algorithm, algorithm.generateKeyPair()); + } + List parameters = new ArrayList<>(); + keyPairs.forEach((algorith, matchingKeyPair) -> { + List nonMatchingKeyPairs = new ArrayList<>(keyPairs.values()); + nonMatchingKeyPairs.remove(matchingKeyPair); + parameters.add(new CertificateMatchingTestSource(algorith, matchingKeyPair, nonMatchingKeyPairs)); + }); + return List.copyOf(parameters); + } + + /** + * An individual algorithm. + * + * @param name the algorithm name + * @param spec the algorithm spec or {@code null} + */ + record Algorithm(String name, AlgorithmParameterSpec spec) { + + KeyPair generateKeyPair() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + KeyPairGenerator generator = KeyPairGenerator.getInstance(this.name); + if (this.spec != null) { + generator.initialize(this.spec); + } + return generator.generateKeyPair(); + } + + @Override + public String toString() { + String spec = (this.spec instanceof NamedParameterSpec namedSpec) ? namedSpec.getName() : ""; + return this.name + ((!spec.isEmpty()) ? ":" + spec : ""); + } + + static Algorithm of(String name) { + return new Algorithm(name, null); + } + + static Algorithm ec(String curve) { + return new Algorithm("EC", new ECGenParameterSpec(curve)); + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/KeyVerifierTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/KeyVerifierTests.java deleted file mode 100644 index 269b824a6782..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/KeyVerifierTests.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2012-2023 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.ssl; - -import java.security.InvalidAlgorithmParameterException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.spec.AlgorithmParameterSpec; -import java.security.spec.ECGenParameterSpec; -import java.util.LinkedList; -import java.util.List; -import java.util.stream.Stream; - -import org.junit.jupiter.api.Named; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import org.springframework.boot.autoconfigure.ssl.KeyVerifier.Result; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link KeyVerifier}. - * - * @author Moritz Halbritter - */ -class KeyVerifierTests { - - private static final List ALGORITHMS = List.of(Algorithm.of("RSA"), Algorithm.of("DSA"), - Algorithm.of("ed25519"), Algorithm.of("ed448"), Algorithm.ec("secp256r1"), Algorithm.ec("secp521r1")); - - private final KeyVerifier keyVerifier = new KeyVerifier(); - - @ParameterizedTest(name = "{0}") - @MethodSource("arguments") - void test(PrivateKey privateKey, PublicKey publicKey, List invalidPublicKeys) { - assertThat(this.keyVerifier.matches(privateKey, publicKey)).isEqualTo(Result.YES); - for (PublicKey invalidPublicKey : invalidPublicKeys) { - assertThat(this.keyVerifier.matches(privateKey, invalidPublicKey)).isEqualTo(Result.NO); - } - } - - static Stream arguments() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { - List keyPairs = new LinkedList<>(); - for (Algorithm algorithm : ALGORITHMS) { - KeyPairGenerator generator = KeyPairGenerator.getInstance(algorithm.name()); - if (algorithm.spec() != null) { - generator.initialize(algorithm.spec()); - } - keyPairs.add(generator.generateKeyPair()); - keyPairs.add(generator.generateKeyPair()); - } - return keyPairs.stream() - .map((kp) -> Arguments.arguments(Named.named(kp.getPrivate().getAlgorithm(), kp.getPrivate()), - kp.getPublic(), without(keyPairs, kp).map(KeyPair::getPublic).toList())); - } - - private static Stream without(List keyPairs, KeyPair without) { - return keyPairs.stream().filter((kp) -> !kp.equals(without)); - } - - private record Algorithm(String name, AlgorithmParameterSpec spec) { - static Algorithm of(String name) { - return new Algorithm(name, null); - } - - static Algorithm ec(String curve) { - return new Algorithm("EC", new ECGenParameterSpec(curve)); - } - } - -} diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index bc4b1cae64ca..fdec73a16394 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -81,4 +81,5 @@ + From 30a7426e8633316021483d85d885cec345955906 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 30 Oct 2023 17:42:10 -0700 Subject: [PATCH 0726/1215] Apply key property to the keystore and not to the truststore Update `PropertiesSslBundle` so that key properties are now only applied to the keystore and not the truststore. Closes gh-38125 --- .../autoconfigure/ssl/PropertiesSslBundle.java | 18 +++++++++++------- .../ssl/PropertiesSslBundleTests.java | 4 ++-- .../boot/web/server/WebServerSslBundle.java | 3 +-- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java index 184c31b17192..39512f2b3e82 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java @@ -112,14 +112,18 @@ public static SslBundle get(JksSslBundleProperties properties) { } private static SslStoreBundle asSslStoreBundle(PemSslBundleProperties properties) { - PemSslStore keyStore = asPemSslStore(properties.getKeystore(), properties.getKey().getAlias()); - PemSslStore trustStore = asPemSslStore(properties.getTruststore(), properties.getKey().getAlias()); + PemSslStore keyStore = asPemSslStore(properties.getKeystore()); + if (keyStore != null) { + keyStore = keyStore.withAlias(properties.getKey().getAlias()) + .withPassword(properties.getKey().getPassword()); + } + PemSslStore trustStore = asPemSslStore(properties.getTruststore()); return new PemSslStoreBundle(keyStore, trustStore); } - private static PemSslStore asPemSslStore(PemSslBundleProperties.Store properties, String alias) { + private static PemSslStore asPemSslStore(PemSslBundleProperties.Store properties) { try { - PemSslStoreDetails details = asStoreDetails(properties, alias); + PemSslStoreDetails details = asStoreDetails(properties); PemSslStore pemSslStore = PemSslStore.load(details); if (properties.isVerifyKeys()) { CertificateMatcher certificateMatcher = new CertificateMatcher(pemSslStore.privateKey()); @@ -133,9 +137,9 @@ private static PemSslStore asPemSslStore(PemSslBundleProperties.Store properties } } - private static PemSslStoreDetails asStoreDetails(PemSslBundleProperties.Store properties, String alias) { - return new PemSslStoreDetails(properties.getType(), alias, null, properties.getCertificate(), - properties.getPrivateKey(), properties.getPrivateKeyPassword()); + private static PemSslStoreDetails asStoreDetails(PemSslBundleProperties.Store properties) { + return new PemSslStoreDetails(properties.getType(), properties.getCertificate(), properties.getPrivateKey(), + properties.getPrivateKeyPassword()); } private static SslStoreBundle asSslStoreBundle(JksSslBundleProperties properties) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundleTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundleTests.java index 6143a7919d34..52447f47b626 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundleTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundleTests.java @@ -66,10 +66,10 @@ void pemPropertiesAreMappedToSslBundle() throws Exception { Certificate certificate = sslBundle.getStores().getKeyStore().getCertificate("alias"); assertThat(certificate).isNotNull(); assertThat(certificate.getType()).isEqualTo("X.509"); - Key key = sslBundle.getStores().getKeyStore().getKey("alias", null); + Key key = sslBundle.getStores().getKeyStore().getKey("alias", "secret".toCharArray()); assertThat(key).isNotNull(); assertThat(key.getAlgorithm()).isEqualTo("RSA"); - certificate = sslBundle.getStores().getTrustStore().getCertificate("alias"); + certificate = sslBundle.getStores().getTrustStore().getCertificate("ssl"); assertThat(certificate).isNotNull(); assertThat(certificate.getType()).isEqualTo("X.509"); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java index c9f989bfae02..adcff6722b0e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java @@ -65,8 +65,7 @@ private static SslStoreBundle createPemStoreBundle(Ssl ssl) { ssl.getCertificatePrivateKey()) .withAlias(ssl.getKeyAlias()); PemSslStoreDetails trustStoreDetails = new PemSslStoreDetails(ssl.getTrustStoreType(), - ssl.getTrustCertificate(), ssl.getTrustCertificatePrivateKey()) - .withAlias(ssl.getKeyAlias()); + ssl.getTrustCertificate(), ssl.getTrustCertificatePrivateKey()); return new PemSslStoreBundle(keyStoreDetails, trustStoreDetails); } From 9b71ef411441d7cb34d02fce61842b62870bae79 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 1 Nov 2023 12:49:44 -0700 Subject: [PATCH 0727/1215] Polish and refactor some SSL internals Polish and refactor some of the internal SSL code to make it easier to add additional functionality in the future. --- .../ssl/BundleContentProperty.java | 77 +++++++++++++++ .../ssl/PemSslBundleProperties.java | 2 +- .../ssl/PropertiesSslBundle.java | 60 ++++++------ .../ssl/SslPropertiesBundleRegistrar.java | 97 ++++++++----------- .../ssl/BundleContentPropertyTests.java | 87 +++++++++++++++++ .../SslPropertiesBundleRegistrarTests.java | 16 ++- .../boot/ssl/pem/PemContent.java | 4 +- .../boot/ssl/pem/PemSslStoreDetails.java | 45 +++++---- 8 files changed, 270 insertions(+), 118 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/BundleContentProperty.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/BundleContentPropertyTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/BundleContentProperty.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/BundleContentProperty.java new file mode 100644 index 000000000000..abaace466f4f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/BundleContentProperty.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2023 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.ssl; + +import java.io.FileNotFoundException; +import java.net.URL; +import java.nio.file.Path; + +import org.springframework.boot.ssl.pem.PemContent; +import org.springframework.util.Assert; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; + +/** + * Helper utility to manage a single bundle content configuration property. May possibly + * contain PEM content, a location or a directory search pattern. + * + * @param name the configuration property name (excluding any prefix) + * @param value the configuration property value + * @author Phillip Webb + */ +record BundleContentProperty(String name, String value) { + + /** + * Return if the property value is PEM content. + * @return if the value is PEM content + */ + boolean isPemContent() { + return PemContent.isPresentInText(this.value); + } + + /** + * Return if there is any property value present. + * @return if the value is present + */ + boolean hasValue() { + return StringUtils.hasText(this.value); + } + + private URL toUrl() throws FileNotFoundException { + Assert.state(!isPemContent(), "Value contains PEM content"); + return ResourceUtils.getURL(this.value); + } + + Path toWatchPath() { + return toPath(); + } + + private Path toPath() { + try { + URL url = toUrl(); + Assert.state(isFileUrl(url), () -> "Vaule '%s' is not a file URL".formatted(url)); + return Path.of(url.toURI()).toAbsolutePath(); + } + catch (Exception ex) { + throw new IllegalStateException("Unable to convert '%s' property to a path".formatted(this.name), ex); + } + } + + private boolean isFileUrl(URL url) { + return "file".equalsIgnoreCase(url.getProtocol()); + } +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemSslBundleProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemSslBundleProperties.java index 7870c6b05925..beb58d87dc91 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemSslBundleProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PemSslBundleProperties.java @@ -58,7 +58,7 @@ public static class Store { private String type; /** - * Location or content of the certificate in PEM format. + * Location or content of the certificate or certificate chain in PEM format. */ private String certificate; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java index 39512f2b3e82..d8cfd084ab47 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java @@ -99,49 +99,47 @@ public SslManagerBundle getManagers() { * @return an {@link SslBundle} instance */ public static SslBundle get(PemSslBundleProperties properties) { - return new PropertiesSslBundle(asSslStoreBundle(properties), properties); - } - - /** - * Get an {@link SslBundle} for the given {@link JksSslBundleProperties}. - * @param properties the source properties - * @return an {@link SslBundle} instance - */ - public static SslBundle get(JksSslBundleProperties properties) { - return new PropertiesSslBundle(asSslStoreBundle(properties), properties); - } - - private static SslStoreBundle asSslStoreBundle(PemSslBundleProperties properties) { - PemSslStore keyStore = asPemSslStore(properties.getKeystore()); - if (keyStore != null) { - keyStore = keyStore.withAlias(properties.getKey().getAlias()) - .withPassword(properties.getKey().getPassword()); - } - PemSslStore trustStore = asPemSslStore(properties.getTruststore()); - return new PemSslStoreBundle(keyStore, trustStore); - } - - private static PemSslStore asPemSslStore(PemSslBundleProperties.Store properties) { try { - PemSslStoreDetails details = asStoreDetails(properties); - PemSslStore pemSslStore = PemSslStore.load(details); - if (properties.isVerifyKeys()) { - CertificateMatcher certificateMatcher = new CertificateMatcher(pemSslStore.privateKey()); - Assert.state(certificateMatcher.matchesAny(pemSslStore.certificates()), - "Private key matches none of the certificates in the chain"); + PemSslStore keyStore = getPemSslStore("keystore", properties.getKeystore()); + if (keyStore != null) { + keyStore = keyStore.withAlias(properties.getKey().getAlias()) + .withPassword(properties.getKey().getPassword()); } - return pemSslStore; + PemSslStore trustStore = getPemSslStore("truststore", properties.getTruststore()); + SslStoreBundle storeBundle = new PemSslStoreBundle(keyStore, trustStore); + return new PropertiesSslBundle(storeBundle, properties); } catch (IOException ex) { throw new UncheckedIOException(ex); } } - private static PemSslStoreDetails asStoreDetails(PemSslBundleProperties.Store properties) { + private static PemSslStore getPemSslStore(String propertyName, PemSslBundleProperties.Store properties) + throws IOException { + PemSslStore pemSslStore = PemSslStore.load(asPemSslStoreDetails(properties)); + if (properties.isVerifyKeys()) { + CertificateMatcher certificateMatcher = new CertificateMatcher(pemSslStore.privateKey()); + Assert.state(certificateMatcher.matchesAny(pemSslStore.certificates()), + "Private key matches none of the certificates in the chain"); + } + return pemSslStore; + } + + private static PemSslStoreDetails asPemSslStoreDetails(PemSslBundleProperties.Store properties) { return new PemSslStoreDetails(properties.getType(), properties.getCertificate(), properties.getPrivateKey(), properties.getPrivateKeyPassword()); } + /** + * Get an {@link SslBundle} for the given {@link JksSslBundleProperties}. + * @param properties the source properties + * @return an {@link SslBundle} instance + */ + public static SslBundle get(JksSslBundleProperties properties) { + SslStoreBundle storeBundle = asSslStoreBundle(properties); + return new PropertiesSslBundle(storeBundle, properties); + } + private static SslStoreBundle asSslStoreBundle(JksSslBundleProperties properties) { JksSslStoreDetails keyStoreDetails = asStoreDetails(properties.getKeystore()); JksSslStoreDetails trustStoreDetails = asStoreDetails(properties.getTruststore()); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java index 810ff772a39f..583702c82ce2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrar.java @@ -16,20 +16,17 @@ package org.springframework.boot.autoconfigure.ssl; -import java.net.URL; import java.nio.file.Path; -import java.util.LinkedHashSet; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; -import java.util.regex.Pattern; +import java.util.function.Supplier; import java.util.stream.Collectors; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundleRegistry; -import org.springframework.util.Assert; -import org.springframework.util.ResourceUtils; -import org.springframework.util.StringUtils; /** * A {@link SslBundleRegistrar} that registers SSL bundles based @@ -41,8 +38,6 @@ */ class SslPropertiesBundleRegistrar implements SslBundleRegistrar { - private static final Pattern PEM_CONTENT = Pattern.compile("-+BEGIN\\s+[^-]*-+", Pattern.CASE_INSENSITIVE); - private final SslProperties.Bundles properties; private final FileWatcher fileWatcher; @@ -54,70 +49,58 @@ class SslPropertiesBundleRegistrar implements SslBundleRegistrar { @Override public void registerBundles(SslBundleRegistry registry) { - registerBundles(registry, this.properties.getPem(), PropertiesSslBundle::get, this::getLocations); - registerBundles(registry, this.properties.getJks(), PropertiesSslBundle::get, this::getLocations); + registerBundles(registry, this.properties.getPem(), PropertiesSslBundle::get, this::watchedPemPaths); + registerBundles(registry, this.properties.getJks(), PropertiesSslBundle::get, this::watchedJksPaths); } private

    void registerBundles(SslBundleRegistry registry, Map properties, - Function bundleFactory, Function> locationsSupplier) { + Function bundleFactory, Function> watchedPaths) { properties.forEach((bundleName, bundleProperties) -> { - SslBundle bundle = bundleFactory.apply(bundleProperties); - registry.registerBundle(bundleName, bundle); - if (bundleProperties.isReloadOnUpdate()) { - Set paths = locationsSupplier.apply(bundleProperties) - .stream() - .filter(Location::hasValue) - .map((location) -> toPath(bundleName, location)) - .collect(Collectors.toSet()); - this.fileWatcher.watch(paths, - () -> registry.updateBundle(bundleName, bundleFactory.apply(bundleProperties))); + Supplier bundleSupplier = () -> bundleFactory.apply(bundleProperties); + try { + registry.registerBundle(bundleName, bundleSupplier.get()); + if (bundleProperties.isReloadOnUpdate()) { + Supplier> pathsSupplier = () -> watchedPaths.apply(bundleProperties); + watchForUpdates(registry, bundleName, pathsSupplier, bundleSupplier); + } + } + catch (IllegalStateException ex) { + throw new IllegalStateException("Unable to register SSL bundle '%s'".formatted(bundleName), ex); } }); } - private Set getLocations(JksSslBundleProperties properties) { - JksSslBundleProperties.Store keystore = properties.getKeystore(); - JksSslBundleProperties.Store truststore = properties.getTruststore(); - Set locations = new LinkedHashSet<>(); - locations.add(new Location("keystore.location", keystore.getLocation())); - locations.add(new Location("truststore.location", truststore.getLocation())); - return locations; - } - - private Set getLocations(PemSslBundleProperties properties) { - PemSslBundleProperties.Store keystore = properties.getKeystore(); - PemSslBundleProperties.Store truststore = properties.getTruststore(); - Set locations = new LinkedHashSet<>(); - locations.add(new Location("keystore.private-key", keystore.getPrivateKey())); - locations.add(new Location("keystore.certificate", keystore.getCertificate())); - locations.add(new Location("truststore.private-key", truststore.getPrivateKey())); - locations.add(new Location("truststore.certificate", truststore.getCertificate())); - return locations; - } - - private Path toPath(String bundleName, Location watchableLocation) { - String value = watchableLocation.value(); - String field = watchableLocation.field(); - Assert.state(!PEM_CONTENT.matcher(value).find(), - () -> "SSL bundle '%s' '%s' is not a URL and can't be watched".formatted(bundleName, field)); + private void watchForUpdates(SslBundleRegistry registry, String bundleName, Supplier> pathsSupplier, + Supplier bundleSupplier) { try { - URL url = ResourceUtils.getURL(value); - Assert.state("file".equalsIgnoreCase(url.getProtocol()), - () -> "SSL bundle '%s' '%s' URL '%s' doesn't point to a file".formatted(bundleName, field, url)); - return Path.of(url.toURI()).toAbsolutePath(); + this.fileWatcher.watch(pathsSupplier.get(), () -> registry.updateBundle(bundleName, bundleSupplier.get())); } - catch (Exception ex) { - throw new RuntimeException( - "SSL bundle '%s' '%s' location '%s' cannot be watched".formatted(bundleName, field, value), ex); + catch (RuntimeException ex) { + throw new IllegalStateException("Unable to watch for reload on update", ex); } } - private record Location(String field, String value) { + private Set watchedJksPaths(JksSslBundleProperties properties) { + List watched = new ArrayList<>(); + watched.add(new BundleContentProperty("keystore.location", properties.getKeystore().getLocation())); + watched.add(new BundleContentProperty("truststore.location", properties.getTruststore().getLocation())); + return watchedPaths(watched); + } - boolean hasValue() { - return StringUtils.hasText(this.value); - } + private Set watchedPemPaths(PemSslBundleProperties properties) { + List watched = new ArrayList<>(); + watched.add(new BundleContentProperty("keystore.private-key", properties.getKeystore().getPrivateKey())); + watched.add(new BundleContentProperty("keystore.certificate", properties.getKeystore().getCertificate())); + watched.add(new BundleContentProperty("truststore.private-key", properties.getTruststore().getPrivateKey())); + watched.add(new BundleContentProperty("truststore.certificate", properties.getTruststore().getCertificate())); + return watchedPaths(watched); + } + private Set watchedPaths(List properties) { + return properties.stream() + .filter(BundleContentProperty::hasValue) + .map(BundleContentProperty::toWatchPath) + .collect(Collectors.toSet()); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/BundleContentPropertyTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/BundleContentPropertyTests.java new file mode 100644 index 000000000000..7ff334f08de6 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/BundleContentPropertyTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2012-2023 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.ssl; + +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link BundleContentProperty}. + * + * @author Moritz Halbritter + * @author Phillip Webb + */ +class BundleContentPropertyTests { + + private static final String PEM_TEXT = """ + -----BEGIN CERTIFICATE----- + -----END CERTIFICATE----- + """; + + @TempDir + Path temp; + + @Test + void isPemContentWhenValueIsPemTextReturnsTrue() { + BundleContentProperty property = new BundleContentProperty("name", PEM_TEXT); + assertThat(property.isPemContent()).isTrue(); + } + + @Test + void isPemContentWhenValueIsNotPemTextReturnsFalse() { + BundleContentProperty property = new BundleContentProperty("name", "file.pem"); + assertThat(property.isPemContent()).isFalse(); + } + + @Test + void hasValueWhenHasValueReturnsTrue() { + BundleContentProperty property = new BundleContentProperty("name", "file.pem"); + assertThat(property.hasValue()).isTrue(); + } + + @Test + void hasValueWhenHasNullValueReturnsFalse() { + BundleContentProperty property = new BundleContentProperty("name", null); + assertThat(property.hasValue()).isFalse(); + } + + @Test + void hasValueWhenHasEmptyValueReturnsFalse() { + BundleContentProperty property = new BundleContentProperty("name", ""); + assertThat(property.hasValue()).isFalse(); + } + + @Test + void toWatchPathWhenNotPathThrowsException() { + BundleContentProperty property = new BundleContentProperty("name", PEM_TEXT); + assertThatIllegalStateException().isThrownBy(property::toWatchPath) + .withMessage("Unable to convert 'name' property to a path"); + } + + @Test + void toWatchPathWhenPathReturnsPath() { + Path file = this.temp.toAbsolutePath().resolve("file.txt"); + BundleContentProperty property = new BundleContentProperty("name", file.toString()); + assertThat(property.toWatchPath()).isEqualTo(file); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrarTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrarTests.java index 2410b8bbaaef..759bb474609b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrarTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/SslPropertiesBundleRegistrarTests.java @@ -106,7 +106,9 @@ void shouldFailIfPemKeystoreCertificateIsEmbedded() { """.strip()); this.properties.getBundle().getPem().put("bundle1", pem); assertThatIllegalStateException().isThrownBy(() -> this.registrar.registerBundles(this.registry)) - .withMessage("SSL bundle 'bundle1' 'keystore.certificate' is not a URL and can't be watched"); + .withMessageContaining("Unable to register SSL bundle 'bundle1'") + .havingCause() + .withMessage("Unable to watch for reload on update"); } @Test @@ -121,7 +123,9 @@ void shouldFailIfPemKeystorePrivateKeyIsEmbedded() { """.strip()); this.properties.getBundle().getPem().put("bundle1", pem); assertThatIllegalStateException().isThrownBy(() -> this.registrar.registerBundles(this.registry)) - .withMessage("SSL bundle 'bundle1' 'keystore.private-key' is not a URL and can't be watched"); + .withMessageContaining("Unable to register SSL bundle 'bundle1'") + .havingCause() + .withMessage("Unable to watch for reload on update"); } @Test @@ -145,7 +149,9 @@ void shouldFailIfPemTruststoreCertificateIsEmbedded() { """.strip()); this.properties.getBundle().getPem().put("bundle1", pem); assertThatIllegalStateException().isThrownBy(() -> this.registrar.registerBundles(this.registry)) - .withMessage("SSL bundle 'bundle1' 'truststore.certificate' is not a URL and can't be watched"); + .withMessageContaining("Unable to register SSL bundle 'bundle1'") + .havingCause() + .withMessage("Unable to watch for reload on update"); } @Test @@ -160,7 +166,9 @@ void shouldFailIfPemTruststorePrivateKeyIsEmbedded() { """.strip()); this.properties.getBundle().getPem().put("bundle1", pem); assertThatIllegalStateException().isThrownBy(() -> this.registrar.registerBundles(this.registry)) - .withMessage("SSL bundle 'bundle1' 'truststore.private-key' is not a URL and can't be watched"); + .withMessageContaining("Unable to register SSL bundle 'bundle1'") + .havingCause() + .withMessage("Unable to watch for reload on update"); } private void pathEndingWith(Set paths, String... suffixes) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java index 280e7094df20..364a3f745a29 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java @@ -105,8 +105,8 @@ public String toString() { } /** - * Load {@link PemContent} from the given content (either the PEM content itself or - * something that can be loaded by {@link ResourceUtils#getURL}). + * Load {@link PemContent} from the given content (either the PEM content itself or a + * reference to the resource to load). * @param content the content to load * @return a new {@link PemContent} instance * @throws IOException on IO error diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java index 57d6d5db60ce..8f97b2b55adb 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java @@ -18,7 +18,6 @@ import java.security.KeyStore; -import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; /** @@ -30,12 +29,12 @@ * @param password the password used * {@link KeyStore#setKeyEntry(String, java.security.Key, char[], java.security.cert.Certificate[]) * setting key entries} in the {@link KeyStore} - * @param certificates the certificates content (either the PEM content itself or - * something that can be loaded by {@link ResourceUtils#getURL}). When a - * {@link #privateKey() private key} is present this value is treated as a certificate - * chain, otherwise it is treated a list of certificates that should all be registered. - * @param privateKey the private key content (either the PEM content itself or something - * that can be loaded by {@link ResourceUtils#getURL}) + * @param certificates the certificates content (either the PEM content itself or or a + * reference to the resource to load). When a {@link #privateKey() private key} is present + * this value is treated as a certificate chain, otherwise it is treated a list of + * certificates that should all be registered. + * @param privateKey the private key content (either the PEM content itself or a reference + * to the resource to load) * @param privateKeyPassword a password used to decrypt an encrypted private key * @author Scott Frederick * @author Phillip Webb @@ -53,10 +52,10 @@ public record PemSslStoreDetails(String type, String alias, String password, Str * @param password the password used * {@link KeyStore#setKeyEntry(String, java.security.Key, char[], java.security.cert.Certificate[]) * setting key entries} in the {@link KeyStore} - * @param certificates the certificate content (either the PEM content itself or - * something that can be loaded by {@link ResourceUtils#getURL}) - * @param privateKey the private key content (either the PEM content itself or - * something that can be loaded by {@link ResourceUtils#getURL}) + * @param certificates the certificate content (either the PEM content itself or a + * reference to the resource to load) + * @param privateKey the private key content (either the PEM content itself or a + * reference to the resource to load) * @param privateKeyPassword a password used to decrypt an encrypted private key * @since 3.2.0 */ @@ -67,10 +66,10 @@ public record PemSslStoreDetails(String type, String alias, String password, Str * Create a new {@link PemSslStoreDetails} instance. * @param type the key store type, for example {@code JKS} or {@code PKCS11}. A * {@code null} value will use {@link KeyStore#getDefaultType()}). - * @param certificate the certificate content (either the PEM content itself or - * something that can be loaded by {@link ResourceUtils#getURL}) - * @param privateKey the private key content (either the PEM content itself or - * something that can be loaded by {@link ResourceUtils#getURL}) + * @param certificate the certificate content (either the PEM content itself or a + * reference to the resource to load) + * @param privateKey the private key content (either the PEM content itself or a + * reference to the resource to load) * @param privateKeyPassword a password used to decrypt an encrypted private key */ public PemSslStoreDetails(String type, String certificate, String privateKey, String privateKeyPassword) { @@ -81,10 +80,10 @@ public PemSslStoreDetails(String type, String certificate, String privateKey, St * Create a new {@link PemSslStoreDetails} instance. * @param type the key store type, for example {@code JKS} or {@code PKCS11}. A * {@code null} value will use {@link KeyStore#getDefaultType()}). - * @param certificate the certificate content (either the PEM content itself or - * something that can be loaded by {@link ResourceUtils#getURL}) - * @param privateKey the private key content (either the PEM content itself or - * something that can be loaded by {@link ResourceUtils#getURL}) + * @param certificate the certificate content (either the PEM content itself or a + * reference to the resource to load) + * @param privateKey the private key content (either the PEM content itself or a + * reference to the resource to load) */ public PemSslStoreDetails(String type, String certificate, String privateKey) { this(type, certificate, privateKey, null); @@ -155,8 +154,8 @@ private boolean isEmpty(String value) { * certificate. Note: This method doesn't actually check if the provided value * only contains a single certificate. It is functionally equivalent to * {@link #forCertificates(String)}. - * @param certificate the certificate content (either the PEM content itself or - * something that can be loaded by {@link ResourceUtils#getURL}) + * @param certificate the certificate content (either the PEM content itself or a + * reference to the resource to load) * @return a new {@link PemSslStoreDetails} instance. */ public static PemSslStoreDetails forCertificate(String certificate) { @@ -166,8 +165,8 @@ public static PemSslStoreDetails forCertificate(String certificate) { /** * Factory method to create a new {@link PemSslStoreDetails} instance for the given * certificates. - * @param certificates the certificates content (either the PEM content itself or - * something that can be loaded by {@link ResourceUtils#getURL}) + * @param certificates the certificates content (either the PEM content itself or a + * reference to the resource to load) * @return a new {@link PemSslStoreDetails} instance. * @since 3.2.0 */ From 663243e60ce1b5aa322e26158f527aba63389825 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 1 Nov 2023 18:16:38 -0700 Subject: [PATCH 0728/1215] Fix open telemetry container lifecycle issues Mark test as `@DirtiesContext` so that the context is closed before the container. Closes gh-38176 --- ...etricsContainerConnectionDetailsFactoryIntegrationTests.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java index b1194f0ca6b0..57a951b4bc54 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java @@ -40,6 +40,7 @@ import org.springframework.boot.testsupport.testcontainers.DockerImageNames; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; @@ -57,6 +58,7 @@ @TestPropertySource(properties = { "management.otlp.metrics.export.resource-attributes.service.name=test", "management.otlp.metrics.export.step=1s" }) @Testcontainers(disabledWithoutDocker = true) +@DirtiesContext class OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests { private static final String OPENMETRICS_001 = "application/openmetrics-text; version=0.0.1; charset=utf-8"; From d3f177be7136da08afd0af190e916b64faa08ccb Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 2 Nov 2023 08:51:42 +0100 Subject: [PATCH 0729/1215] Polish SSL --- .../boot/autoconfigure/ssl/CertificateMatcher.java | 6 +++++- .../boot/autoconfigure/ssl/PropertiesSslBundle.java | 2 +- .../boot/autoconfigure/ssl/PropertiesSslBundleTests.java | 2 +- .../org/springframework/boot/ssl/pem/PemContent.java | 2 +- .../boot/ssl/pem/PemPrivateKeyParser.java | 7 ++----- .../org/springframework/boot/ssl/pem/PemSslStore.java | 2 +- .../springframework/boot/ssl/pem/PemSslStoreBundle.java | 1 - .../springframework/boot/ssl/pem/PemSslStoreDetails.java | 2 +- .../springframework/boot/ssl/pem/PemContentTests.java | 9 --------- 9 files changed, 12 insertions(+), 21 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/CertificateMatcher.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/CertificateMatcher.java index 343305fd2894..3f25ecc2c0c4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/CertificateMatcher.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/CertificateMatcher.java @@ -26,6 +26,8 @@ import java.util.List; import java.util.Objects; +import org.springframework.util.Assert; + /** * Helper used to match certificates against a {@link PrivateKey}. * @@ -48,14 +50,16 @@ class CertificateMatcher { private final byte[] generatedSignature; CertificateMatcher(PrivateKey privateKey) { + Assert.notNull(privateKey, "Private key must not be null"); this.privateKey = privateKey; this.signature = createSignature(privateKey); + Assert.notNull(this.signature, "Failed to create signature"); this.generatedSignature = sign(this.signature, privateKey); } private Signature createSignature(PrivateKey privateKey) { try { - String algorithm = getSignatureAlgorithm(this.privateKey); + String algorithm = getSignatureAlgorithm(privateKey); return (algorithm != null) ? Signature.getInstance(algorithm) : null; } catch (NoSuchAlgorithmException ex) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java index d8cfd084ab47..a76f5c2fa2b1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java @@ -120,7 +120,7 @@ private static PemSslStore getPemSslStore(String propertyName, PemSslBundlePrope if (properties.isVerifyKeys()) { CertificateMatcher certificateMatcher = new CertificateMatcher(pemSslStore.privateKey()); Assert.state(certificateMatcher.matchesAny(pemSslStore.certificates()), - "Private key matches none of the certificates in the chain"); + "Private key in %s matches none of the certificates in the chain".formatted(propertyName)); } return pemSslStore; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundleTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundleTests.java index 52447f47b626..d6b770a3d927 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundleTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundleTests.java @@ -134,7 +134,7 @@ void getWithPemSslBundlePropertiesWhenVerifyKeyStoreWithNoMatchThrowsException() properties.getKeystore().setVerifyKeys(true); properties.getKey().setAlias("test-alias"); assertThatIllegalStateException().isThrownBy(() -> PropertiesSslBundle.get(properties)) - .withMessageContaining("Private key matches none of the certificates"); + .withMessageContaining("Private key in keystore matches none of the certificates"); } private Consumer storeContainingCertAndKey(String keyAlias) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java index 364a3f745a29..e6bb75a44bcd 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java @@ -48,7 +48,7 @@ public final class PemContent { private static final Pattern PEM_FOOTER = Pattern.compile("-+END\\s+[^-]*-+", Pattern.CASE_INSENSITIVE); - private String text; + private final String text; private PemContent(String text) { this.text = text; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java index dbc5ca697275..113d490ea18c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemPrivateKeyParser.java @@ -130,7 +130,7 @@ private static int[] getEcParameters(DerElement parameters) { } Assert.state(parameters.isType(ValueType.ENCODED), "Key spec should contain encoded parameters"); DerElement contents = DerElement.of(parameters.getContents()); - Assert.state(contents.isType(ValueType.PRIMITIVE, TagType.OBJECT_IDENTIFIER), + Assert.state(contents != null && contents.isType(ValueType.PRIMITIVE, TagType.OBJECT_IDENTIFIER), "Key spec parameters should contain object identifier"); return getEcParameters(contents.getContents()); } @@ -237,6 +237,7 @@ private PrivateKey parse(byte[] bytes, String password) { return keyFactory.generatePrivate(keySpec); } catch (InvalidKeySpecException | NoSuchAlgorithmException ex) { + // Ignore } } return null; @@ -264,10 +265,6 @@ void octetString(byte[] bytes) throws IOException { codeLengthBytes(0x04, bytes); } - void sequence(int... elements) throws IOException { - sequence(bytes(elements)); - } - void sequence(byte[] bytes) throws IOException { codeLengthBytes(0x30, bytes); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java index 7eb3ce7b6757..e1ed146f3cdb 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java @@ -48,7 +48,7 @@ public interface PemSslStore { String alias(); /** - * the password used + * The password used when * {@link KeyStore#setKeyEntry(String, java.security.Key, char[], java.security.cert.Certificate[]) * setting key entries} in the {@link KeyStore}. * @return the password diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java index 441d9cfb3f89..44c6e0fbff47 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java @@ -51,7 +51,6 @@ public class PemSslStoreBundle implements SslStoreBundle { * @param keyStoreDetails the key store details * @param trustStoreDetails the trust store details */ - @SuppressWarnings("removal") public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails) { this(keyStoreDetails, trustStoreDetails, null); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java index 8f97b2b55adb..2f7dfff29c13 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreDetails.java @@ -73,7 +73,7 @@ public record PemSslStoreDetails(String type, String alias, String password, Str * @param privateKeyPassword a password used to decrypt an encrypted private key */ public PemSslStoreDetails(String type, String certificate, String privateKey, String privateKeyPassword) { - this(type, null, null, certificate, privateKey, null); + this(type, null, null, certificate, privateKey, privateKeyPassword); } /** diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java index e4318afe663c..5f65058ef1f5 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java @@ -154,13 +154,4 @@ void ofReturnsContent() { assertThat(PemContent.of("test")).hasToString("test"); } - @Test - void hashCodeAndEquals() { - PemContent a = PemContent.of("1"); - PemContent b = PemContent.of("1"); - PemContent c = PemContent.of("2"); - assertThat(a.hashCode()).isEqualTo(b.hashCode()); - assertThat(a).isEqualTo(a).isEqualTo(b).isNotEqualTo(c); - } - } From 99986a2fdd686dedd265e5675b29f910948983a3 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Thu, 2 Nov 2023 13:53:08 -0500 Subject: [PATCH 0730/1215] Polish SSL internals --- .../ssl/BundleContentProperty.java | 16 ++++++++------ .../ssl/BundleContentPropertyTests.java | 2 +- .../boot/ssl/pem/PemContent.java | 20 ++++++----------- .../boot/ssl/pem/PemContentTests.java | 22 ++++++++----------- 4 files changed, 26 insertions(+), 34 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/BundleContentProperty.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/BundleContentProperty.java index abaace466f4f..7be8e3eceb37 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/BundleContentProperty.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/BundleContentProperty.java @@ -51,11 +51,6 @@ boolean hasValue() { return StringUtils.hasText(this.value); } - private URL toUrl() throws FileNotFoundException { - Assert.state(!isPemContent(), "Value contains PEM content"); - return ResourceUtils.getURL(this.value); - } - Path toWatchPath() { return toPath(); } @@ -63,15 +58,22 @@ Path toWatchPath() { private Path toPath() { try { URL url = toUrl(); - Assert.state(isFileUrl(url), () -> "Vaule '%s' is not a file URL".formatted(url)); + Assert.state(isFileUrl(url), () -> "Value '%s' is not a file URL".formatted(url)); return Path.of(url.toURI()).toAbsolutePath(); } catch (Exception ex) { - throw new IllegalStateException("Unable to convert '%s' property to a path".formatted(this.name), ex); + throw new IllegalStateException("Unable to convert value of property '%s' to a path".formatted(this.name), + ex); } } + private URL toUrl() throws FileNotFoundException { + Assert.state(!isPemContent(), "Value contains PEM content"); + return ResourceUtils.getURL(this.value); + } + private boolean isFileUrl(URL url) { return "file".equalsIgnoreCase(url.getProtocol()); } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/BundleContentPropertyTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/BundleContentPropertyTests.java index 7ff334f08de6..72d5f6ae3190 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/BundleContentPropertyTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ssl/BundleContentPropertyTests.java @@ -74,7 +74,7 @@ void hasValueWhenHasEmptyValueReturnsFalse() { void toWatchPathWhenNotPathThrowsException() { BundleContentProperty property = new BundleContentProperty("name", PEM_TEXT); assertThatIllegalStateException().isThrownBy(property::toWatchPath) - .withMessage("Unable to convert 'name' property to a path"); + .withMessage("Unable to convert value of property 'name' to a path"); } @Test diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java index e6bb75a44bcd..d3013bcb6f3a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemContent.java @@ -126,19 +126,6 @@ static PemContent load(String content) throws IOException { } } - /** - * Load {@link PemContent} from the given {@link URL}. - * @param url the URL to load content from - * @return the loaded PEM content - * @throws IOException on IO error - */ - public static PemContent load(URL url) throws IOException { - Assert.notNull(url, "Url must not be null"); - try (InputStream in = url.openStream()) { - return load(in); - } - } - /** * Load {@link PemContent} from the given {@link Path}. * @param path a path to load the content from @@ -152,6 +139,13 @@ public static PemContent load(Path path) throws IOException { } } + private static PemContent load(URL url) throws IOException { + Assert.notNull(url, "Url must not be null"); + try (InputStream in = url.openStream()) { + return load(in); + } + } + private static PemContent load(InputStream in) throws IOException { return of(StreamUtils.copyToString(in, StandardCharsets.UTF_8)); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java index 5f65058ef1f5..fac38bc5fd50 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/PemContentTests.java @@ -46,7 +46,7 @@ void getCertificateWhenNoCertificatesThrowsException() { @Test void getCertificateReturnsCertificates() throws Exception { - PemContent content = PemContent.load(getClass().getResource("/test-cert-chain.pem")); + PemContent content = PemContent.load(contentFromClasspath("/test-cert-chain.pem")); List certificates = content.getCertificates(); assertThat(certificates).isNotNull(); assertThat(certificates).hasSize(2); @@ -64,7 +64,7 @@ void getPrivateKeyWhenNoKeyThrowsException() { @Test void getPrivateKeyReturnsPrivateKey() throws Exception { PemContent content = PemContent - .load(getClass().getResource("/org/springframework/boot/web/server/pkcs8/dsa.key")); + .load(contentFromClasspath("/org/springframework/boot/web/server/pkcs8/dsa.key")); PrivateKey privateKey = content.getPrivateKey(); assertThat(privateKey).isNotNull(); assertThat(privateKey.getFormat()).isEqualTo("PKCS#8"); @@ -117,22 +117,14 @@ void loadWithStringWhenContentIsPemContentReturnsContent() throws Exception { @Test void loadWithStringWhenClasspathLocationReturnsContent() throws IOException { String actual = PemContent.load("classpath:test-cert.pem").toString(); - String expected = new ClassPathResource("test-cert.pem").getContentAsString(StandardCharsets.UTF_8); + String expected = contentFromClasspath("test-cert.pem"); assertThat(actual).isEqualTo(expected); } @Test void loadWithStringWhenFileLocationReturnsContent() throws IOException { String actual = PemContent.load("src/test/resources/test-cert.pem").toString(); - String expected = new ClassPathResource("test-cert.pem").getContentAsString(StandardCharsets.UTF_8); - assertThat(actual).isEqualTo(expected); - } - - @Test - void loadWithUrlReturnsContent() throws Exception { - ClassPathResource resource = new ClassPathResource("test-cert.pem"); - String expected = resource.getContentAsString(StandardCharsets.UTF_8); - String actual = PemContent.load(resource.getURL()).toString(); + String expected = contentFromClasspath("test-cert.pem"); assertThat(actual).isEqualTo(expected); } @@ -140,7 +132,7 @@ void loadWithUrlReturnsContent() throws Exception { void loadWithPathReturnsContent() throws IOException { Path path = Path.of("src/test/resources/test-cert.pem"); String actual = PemContent.load(path).toString(); - String expected = new ClassPathResource("test-cert.pem").getContentAsString(StandardCharsets.UTF_8); + String expected = contentFromClasspath("test-cert.pem"); assertThat(actual).isEqualTo(expected); } @@ -154,4 +146,8 @@ void ofReturnsContent() { assertThat(PemContent.of("test")).hasToString("test"); } + private static String contentFromClasspath(String path) throws IOException { + return new ClassPathResource(path).getContentAsString(StandardCharsets.UTF_8); + } + } From 47c10881118e707e4beff4fee876d134a608b84f Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 2 Nov 2023 14:19:07 -0700 Subject: [PATCH 0731/1215] Polish --- .../health/NoSuchHealthContributorFailureAnalyzer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/NoSuchHealthContributorFailureAnalyzer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/NoSuchHealthContributorFailureAnalyzer.java index 262665268254..b3ea8f0165e8 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/NoSuchHealthContributorFailureAnalyzer.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/health/NoSuchHealthContributorFailureAnalyzer.java @@ -30,8 +30,8 @@ class NoSuchHealthContributorFailureAnalyzer extends AbstractFailureAnalyzer Date: Thu, 2 Nov 2023 14:24:05 -0700 Subject: [PATCH 0732/1215] Polish --- .../src/docs/asciidoc/deployment/efficient.adoc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc index 1880abcd4731..be9b80f178c5 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc @@ -34,7 +34,6 @@ The jar contains a `classpath.idx` file which is used by the `JarLauncher` when [[deployment.efficient.aot]] === Using Ahead-of-time Processing With the JVM - It's beneficial for the startup time to run your application using the AOT generated initialization code. First, you need to ensure that the jar you are building includes AOT generated code. @@ -51,9 +50,9 @@ When the JAR has been built, run it with `spring.aot.enabled` system property se [source,shell,indent=0,subs="verbatim"] ---- - $ java -Dspring.aot.enabled=true -jar myapplication.jar + $ java -Dspring.aot.enabled=true -jar myapplication.jar - ........ Starting AOT-processed MyApplication ... + ........ Starting AOT-processed MyApplication ... ---- Beware that using the ahead-of-time processing has drawbacks. @@ -66,10 +65,11 @@ It implies the following restrictions: To learn more about ahead-of-time processing, please see the <>. + + [[deployment.efficient.checkpoint-restore]] === Checkpoint and Restore With the JVM - -https://wiki.openjdk.org/display/crac/Main[CRaC] is an OpenJDK project that defines a new Java API to allow you to checkpoint and restore an application on the HotSpot JVM. +https://wiki.openjdk.org/display/crac/Main[Coordinated Restore at Checkpoint] (CRaC) is an OpenJDK project that defines a new Java API to allow you to checkpoint and restore an application on the HotSpot JVM. It is based on https://github.com/checkpoint-restore/criu[CRIU], a project that implements checkpoint/restore functionality on Linux. The principle is the following: you start your application almost as usual but with a CRaC enabled version of the JDK like https://www.azul.com/downloads/?package=jdk-crac#zulu[the one provided by Azul]. From ff9d9de1eeae66dbad0b163bbd47e8ea1c1440f5 Mon Sep 17 00:00:00 2001 From: "Zhiyang.Wang1" Date: Thu, 26 Oct 2023 20:18:46 +0800 Subject: [PATCH 0733/1215] Add observationEnabled properties for Apache Kafka See gh-38057 --- ...fkaListenerContainerFactoryConfigurer.java | 1 + .../kafka/KafkaAutoConfiguration.java | 1 + .../autoconfigure/kafka/KafkaProperties.java | 26 +++++++++++++++++++ ...itional-spring-configuration-metadata.json | 8 ++++++ .../kafka/KafkaAutoConfigurationTests.java | 5 +++- 5 files changed, 40 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java index 1a2102ad8a26..95108c275e0d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java @@ -236,6 +236,7 @@ private void configureContainer(ContainerProperties container) { map.from(properties::getLogContainerConfig).to(container::setLogContainerConfig); map.from(properties::isMissingTopicsFatal).to(container::setMissingTopicsFatal); map.from(properties::isImmediateStop).to(container::setStopImmediate); + map.from(properties::getObservationEnabled).to(container::setObservationEnabled); map.from(this.transactionManager).to(container::setTransactionManager); map.from(this.rebalanceListener).to(container::setConsumerRebalanceListener); map.from(this.listenerTaskExecutor).to(container::setListenerTaskExecutor); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfiguration.java index 9e2b2f22c8c5..b9d2edc045f7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfiguration.java @@ -98,6 +98,7 @@ PropertiesKafkaConnectionDetails kafkaConnectionDetails(KafkaProperties properti map.from(kafkaProducerListener).to(kafkaTemplate::setProducerListener); map.from(this.properties.getTemplate().getDefaultTopic()).to(kafkaTemplate::setDefaultTopic); map.from(this.properties.getTemplate().getTransactionIdPrefix()).to(kafkaTemplate::setTransactionIdPrefix); + map.from(this.properties.getTemplate().getObservationEnabled()).to(kafkaTemplate::setObservationEnabled); return kafkaTemplate; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java index a7da43eea30e..f1ad2df87e0e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java @@ -984,6 +984,11 @@ public static class Template { */ private String transactionIdPrefix; + /** + * Whether to enable observation. + */ + private boolean observationEnabled; + public String getDefaultTopic() { return this.defaultTopic; } @@ -1000,6 +1005,14 @@ public void setTransactionIdPrefix(String transactionIdPrefix) { this.transactionIdPrefix = transactionIdPrefix; } + public boolean getObservationEnabled() { + return this.observationEnabled; + } + + public void setObservationEnabled(boolean observationEnabled) { + this.observationEnabled = observationEnabled; + } + } public static class Listener { @@ -1117,6 +1130,11 @@ public enum Type { */ private Boolean changeConsumerThreadName; + /** + * Whether to enable observation. + */ + private boolean observationEnabled; + public Type getType() { return this.type; } @@ -1261,6 +1279,14 @@ public void setChangeConsumerThreadName(Boolean changeConsumerThreadName) { this.changeConsumerThreadName = changeConsumerThreadName; } + public boolean getObservationEnabled() { + return this.observationEnabled; + } + + public void setObservationEnabled(boolean observationEnabled) { + this.observationEnabled = observationEnabled; + } + } public static class Ssl { 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 7d21dfd05e2d..701404062954 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 @@ -1811,6 +1811,10 @@ "name": "spring.kafka.jaas.control-flag", "defaultValue": "required" }, + { + "name": "spring.kafka.listener.observation-enabled", + "defaultValue": false + }, { "name": "spring.kafka.listener.only-log-record-metadata", "type": "java.lang.Boolean", @@ -1905,6 +1909,10 @@ "level": "error" } }, + { + "name": "spring.kafka.template.observation-enabled", + "defaultValue": false + }, { "name": "spring.liquibase.check-change-log-location", "type": "java.lang.Boolean", diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java index 2b8c48d3e02a..4adc774e8d7f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java @@ -618,7 +618,8 @@ void listenerProperties() { "spring.kafka.listener.missing-topics-fatal=true", "spring.kafka.jaas.enabled=true", "spring.kafka.listener.immediate-stop=true", "spring.kafka.producer.transaction-id-prefix=foo", "spring.kafka.jaas.login-module=foo", "spring.kafka.jaas.control-flag=REQUISITE", - "spring.kafka.jaas.options.useKeyTab=true", "spring.kafka.listener.async-acks=true") + "spring.kafka.jaas.options.useKeyTab=true", "spring.kafka.listener.async-acks=true", + "spring.kafka.template.observation-enabled=true", "spring.kafka.listener.observation-enabled=true") .run((context) -> { DefaultKafkaProducerFactory producerFactory = context.getBean(DefaultKafkaProducerFactory.class); DefaultKafkaConsumerFactory consumerFactory = context.getBean(DefaultKafkaConsumerFactory.class); @@ -629,6 +630,7 @@ void listenerProperties() { assertThat(kafkaTemplate).hasFieldOrPropertyWithValue("producerFactory", producerFactory); assertThat(kafkaTemplate.getDefaultTopic()).isEqualTo("testTopic"); assertThat(kafkaTemplate).hasFieldOrPropertyWithValue("transactionIdPrefix", "txOverride"); + assertThat(kafkaTemplate).hasFieldOrPropertyWithValue("observationEnabled", true); assertThat(kafkaListenerContainerFactory.getConsumerFactory()).isEqualTo(consumerFactory); ContainerProperties containerProperties = kafkaListenerContainerFactory.getContainerProperties(); assertThat(containerProperties.getAckMode()).isEqualTo(AckMode.MANUAL); @@ -645,6 +647,7 @@ void listenerProperties() { assertThat(containerProperties.isLogContainerConfig()).isTrue(); assertThat(containerProperties.isMissingTopicsFatal()).isTrue(); assertThat(containerProperties.isStopImmediate()).isTrue(); + assertThat(containerProperties.isObservationEnabled()).isTrue(); assertThat(kafkaListenerContainerFactory).extracting("concurrency").isEqualTo(3); assertThat(kafkaListenerContainerFactory.isBatchListener()).isTrue(); assertThat(kafkaListenerContainerFactory).hasFieldOrPropertyWithValue("autoStartup", true); From 55ab56f8d9dbc67fb5dcacdb1dba7b8e0d744f72 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Thu, 2 Nov 2023 17:02:14 -0500 Subject: [PATCH 0734/1215] Polish "Add observationEnabled properties for Apache Kafka" See gh-38057 --- .../additional-spring-configuration-metadata.json | 8 -------- 1 file changed, 8 deletions(-) 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 701404062954..7d21dfd05e2d 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 @@ -1811,10 +1811,6 @@ "name": "spring.kafka.jaas.control-flag", "defaultValue": "required" }, - { - "name": "spring.kafka.listener.observation-enabled", - "defaultValue": false - }, { "name": "spring.kafka.listener.only-log-record-metadata", "type": "java.lang.Boolean", @@ -1909,10 +1905,6 @@ "level": "error" } }, - { - "name": "spring.kafka.template.observation-enabled", - "defaultValue": false - }, { "name": "spring.liquibase.check-change-log-location", "type": "java.lang.Boolean", From 9eda564d063e347e7f937e61cd8c84819c670604 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 3 Nov 2023 09:57:12 +0000 Subject: [PATCH 0735/1215] Fix check for using CoordinatedRestoreAtCheckpointStartup Closes gh-38186 --- .../java/org/springframework/boot/SpringApplication.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index c22c51722414..857529e2aaab 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -1684,8 +1684,10 @@ Duration timeTakenToStarted() { } static Startup create() { - return (!ClassUtils.isPresent("jdk.crac.management.CRaCMXBean", Startup.class.getClassLoader())) - ? new StandardStartup() : new CoordinatedRestoreAtCheckpointStartup(); + ClassLoader classLoader = Startup.class.getClassLoader(); + return (ClassUtils.isPresent("jdk.crac.management.CRaCMXBean", classLoader) + && ClassUtils.isPresent("org.crac.management.CRaCMXBean", classLoader)) + ? new CoordinatedRestoreAtCheckpointStartup() : new StandardStartup(); } } From c0f8b90d31f5080a7d8fe5f00ca441c394bc510a Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sat, 4 Nov 2023 19:43:54 -0700 Subject: [PATCH 0736/1215] Return getLastModified result from JarUrlConnection Update `JarUrlConnection` and `NestedUrlConnection` so that calls to `getLastModified()` and `getHeaderFieldDate("last-modified", 0)` always return a result. Fixes gh-38204 --- .../net/protocol/jar/JarUrlConnection.java | 5 ++ .../protocol/nested/NestedUrlConnection.java | 68 +++++++++++++++++++ .../protocol/jar/JarUrlConnectionTests.java | 20 ++++++ .../nested/NestedUrlConnectionTests.java | 23 +++++++ 4 files changed, 116 insertions(+) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java index c9a7475a1f44..513bc5cab7ae 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java @@ -154,6 +154,11 @@ private String deduceContentTypeFromEntryName() { return guessContentTypeFromName(this.entryName); } + @Override + public long getLastModified() { + return (this.jarFileConnection != null) ? this.jarFileConnection.getLastModified() : super.getLastModified(); + } + @Override public String getHeaderField(String name) { return (this.jarFileConnection != null) ? this.jarFileConnection.getHeaderField(name) : null; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnection.java index 32051123abfc..0409fe6e3d99 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnection.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnection.java @@ -28,6 +28,15 @@ import java.net.URLConnection; import java.nio.file.Files; import java.security.Permission; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import org.springframework.boot.loader.ref.Cleaner; @@ -39,6 +48,9 @@ */ class NestedUrlConnection extends URLConnection { + private static final DateTimeFormatter RFC_1123_DATE_TIME = DateTimeFormatter.RFC_1123_DATE_TIME + .withZone(ZoneId.of("GMT")); + private static final String CONTENT_TYPE = "x-java/jar"; private final NestedUrlConnectionResources resources; @@ -49,6 +61,8 @@ class NestedUrlConnection extends URLConnection { private FilePermission permission; + private Map> headerFields; + NestedUrlConnection(URL url) throws MalformedURLException { this(url, Cleaner.instance); } @@ -69,6 +83,60 @@ private NestedLocation parseNestedLocation(URL url) throws MalformedURLException } } + @Override + public String getHeaderField(String name) { + List values = getHeaderFields().get(name); + return (values != null && !values.isEmpty()) ? values.get(0) : null; + } + + @Override + public String getHeaderField(int n) { + Entry> entry = getHeaderEntry(n); + List values = (entry != null) ? entry.getValue() : null; + return (values != null && !values.isEmpty()) ? values.get(0) : null; + } + + @Override + public String getHeaderFieldKey(int n) { + Entry> entry = getHeaderEntry(n); + return (entry != null) ? entry.getKey() : null; + } + + private Entry> getHeaderEntry(int n) { + Iterator>> iterator = getHeaderFields().entrySet().iterator(); + Entry> entry = null; + for (int i = 0; i < n; i++) { + entry = (!iterator.hasNext()) ? null : iterator.next(); + } + return entry; + } + + @Override + public Map> getHeaderFields() { + try { + connect(); + } + catch (IOException ex) { + return Collections.emptyMap(); + } + Map> headerFields = this.headerFields; + if (headerFields == null) { + headerFields = new LinkedHashMap<>(); + long contentLength = getContentLengthLong(); + long lastModified = getLastModified(); + if (contentLength > 0) { + headerFields.put("content-length", List.of(String.valueOf(contentLength))); + } + if (getLastModified() > 0) { + headerFields.put("last-modified", + List.of(RFC_1123_DATE_TIME.format(Instant.ofEpochMilli(lastModified)))); + } + headerFields = Collections.unmodifiableMap(headerFields); + this.headerFields = headerFields; + } + return headerFields; + } + @Override public int getContentLength() { long contentLength = getContentLengthLong(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java index e84ab435215a..aa915103563a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java @@ -27,6 +27,8 @@ import java.net.URLConnection; import java.nio.charset.StandardCharsets; import java.security.Permission; +import java.time.Instant; +import java.time.temporal.ChronoField; import java.util.List; import java.util.Map; import java.util.jar.JarEntry; @@ -490,4 +492,22 @@ void openReturnsConnection() throws Exception { assertThat(connection).isNotNull(); } + @Test // gh-38204 + void getLastModifiedReturnsFileModifiedTime() throws Exception { + JarUrlConnection connection = JarUrlConnection.open(this.url); + assertThat(connection.getLastModified()).isEqualTo(this.file.lastModified()); + } + + @Test // gh-38204 + void getLastModifiedHeaderReturnsFileModifiedTime() throws IOException { + JarUrlConnection connection = JarUrlConnection.open(this.url); + URLConnection fileConnection = this.file.toURI().toURL().openConnection(); + assertThat(connection.getHeaderFieldDate("last-modified", 0)).isEqualTo(withoutNanos(this.file.lastModified())) + .isEqualTo(fileConnection.getHeaderFieldDate("last-modified", 0)); + } + + private long withoutNanos(long epochMilli) { + return Instant.ofEpochMilli(epochMilli).with(ChronoField.NANO_OF_SECOND, 0).toEpochMilli(); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionTests.java index 7efcf2b25726..488a16852361 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionTests.java @@ -18,11 +18,15 @@ import java.io.File; import java.io.FilePermission; +import java.io.IOException; import java.io.InputStream; import java.lang.ref.Cleaner.Cleanable; import java.net.MalformedURLException; import java.net.URL; +import java.net.URLConnection; import java.security.Permission; +import java.time.Instant; +import java.time.temporal.ChronoField; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -148,4 +152,23 @@ void inputStreamCloseCleansResource() throws Exception { actionCaptor.getValue().run(); } + @Test // gh-38204 + void getLastModifiedReturnsFileModifiedTime() throws Exception { + NestedUrlConnection connection = new NestedUrlConnection(this.url); + assertThat(connection.getLastModified()).isEqualTo(this.jarFile.lastModified()); + } + + @Test // gh-38204 + void getLastModifiedHeaderReturnsFileModifiedTime() throws IOException { + NestedUrlConnection connection = new NestedUrlConnection(this.url); + URLConnection fileConnection = this.jarFile.toURI().toURL().openConnection(); + assertThat(connection.getHeaderFieldDate("last-modified", 0)) + .isEqualTo(withoutNanos(this.jarFile.lastModified())) + .isEqualTo(fileConnection.getHeaderFieldDate("last-modified", 0)); + } + + private long withoutNanos(long epochMilli) { + return Instant.ofEpochMilli(epochMilli).with(ChronoField.NANO_OF_SECOND, 0).toEpochMilli(); + } + } From 8126d2652dc22c7459546854a3ea0041af011df6 Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Fri, 3 Nov 2023 10:31:30 +0800 Subject: [PATCH 0737/1215] Report friendly error when failing to find AOT initializer See gh-38188 --- .../springframework/boot/SpringApplication.java | 7 +++++++ .../boot/SpringApplicationTests.java | 16 ++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index c1420ba6bdf3..c94427f05211 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -168,6 +168,7 @@ * @author Chris Bono * @author Moritz Halbritter * @author Tadaya Tsuyukubo + * @author Yanming Zhou * @since 1.0.0 * @see #run(Class, String[]) * @see #run(Class[], String[]) @@ -436,6 +437,12 @@ private void addAotGeneratedInitializerIfNecessary(List Date: Tue, 7 Nov 2023 10:36:38 +0100 Subject: [PATCH 0738/1215] Polish "Report friendly error when failing to find AOT initializer" See gh-38188 --- .../org/springframework/boot/SpringApplication.java | 10 ++++------ .../springframework/boot/SpringApplicationTests.java | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index c94427f05211..1cbd34264655 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -437,12 +437,10 @@ private void addAotGeneratedInitializerIfNecessary(List Date: Fri, 20 Oct 2023 16:04:08 -0500 Subject: [PATCH 0739/1215] Add smoke test for Kafka with SSL Closes gh-38260 --- .../spring-boot-smoke-test-kafka/build.gradle | 1 + .../main/java/smoketest/kafka/Consumer.java | 4 +- .../kafka/ssl/SampleKafkaSslApplication.java | 29 +++++++ .../ssl/SampleKafkaSslApplicationTests.java | 73 ++++++++++++++++++ .../src/test/resources/docker-compose.yml | 30 +++++++ .../src/test/resources/ssl/credentials | 1 + .../src/test/resources/ssl/test-ca.p12 | Bin 0 -> 4173 bytes .../src/test/resources/ssl/test-client.p12 | Bin 0 -> 2653 bytes .../src/test/resources/ssl/test-server.p12 | Bin 0 -> 2653 bytes 9 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/main/java/smoketest/kafka/ssl/SampleKafkaSslApplication.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/java/smoketest/kafka/ssl/SampleKafkaSslApplicationTests.java create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/resources/docker-compose.yml create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/resources/ssl/credentials create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/resources/ssl/test-ca.p12 create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/resources/ssl/test-client.p12 create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/resources/ssl/test-server.p12 diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/build.gradle index abe8f0e0f35c..426a515062e1 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/build.gradle @@ -14,4 +14,5 @@ dependencies { testImplementation("org.springframework.kafka:spring-kafka-test") { exclude group: "commons-logging", module: "commons-logging" } + testImplementation("org.testcontainers:junit-jupiter") } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/main/java/smoketest/kafka/Consumer.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/main/java/smoketest/kafka/Consumer.java index 4beb1a980ff1..3daf110da707 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/main/java/smoketest/kafka/Consumer.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/main/java/smoketest/kafka/Consumer.java @@ -23,7 +23,7 @@ import org.springframework.stereotype.Component; @Component -class Consumer { +public class Consumer { private final List messages = new CopyOnWriteArrayList<>(); @@ -33,7 +33,7 @@ void processMessage(SampleMessage message) { System.out.println("Received sample message [" + message + "]"); } - List getMessages() { + public List getMessages() { return this.messages; } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/main/java/smoketest/kafka/ssl/SampleKafkaSslApplication.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/main/java/smoketest/kafka/ssl/SampleKafkaSslApplication.java new file mode 100644 index 000000000000..30b8c8ad2e30 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/main/java/smoketest/kafka/ssl/SampleKafkaSslApplication.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2023 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 smoketest.kafka.ssl; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SampleKafkaSslApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleKafkaSslApplication.class, args); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/java/smoketest/kafka/ssl/SampleKafkaSslApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/java/smoketest/kafka/ssl/SampleKafkaSslApplicationTests.java new file mode 100644 index 000000000000..2d239233f4f6 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/java/smoketest/kafka/ssl/SampleKafkaSslApplicationTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2023 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 smoketest.kafka.ssl; + +import java.io.File; +import java.time.Duration; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import smoketest.kafka.Consumer; +import smoketest.kafka.Producer; +import smoketest.kafka.SampleMessage; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.not; + +@Testcontainers +@SpringBootTest(classes = { SampleKafkaSslApplication.class, Producer.class, Consumer.class }, + properties = { "spring.kafka.security.protocol=SSL", "spring.kafka.bootstrap-servers=localhost:9093", + "spring.kafka.ssl.bundle=client", + "spring.ssl.bundle.jks.client.keystore.location=classpath:ssl/test-client.p12", + "spring.ssl.bundle.jks.client.keystore.password=password", + "spring.ssl.bundle.jks.client.truststore.location=classpath:ssl/test-ca.p12", + "spring.ssl.bundle.jks.client.truststore.password=password" }) +class SampleKafkaSslApplicationTests { + + private static final File KAFKA_COMPOSE_FILE = new File("src/test/resources/docker-compose.yml"); + + private static final String KAFKA_COMPOSE_SERVICE = "kafka"; + + private static final int KAFKA_SSL_PORT = 9093; + + @Container + public DockerComposeContainer container = new DockerComposeContainer<>(KAFKA_COMPOSE_FILE) + .withExposedService(KAFKA_COMPOSE_SERVICE, KAFKA_SSL_PORT, Wait.forListeningPorts(KAFKA_SSL_PORT)); + + @Autowired + private Producer producer; + + @Autowired + private Consumer consumer; + + @Test + void testVanillaExchange() { + this.producer.send(new SampleMessage(1, "A simple test message")); + + Awaitility.waitAtMost(Duration.ofSeconds(30)).until(this.consumer::getMessages, not(empty())); + assertThat(this.consumer.getMessages()).extracting("message").containsOnly("A simple test message"); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/resources/docker-compose.yml b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/resources/docker-compose.yml new file mode 100644 index 000000000000..326e5a30c030 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/resources/docker-compose.yml @@ -0,0 +1,30 @@ +--- +version: '2' +services: + zookeeper: + image: confluentinc/cp-zookeeper:7.4.0 + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + + kafka: + image: confluentinc/cp-kafka:7.4.0 + depends_on: + - zookeeper + ports: + - "9092:9092" + - "9093:9093" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,SSL://localhost:9093 + KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true' + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_SSL_CLIENT_AUTH: "required" + KAFKA_SSL_KEYSTORE_FILENAME: '/certs/test-server.p12' + KAFKA_SSL_KEYSTORE_CREDENTIALS: '/certs/credentials' + KAFKA_SSL_KEY_CREDENTIALS: '/certs/credentials' + KAFKA_SSL_TRUSTSTORE_FILENAME: '/certs/test-ca.p12' + KAFKA_SSL_TRUSTSTORE_CREDENTIALS: '/certs/credentials' + volumes: + - ./ssl/:/etc/kafka/secrets/certs diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/resources/ssl/credentials b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/resources/ssl/credentials new file mode 100644 index 000000000000..7aa311adf93f --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/resources/ssl/credentials @@ -0,0 +1 @@ +password \ No newline at end of file diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/resources/ssl/test-ca.p12 b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/resources/ssl/test-ca.p12 new file mode 100644 index 0000000000000000000000000000000000000000..fd0a5d99b0c0d0bccaafb69c0e7d179a2fe8c5d5 GIT binary patch literal 4173 zcmY+GWl$81w})9e7X;}}X_gWtB&4N7z?YKFMOc;+VOhGA?rxS?B$srhBqXFk5Re5C zq&pU_@65gTfA5DgXXg3MIbWY22u`Ji3%~=xsVE6a1fw*gE+_y*fFd{*4nCan76hlf z1i=Z#{u2=v!3jD3A{JZ#&fi7*p9BD6Ate3p599zs5D=f(xw6Fnxnn2*7nc|Z1}E4K zfbu!2wr{*2Dx4bw**|C;C-<4wyB9W8bn~tsW&s1g&q$RbkfH`QCV)C&XGodPc*~oX zI6S0`XQeO!T}JMC8imk{f0n>+ud(VN1x zyLw+}OkEhpCsz7S53g6H7!+(?1xnlyzxXD>t)R$1^m$5TlJ3_h-o=8j1C#Qug>r+S za5Zd@aOcQ;?jA_X&ZIW_{^Dl_p%Bv2^m8wZuHC9@(t7aJgf-#7zUNEQ{*TTB+Cz## zhY9JFO+CCf&Lr$Cw4gjv8krz2wNpGYE@58<`nsv+x}cP1iEniBGqeKBrI9B=0W$kW zyx?5<1!fFiNATpCkGEhu)L^tKQZB#-u}6YUo*cN>wqExT@2*HV$<=sO_RVR}GIPDYYuo0s;LbTA94nQ(Zm| ztSE2}jFg4I2y=D(rG^JvDsAdYW&-;fTVF-$pQh@Y{m|EkRgtkwb$IDn5_oAX-rDV` zl5~JOmS?|dW)!1+0w>1@D<5#WE9J<_hfA!!n|AKRu$f-Mt^|4Q0vw^n^H;uW)yko* zkeXorbH1-_F=1I(ML=#->8E*f+{m$yh4ETiduAWZq~<*udmR2$b>+$Kg%{mcY6r*g zNQ-rMh4N>-wmYOk7vS(2hR^H;*QALeLcA>(NGaOq)jqB;)0~#HtA5^KqTC>d#xmye zk_U1n4cSi)tMiXUX%=<~6&vWJqqYQ!AqCzGl%9ICktwbfl0DrswR*HP&&^+a&GA`0 zT@4{MHWX>`V9Mv%s#{zxN%DZ5>qPB8%<12(8O$nt{i%+9s_KiT((9!#_EUNzu$?;U zGSE|a&Cq0t_*reNUgU>=-W|Wp(w#-I<^%-9=yN%I+Y;>OCkGz#U#UN^*d_>#Zd0Gy zI*7&34Ym0EhnnutX@5O#D&$BTnJ8Q*$?OiS9u$&aPpU0J#CAR3w|V~P+}giW>f+8n zRy7ymlQVvnX|VtJ*^8wb70&FutR~(OAK2r8d;j^%i4VBtQp{l`89lN%yY}SzMb|Lc z({L6!i_!g|Ex454LRf5G!jtZZv%FC=o1Tep#t%dut53zmx>C=!;M^m$j;A?K1?nS5 z)4Td1u*O8L@=m1thU9?udEa~g0_F4OxssaAtEzd9x$_ff-(HG?*aMVdTD88tYvh)1 zwo{>kWm51_``pqh7bm5Ft96l`4e3NzdDqvVuUBBp!IT87f+=_SmU;2I7bA_a>Fd%H zbB0_4FuTxi30SN)nkI&0ID|^Dh3WGdWiOD)>L|RPH7Vga8HW~{F6#*xMuaqub*@bq z?W%O3J`-JOi#>eLQ&A=+M$cos+}=I)ET}3T^Xtow5PY7=F9i`($WmGnoD=xd!;2w7 zJ799BuJ}7H&9C3h`?s(LBS-;Dimq6t^&`@bK{|HyKQaoran*-+nb`@mumxwe4Qc{& zr|(*t^oH>BrM}i4M~|hkPX@9fe{2q4czV)$G(~j}W0#cZ&4QKTh_l!|`v8eNzrqnK z5(G(C?R|bJg%@shXSB`W`*#HACx$Pb{P9Ws;xIEmY91=jssNf&$r->#8dm63XfWK3 zey~P!D=ftVkI<>rTVNtA(u74}n$X2$LrcVcKW)V-hf=-SQ{Y&v%-NvtkOFZ3hTy-@?f=;W?_YZ)h%TxROH7*o zYmdL@n(b3C+i%zyNc$Mr*COYvqb$<^!)m6}Zrb(R|CP6mbXr@@?+G@_MN?z8{0bS0 z9^bV9B4I1riL3H=(TbVWN;@8I=}H}5A!TX&tQrwXXqrS%$>p?N$vGg5Sxt#lqn>vi z71JkW&3TR# zmR~kP*C~zO+@feqH6)I_n3C%8^Td|Kfb>g2gd_%;)JYvUZmNMvrYRCni z5YR^ffjO(?O?3;pqo~v6U$w~e-|alHGC7qC!79|l)RVHN!J&<}`)o399@&0Kj6T>! zO2VV$_vkVBT+gc8ZPA(HJL^rBp^ypp@Ot*;64`kG$u@+{& z?rTV>x|_H+-3NSj(kqZ}zeaXv=oJ!saWs${KEC&$X{g(#1)3oX82VfoJp&@~VtzP!^G73$x0J@*l~`M{t=$vIA(91#KgU{qV*gxc zW}r)wcAjhgs!*dBwaQ48(Ow~lp!Th(0MkjCyB=6pO`!4C?Nlic_=Cu(Kbi^NZ#Db8 z`CupWztJY=@TX$KeC|?8jPglTTFdMOm9`koh$XznGym+;)HsIR;EPp-AJKw_B;=!Y zamHpY5#RR?Gk%GCHtVRrD1|my`eI{aINM`>m1SPoXjje*#ms#J z0tD3*>~K*ryT-61atr(UQFwxowTw$^pPlf5RKjHZ`dnx!i{2()g5)M6&FmcK)3WOq zn$Fq-E>d8N;|bgR#Z+RA;45>d*|h_^*ae!sap_5wDm!clk^Dp?$;OvoFoPp#$Gmx; z1gGm}Mu(gEU_%3r>T9FoDIK;{9^Y8gi0v2JyCjoDDF1o$ZZE5a$t{EW(%k3yta-LS zU7CpZa~Si&vdr?^a2B%iA3Q-h6d?(+yFh_AFGlW??9fX5jSWY{btoptV;h<2B0eOq z*Wf}+9EQ!2!&YB=m`U)=cED>5zY?`YhyQuGe}0)<9#wEH_+Z9ll+HTmZAxN@$!LW@ z5q+(QbARXCI!cE3#NJ{=?`X^?b~v&qpQf7!P}Fu(c(93*^*1SX#V6j8(Pgb9#lKN` z9h12e@~l-%sZ^bs7Nk1sQ+a%{FZQf-cLN(?EZ!0FG{+ri96cKQWo|d-m3Y87$uNoy zL&8WfotssO*{|ssOyx_F2;FRfLW6f}rFmN7+cr0we@(pH^LX|1D`UvY%4n1iuT4&-u1x|Hht_ZodVBYIGn1iHGXSC6r9!Kgvp>SAh7O zho}ssoPT#TBxvMO$K#|K)E7g)@<{8z#Jl$SFULDWP&=3LM*8TMx1ff}lm5y@ReX&p z3qf^*w}XC9u6H+*mxfy_x&y}vJj?1kJLF^LR(R--e9R~sYeO@JrO(w}z(r77R@@5; z{#yt-P*9ufl@gY1J9y7FoLq)~L~Ao>;gdx(Zm5v!PP?d_a!<9Ph+WW^+6NOA|8UQD zZ+EWNO(1%tkKZ%3cxkh#c7(L>5gcI6WSZcZ91|&m1z5p zvU-mg<*V#%MK^&sn_EtZy~(S5dY<#9BoSUY-x7^GnF}!8=^<<;Lk~Pn<0!T9LvyYW zH7>(!8P1CH>G8L`DIQA8%0)WZK9XhJPAx%Wq6Dw>x0j~uFM8ZAFAy#5gB z^lr$axQ3eQK3dHri;OUkj@<)tXxRlqTi!Lje~ed0i&%iObN7D;ug+C$)60L+$S5-~ z&TM%5b@LjCMVu<7R}s0RsHon%{opNFsyBAbLJAc%Obe*5LRc^9<5IM;mhU5*#@ZS& zozC``7tv-kM^1FcOvErLxvs)OS|4`v*LJ$fvv@%a^7VHO%t)RMk{}S|zu(EB>OP!Y$6NTlbm?B+*%2CEskzT6~ z$*-;)wGN3}-H*`28Ko(9rKk5*TBq9&b3U>!i!KG&RR!6Xw66qzs`IUyA&+Nin8D*q zGgO>}Pji~ZB==e$fFhPU+LuyZD!SAdJ_2kTJ3Wy^SJSfy)I#w<2W5ltSmo)NwWe4# z;(%AxE=~@pZ{G8Ena3+x)^3Ml#xoDGPh3USdT5?Rt1P6fWY`;aXqz(iSGgoF1Rk~6 zNT+;aSo*{zF~+>uphrBTE{Z;7U6+2KnTx$1;B3F-=lFSzhU~UoC$;rrNp-whjS!*E zFRLjh$=-kx6zIjcZZneR=6>=7a125%k3D_YpHMC9yo0D;UjiT!4GZImw%yGxoq)`@ zy@@d;>Lkjz=Y<*^y2|4Zo%J#)@3y{2V4TIwZKxgx^OrTIu0`n=EUn-C$xQ(#dyV}T zun*!9jqg{KD<`dzZ@r~9p|($3U%vj;v?~6ScCI3rM{kF5*6*MX%VW@83s!G)LRk>? zUaT{$2%)%6_x42fxyo>hgx-1Xyt>fLSIfUw6BZWjKgt`fJ*~AfUewC`;Cmk0yDj!> z#PP5+MNs0$M0;GZZcmG4aGixAojKT01f9!=n3q$Vr^PI9Z@|j?Ca+_0C9k^cRB%FS zO0yNqO5Zs7IE1?D9|T=kJw<4rkT;ItahOw8&4KTuKZk22L$_}r)4$Jme41Fz#^aMZ z`(~CkxogM*7@ZWISa=d~xuwGy^CHIC0j|^jA34eOMd8_P#kk_UAnyNdXC$=ZbodX0 zn2~?dLwzF(fZHKJoUpaz$W1YjIEWKOLP#J4#KUDGz`=i*pDtV0d%e?v8e`u!VM=0c VSiLX9CssRFv)7Vih5p;p{tLI(450u3 literal 0 HcmV?d00001 diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/resources/ssl/test-client.p12 b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/resources/ssl/test-client.p12 new file mode 100644 index 0000000000000000000000000000000000000000..d2fd1d0f3228b3634eee258a820bec23114652cc GIT binary patch literal 2653 zcmY+^c{me}9|v%o*+y989AZK&G=vnl0qg>D(oBTiT9io#Vc{@7;m?-4Ht%OMXu53#G2>(ZWdwa)u zh01Fc_=(RYUle1OEp_alB~v59BOl9(J2~6s6TH%J5X0XsRqb-YX`_}VKrfG|s2ro- zDSIK*q@t7gs4d^m&lkbuRF@HXUfF2Zik6GuQF|<#qyJ4aVpg&w8=mUI?k>O&IyoB zG;r=OELLm6aGj5Mb_+bLg6k-xH>!HdjnqtW9E9s*P59wA{fggxHEY|Zx;@pM@)+`3 ziFqq|;=}OZfCeqxSA*W$!2umZcTg+BB^lXm1f(>lX}Wj zcesk)b?x6(ksMIy)cSq*)Y36p{0WR*VHBb1;zsm4lgde>o*lm99~Rw1hbHI5-)a)2 zu|_I!%gpv@o`67jlVJ@F3UYYGkn7pJDLsS^&G<^NdCpS-8f-y_3a;j_U6Z>~Fqun< zcg@iLsV-r!wJ0)5H>qnjPCSXVR+*y$Ob*GqTG5-E`*d)f5_%j=*9Viu;a@P|4fK)f zc84C_KP`9O3uGOdr}eoj@U=M9yGx8&z-wJ^jb98q?g^Nm^;4zHg_LppxKl1`w}L~~ z+QXUy4izmw^(up1e`pA&pH|*aPrk3JErv@Ta#(Lkb0_IkrS#DwPR*KBdab!d3MhA+ z#j-AZ@_j@{N92(zBUB`1`>@0N0uDB6>-{U*lMfGMY;j?5aZRR2RK2d#oTn76ZT$OSLBVted_*p=LW^PPUyG~m-hhvF zk$%+Mz6^SmvgcwQwBu3yn#`b4=H9~n&y5~8oPS^$)SMqJ^ZGqv(t4S|RGM-M- zpPePoLRK`=V`<(mW{>I_mQ}@FsmsFT39WH7A%eAb1+ocvP7Z+!Eo!L0+W^ju6~5DY z;oLCRZ|&2?2O7!iL~FA%Gdh~dJ}*@NT4|Hc%L?BHQIAgau~{25M}=0aCww{=l*^s! zU^fj+>^FTdIv9yLrlip;kitE! ziJ3kd)w-%?-fFI*;$qn8ig-d`fW~jnYAajK4?lPmcWi_bFTF#;6#i6lIQ6w?&lRQ8 zAVIw^@RoPbeFON1X$i~{U^1xkROQHMh8#xPPE1Ooqnq8R@5uE3Rkt8Du=Gk?e4hgM zGNSZWh;VSogbonjGNJu8XK#;3sZ4T`-6FO|!H90ez(v zXUM$*pOFhH`bN~SnkmGpvy#>GUwe4|rXD@sBt;3!%g{(xBv%>0hx`caIF*~divccboCpYuNcH* z5LQdC$)T)s15b{;I_qwklUNN8WW~y_AA_}#km>UaFF64&B4FajmZTqU`=^F-4^kAW zc-+wO_SeF1Qde?G&yqBx)guIH2YnWN%IjS*firJ2iyhkdG241PNk&t5 z;)COMx_r08A1%k~zUNo9f(XIn3x@Q56v1mVH0?5!yc`EDH?rdCKN2Y1ysoXp4tnRO z51HSJH!kG%T9YhTjZ9SUSMQR!edQ2YFU|fD+EJ7Q{i<5v$J^(E#)C-XwzqDVr6Bh9 zW~QmNFD}gcicliDco3=ICIq%cn0I;2mU|<;DP-{$ySE@>N8(UBPCovz%Gq9HqfvEE zrRa3zWYH!^FTuU&`>K{W%OIR@^Q*g_qiB^ySebe^kKH;cd2cQu#1F6ow{xix;-QqlYPT)%_Em%~KEhRNiuobz|?+MQwG+%VlQ^PTo(oML(#{i>75AFBYsbp9pXP_nklPr}6K+nOii-Y!0Gi^G`u!F;s0 z?$e%OeW2us#62|%FPvmIp&4S_CftwjC;N3&Q3w93qoAaY6iA{ZX}|EA2+17!u8T4d z_tkY(b~Xd0gF>JL!2*Xw`GIf&K9Iy(*;@iu>yyIjI2f||oba`7vwRQ)J;l0uFMB8E I=-*2I3xO8h1poj5 literal 0 HcmV?d00001 diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/resources/ssl/test-server.p12 b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/resources/ssl/test-server.p12 new file mode 100644 index 0000000000000000000000000000000000000000..5f1bd89eccfcc136a696775401e5bb754ba015e7 GIT binary patch literal 2653 zcmV-j3ZnHef(lsz0Ru3C3LgduDuzgg_YDCD0ic2k5Cnn>3^0NU2rz;KzXk~^hDe6@ z4FLxRpn?RjFoFcE0s#Opf&`@o2`Yw2hW8Bt2LUh~1_~;MNQUKI#dRVHjOx(sGe3 zZg!JE*R<;|FgnQD$f#NNoj|Dcgj5S>zLR2-ZVt@K_ z4Qsr)aPW^ZDb^wZz52cB2AoIRme?v*{Vo8h9*?jAorfLPvjRc}0KYGKjoqQnFn*eb zxGDJz(Xs{ew{z0)WzaL>#iak^Zzk_+8ks_e<~oDg)+}{5wYpPW*=)ACj@;28kn-|P zPA|n%+RVp$VHr!NDFWDMFNP)^L2+bnR9cfF2M^I7gP^NW)ygHb<;&(|E`uePi)9=xy6=|{gi z^Rq+d94jpnXfj{z^@M!dGU9TBf2yBa?@S$;`6wpN4;`g!mT2K~9RTj6x}s?(=>b9+ z+Q4)UA!r5NOGnmkg?&_V{JH74ok5PGo9f^Q_6w-7tJookf<^c@RCmMO|JtE;n;f`V z0CgWH1m-Y%!pv4>*-s(Go|6Jy&qs>lL3ku~5)N6;9if-FeK5K=Lo?mP3Dr~EFCj3# zx~?lNEvr8;-4%3)gfp?0?f>HGB2|5)f$cqf{9Q}yZ}8Kx@jOdFzxKK)=TiN72L~>M z<4;28<`2ra=@$2y+YQi6@Nxe~GmccXG)M?s%8xcTil%_qzIFYZuYq!B$k`wH#Xpfl zWira+p8EY@`C*GS76`@HWBrtCj=yfUS%h$@V)%bx8c7pZ-5G5eQ8$it5s&R(;D>og znloC6quqsC=_Chi@NJ0(YaLm$? z0NQ9!w?^N6`L^`;oyRodU_}QkxlA6t7`zxXw@2g~VMB(oa{$mJXt0J< zHMVDTy1H3RXnn?K#Wzi+oIeTUKA-a-(TxTmzIP0ml=|6VE5TzdrVqLKXag{oq9Miq zLXfPbmlqSoN-=0*3!c*ZcI5iRgL3ob{sMMBM7sLVxRKlI0W1GV)guesu-%_Y-N#^v z4=J^1tOhM5+#IAaQi~oRro?@Cvf%OxcHsM2;HEsas{RzFKqgOIZH4wgXHj$ z8G5Pk%brUQ+(r0OE4yHQ^?5a7!b&n(w|v!&0RiFRYU?gLkhLe{Q7}6WzWs@@48D#5 zx+#T>!wswZ>}*8IFoFd^1_>&LNQUlyMg0EK>b+XmBP-GB_uRlw**qE7O0wfWWVa zM28*XO)V->1DPs+vLg9X@G~01n)Nsh$E-2U^)`gV{*;Sn@KYoPDj_7?olJ9NDHpAR zRjG-udZ@$tK&>rzg9nbDhDspxlom{Sk@J;8(Jjb0RHvDSW`lj|AoX`MBQ`v1wO04353H&SY z815%-cGdsIhz7nB);>Bpf3@PYJ6)v<)+A?JIh%`6hZ_eaJ;!>QSA ze(UbVsk22qK_^UXm7gv9Yeqt73u4?|0q)Wa2ly3U+p1SIO-k6mKLGkf$=~U;hPz}Z zEBU0t0;Qjq{$6&~9GkUJb3D~%^32|}Iwhh+3D-4RElK9}7E!p%?FM`~jx?d83@SxJ z1Ps?_blA3w1p{?fDCQCq#D&IADD281czN#gRs`Y_o#7_x5w zB%K*E6=RsTgP#}6R3W!k)yV+KoSX3Xk=wN@JQT_zqjE1kf+x7sz#k-ir>Yn9mbEUj zC1e(gvC?79FY7pO;~nYIp5(M9KaxQr-8$u3O3wg-$Uo!ge`T~RkR=6BShS2T-BfNn ztAaWyS4ORm!VmSZ@P;n*24ZY~_2)vPvbW`)}lUfcrq-Va1{^lEk7g=4pVP&ZIx%MxX zbD?3uGXulo=GW-ECZPY~tnCl<+N=7d#08bDil#8K*TzQQsP*{z`Mu`CfZKd8V<*t0 zl+FTAifi*I@rayU5X1-B+5lo&4j6Y12s}U<7Ht2aU)lNB>8%rx_@*XVGr(Li9wUf! zGcg|EwrL39vWZFpwLUiZ9YgE_)IQU95dM~^Ea{d-rkvw|z(q(B`!LS=!n#GW-n7llYVt{)b-p=08)sOKQf zt$#5skg?cgP2`5mY#3>5F+EcbG)%r|$=MQ0q0AE5z67&%G~+${U!D150y3GJc9`ZL zE@Y|1C5$M!znazov_DhIgW49|mD@kD(x!B!fPtPOsPh@%dmZ?Q{&u*yEGS_Rs)EIU zngz~sL#ANIMDx-(nNr8NiMRjmDAU$8Bk?u?!a&QSxo@##Eav;mmh@UU!f*$V9KJeW zQJjwTuMU<%amwrWM-kLTr0)?QJ# Date: Tue, 7 Nov 2023 11:53:10 -0800 Subject: [PATCH 0740/1215] Provide a way to create custom ApplicationContextFactory Update `SpringBootContextLoader` so that `getApplicationContextFactory` is now a protected that may be overridden to provide a custom `ApplicationContextFactory` instance. Closes gh-38205 --- .../test/context/SpringBootContextLoader.java | 32 ++++++++++++------- .../context/SpringBootContextLoaderTests.java | 28 ++++++++++++++++ 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java index 00d9c714eb94..fa192d3ff007 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java @@ -196,8 +196,7 @@ else if (mergedConfig instanceof ReactiveWebMergedContextConfiguration) { else { application.setWebApplicationType(WebApplicationType.NONE); } - application.setApplicationContextFactory( - (webApplicationType) -> getApplicationContextFactory(mergedConfig, webApplicationType)); + application.setApplicationContextFactory(getApplicationContextFactory(mergedConfig)); if (mergedConfig.getParent() != null) { application.setBannerMode(Banner.Mode.OFF); } @@ -212,17 +211,26 @@ else if (mergedConfig instanceof ReactiveWebMergedContextConfiguration) { } } - private ConfigurableApplicationContext getApplicationContextFactory(MergedContextConfiguration mergedConfig, - WebApplicationType webApplicationType) { - if (webApplicationType != WebApplicationType.NONE && !isEmbeddedWebEnvironment(mergedConfig)) { - if (webApplicationType == WebApplicationType.REACTIVE) { - return new GenericReactiveWebApplicationContext(); - } - if (webApplicationType == WebApplicationType.SERVLET) { - return new GenericWebApplicationContext(); + /** + * Return the {@link ApplicationContextFactory} that should be used for the test. By + * default this method will return a factory that will create an appropriate + * {@link ApplicationContext} for the {@link WebApplicationType}. + * @param mergedConfig the merged context configuration + * @return the application context factory to use + * @since 3.2.0 + */ + protected ApplicationContextFactory getApplicationContextFactory(MergedContextConfiguration mergedConfig) { + return (webApplicationType) -> { + if (webApplicationType != WebApplicationType.NONE && !isEmbeddedWebEnvironment(mergedConfig)) { + if (webApplicationType == WebApplicationType.REACTIVE) { + return new GenericReactiveWebApplicationContext(); + } + if (webApplicationType == WebApplicationType.SERVLET) { + return new GenericWebApplicationContext(); + } } - } - return ApplicationContextFactory.DEFAULT.create(webApplicationType); + return ApplicationContextFactory.DEFAULT.create(webApplicationType); + }; } private void prepareEnvironment(MergedContextConfiguration mergedConfig, SpringApplication application, diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootContextLoaderTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootContextLoaderTests.java index 5102e242d562..ee8777e176a7 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootContextLoaderTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/SpringBootContextLoaderTests.java @@ -26,12 +26,14 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanCreationException; +import org.springframework.boot.ApplicationContextFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.test.context.SpringBootTest.UseMainMethod; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.boot.web.reactive.context.GenericReactiveWebApplicationContext; import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.ConfigurableEnvironment; @@ -246,6 +248,13 @@ void whenUseMainMethodWithContextHierarchyThrowsException() { .withMessage("UseMainMethod.ALWAYS cannot be used with @ContextHierarchy tests"); } + @Test + void whenSubclassProvidesCustomApplicationContextFactory() { + TestContext testContext = new ExposedTestContextManager(CustomApplicationContextTest.class) + .getExposedTestContext(); + assertThat(testContext.getApplicationContext()).isInstanceOf(CustomAnnotationConfigApplicationContext.class); + } + private String[] getActiveProfiles(Class testClass) { TestContext testContext = new ExposedTestContextManager(testClass).getExposedTestContext(); ApplicationContext applicationContext = testContext.getApplicationContext(); @@ -370,6 +379,25 @@ static class UseMainMethodWithContextHierarchy { } + @SpringBootTest + @ContextConfiguration(classes = Config.class, loader = CustomApplicationContextSpringBootContextLoader.class) + static class CustomApplicationContextTest { + + } + + static class CustomApplicationContextSpringBootContextLoader extends SpringBootContextLoader { + + @Override + protected ApplicationContextFactory getApplicationContextFactory(MergedContextConfiguration mergedConfig) { + return (webApplicationType) -> new CustomAnnotationConfigApplicationContext(); + } + + } + + static class CustomAnnotationConfigApplicationContext extends AnnotationConfigApplicationContext { + + } + @Configuration(proxyBeanMethods = false) static class Config { From dbbde18d4175d4f57a139050d4a6aa165151ad9e Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 8 Nov 2023 07:02:35 -0800 Subject: [PATCH 0741/1215] Attempt to fix Windows build failure due to open files See gh-38204 --- .../net/protocol/jar/JarUrlConnectionTests.java | 10 ++++++++-- .../net/protocol/nested/NestedUrlConnectionTests.java | 11 ++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java index aa915103563a..c9553c731379 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java @@ -502,8 +502,14 @@ void getLastModifiedReturnsFileModifiedTime() throws Exception { void getLastModifiedHeaderReturnsFileModifiedTime() throws IOException { JarUrlConnection connection = JarUrlConnection.open(this.url); URLConnection fileConnection = this.file.toURI().toURL().openConnection(); - assertThat(connection.getHeaderFieldDate("last-modified", 0)).isEqualTo(withoutNanos(this.file.lastModified())) - .isEqualTo(fileConnection.getHeaderFieldDate("last-modified", 0)); + try { + assertThat(connection.getHeaderFieldDate("last-modified", 0)) + .isEqualTo(withoutNanos(this.file.lastModified())) + .isEqualTo(fileConnection.getHeaderFieldDate("last-modified", 0)); + } + finally { + fileConnection.getInputStream().close(); + } } private long withoutNanos(long epochMilli) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionTests.java index 488a16852361..c3a8d8aa566a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionTests.java @@ -162,9 +162,14 @@ void getLastModifiedReturnsFileModifiedTime() throws Exception { void getLastModifiedHeaderReturnsFileModifiedTime() throws IOException { NestedUrlConnection connection = new NestedUrlConnection(this.url); URLConnection fileConnection = this.jarFile.toURI().toURL().openConnection(); - assertThat(connection.getHeaderFieldDate("last-modified", 0)) - .isEqualTo(withoutNanos(this.jarFile.lastModified())) - .isEqualTo(fileConnection.getHeaderFieldDate("last-modified", 0)); + try { + assertThat(connection.getHeaderFieldDate("last-modified", 0)) + .isEqualTo(withoutNanos(this.jarFile.lastModified())) + .isEqualTo(fileConnection.getHeaderFieldDate("last-modified", 0)); + } + finally { + fileConnection.getInputStream().close(); + } } private long withoutNanos(long epochMilli) { From 759d09686722d50209246d3d57996e704e7651e1 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Wed, 8 Nov 2023 13:18:12 -0600 Subject: [PATCH 0742/1215] Disable Kafka SSL smoke test when Docker is not available See gh-38260 --- .../smoketest/kafka/ssl/SampleKafkaSslApplicationTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/java/smoketest/kafka/ssl/SampleKafkaSslApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/java/smoketest/kafka/ssl/SampleKafkaSslApplicationTests.java index 2d239233f4f6..8ffe00221173 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/java/smoketest/kafka/ssl/SampleKafkaSslApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/java/smoketest/kafka/ssl/SampleKafkaSslApplicationTests.java @@ -36,7 +36,7 @@ import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.not; -@Testcontainers +@Testcontainers(disabledWithoutDocker = true) @SpringBootTest(classes = { SampleKafkaSslApplication.class, Producer.class, Consumer.class }, properties = { "spring.kafka.security.protocol=SSL", "spring.kafka.bootstrap-servers=localhost:9093", "spring.kafka.ssl.bundle=client", From 61aecdedd6fde1d6cf37cd5f3dbbfc14557ae94b Mon Sep 17 00:00:00 2001 From: Claudio Nave Date: Thu, 9 Nov 2023 11:37:43 -0800 Subject: [PATCH 0743/1215] Remove Liquibase javax.activation excludes Liquibse no longer declares a dependency on `javax.activation` (see https://github.com/liquibase/liquibase/issues/4487) so we can now remove our exclusions. See gh-38274 --- .../spring-boot-actuator-autoconfigure/build.gradle | 1 - spring-boot-project/spring-boot-actuator/build.gradle | 1 - spring-boot-project/spring-boot-autoconfigure/build.gradle | 1 - spring-boot-project/spring-boot/build.gradle | 1 - .../spring-boot-smoke-test-data-r2dbc-liquibase/build.gradle | 1 - .../spring-boot-smoke-test-liquibase/build.gradle | 1 - 6 files changed, 6 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index 7da8f6362edf..c901a3fc0af2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -108,7 +108,6 @@ dependencies { optional("org.hibernate.validator:hibernate-validator") optional("org.influxdb:influxdb-java") optional("org.liquibase:liquibase-core") { - exclude group: "javax.activation", module: "javax.activation-api" exclude group: "javax.xml.bind", module: "jaxb-api" } optional("org.mongodb:mongodb-driver-reactivestreams") diff --git a/spring-boot-project/spring-boot-actuator/build.gradle b/spring-boot-project/spring-boot-actuator/build.gradle index e9a027a00e33..37325cfd7e19 100644 --- a/spring-boot-project/spring-boot-actuator/build.gradle +++ b/spring-boot-project/spring-boot-actuator/build.gradle @@ -50,7 +50,6 @@ dependencies { optional("org.hibernate.validator:hibernate-validator") optional("org.influxdb:influxdb-java") optional("org.liquibase:liquibase-core") { - exclude group: "javax.activation", module: "javax.activation-api" exclude(group: "javax.xml.bind", module: "jaxb-api") } optional("org.mongodb:mongodb-driver-reactivestreams") diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index 452c3e223536..0841bd8b7076 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -122,7 +122,6 @@ dependencies { exclude group: "javax.xml.bind", module: "jaxb-api" } optional("org.liquibase:liquibase-core") { - exclude group: "javax.activation", module: "javax.activation-api" exclude group: "javax.xml.bind", module: "jaxb-api" } optional("org.messaginghub:pooled-jms") { diff --git a/spring-boot-project/spring-boot/build.gradle b/spring-boot-project/spring-boot/build.gradle index bad1e5e57f21..fcb00b66480a 100644 --- a/spring-boot-project/spring-boot/build.gradle +++ b/spring-boot-project/spring-boot/build.gradle @@ -71,7 +71,6 @@ dependencies { exclude(group: "javax.xml.bind", module: "jaxb-api") } optional("org.liquibase:liquibase-core") { - exclude group: "javax.activation", module: "javax.activation-api" exclude(group: "javax.xml.bind", module: "jaxb-api") } optional("org.postgresql:postgresql") diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-r2dbc-liquibase/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-r2dbc-liquibase/build.gradle index 376dda13dd7a..bd8a4c685dad 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-r2dbc-liquibase/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-r2dbc-liquibase/build.gradle @@ -9,7 +9,6 @@ dependencies { implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-data-r2dbc")) runtimeOnly("org.liquibase:liquibase-core") { - exclude group: "javax.activation", module: "javax.activation-api" exclude group: "javax.xml.bind", module: "jaxb-api" } runtimeOnly("org.postgresql:postgresql") diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-liquibase/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-liquibase/build.gradle index 1126e59d0b42..292368536ed1 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-liquibase/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-liquibase/build.gradle @@ -11,7 +11,6 @@ dependencies { implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web")) implementation("jakarta.xml.bind:jakarta.xml.bind-api") implementation("org.liquibase:liquibase-core") { - exclude group: "javax.activation", module: "javax.activation-api" exclude group: "javax.xml.bind", module: "jaxb-api" } From 4a6564c0a944c9c25ca8c346699a40a2c8f2b5a4 Mon Sep 17 00:00:00 2001 From: Claudio Nave Date: Thu, 9 Nov 2023 11:38:35 -0800 Subject: [PATCH 0744/1215] Add Liquibase `show-summary` and `show-summary-output` properties Update `LiquibaseProperties` and `LiquibaseAutoConfiguration` to support the recently added `setShowSummary` and `setShowSummaryOutput` methods. See gh-38274 --- .../liquibase/LiquibaseAutoConfiguration.java | 2 ++ .../liquibase/LiquibaseProperties.java | 30 +++++++++++++++++++ .../LiquibaseAutoConfigurationTests.java | 27 +++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java index f19ab38f011b..8beb5ffcae18 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java @@ -113,6 +113,8 @@ public SpringLiquibase liquibase(ObjectProvider dataSource, liquibase.setRollbackFile(properties.getRollbackFile()); liquibase.setTestRollbackOnUpdate(properties.isTestRollbackOnUpdate()); liquibase.setTag(properties.getTag()); + liquibase.setShowSummary(properties.getShowSummary()); + liquibase.setShowSummaryOutput(properties.getShowSummaryOutput()); return liquibase; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java index 74588c2b8124..fa92d5c5694f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java @@ -19,6 +19,8 @@ import java.io.File; import java.util.Map; +import liquibase.UpdateSummaryEnum; +import liquibase.UpdateSummaryOutputEnum; import liquibase.integration.spring.SpringLiquibase; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -135,6 +137,18 @@ public class LiquibaseProperties { */ private String tag; + /** + * Whether to print a summary of the update operation. Values can be 'off', 'summary' + * (default), 'verbose' + */ + private UpdateSummaryEnum showSummary; + + /** + * Where to print a summary of the update operation. Values can be 'log' (default), + * 'console', or 'all'. + */ + private UpdateSummaryOutputEnum showSummaryOutput; + public String getChangeLog() { return this.changeLog; } @@ -288,4 +302,20 @@ public void setTag(String tag) { this.tag = tag; } + public UpdateSummaryEnum getShowSummary() { + return this.showSummary; + } + + public void setShowSummary(UpdateSummaryEnum showSummary) { + this.showSummary = showSummary; + } + + public UpdateSummaryOutputEnum getShowSummaryOutput() { + return this.showSummaryOutput; + } + + public void setShowSummaryOutput(UpdateSummaryOutputEnum showSummaryOutput) { + this.showSummaryOutput = showSummaryOutput; + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java index 2e0be6c31b52..6a9490f5dd5b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java @@ -29,6 +29,8 @@ import javax.sql.DataSource; import com.zaxxer.hikari.HikariDataSource; +import liquibase.UpdateSummaryEnum; +import liquibase.UpdateSummaryOutputEnum; import liquibase.integration.spring.SpringLiquibase; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -117,6 +119,9 @@ void defaultSpringLiquibase() { assertThat(liquibase.getDefaultSchema()).isNull(); assertThat(liquibase.isDropFirst()).isFalse(); assertThat(liquibase.isClearCheckSums()).isFalse(); + UpdateSummaryOutputEnum showSummaryOutput = (UpdateSummaryOutputEnum) ReflectionTestUtils + .getField(liquibase, "showSummaryOutput"); + assertThat(showSummaryOutput).isEqualTo(UpdateSummaryOutputEnum.LOG); })); } @@ -383,6 +388,28 @@ void overrideLabelFilter() { .run(assertLiquibase((liquibase) -> assertThat(liquibase.getLabelFilter()).isEqualTo("test, production"))); } + @Test + void overrideShowSummary() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.liquibase.show-summary=off") + .run(assertLiquibase((liquibase) -> { + UpdateSummaryEnum showSummary = (UpdateSummaryEnum) ReflectionTestUtils.getField(liquibase, + "showSummary"); + assertThat(showSummary).isEqualTo(UpdateSummaryEnum.OFF); + })); + } + + @Test + void overrideShowSummaryOutput() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.liquibase.show-summary-output=all") + .run(assertLiquibase((liquibase) -> { + UpdateSummaryOutputEnum showSummaryOutput = (UpdateSummaryOutputEnum) ReflectionTestUtils + .getField(liquibase, "showSummaryOutput"); + assertThat(showSummaryOutput).isEqualTo(UpdateSummaryOutputEnum.ALL); + })); + } + @Test @SuppressWarnings("unchecked") void testOverrideParameters() { From 67c5d100514272ae093fbe31f1173edd97fa753b Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 10 Nov 2023 11:36:13 +0100 Subject: [PATCH 0745/1215] Process multipart properties for PartEvent support Prior to this commit, some properties in the `spring.webflux.multipart` namespace were ignored for the streaming use case because those were not supported in streaming mode with `PartEvent`. As of Spring Framework 6.1, the `max-parts` and `max-disk-usage-per-part` properties can be supported and this commit maps those properties accordingly. Fixes gh-37642 --- .../web/reactive/ReactiveMultipartAutoConfiguration.java | 4 ++++ .../web/reactive/ReactiveMultipartProperties.java | 8 ++++---- .../reactive/ReactiveMultipartAutoConfigurationTests.java | 6 +++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfiguration.java index 722c5706f31a..45ba63007162 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfiguration.java @@ -78,6 +78,10 @@ else if (codec instanceof PartEventHttpMessageReader partEventHttpMessageReader) map.from(multipartProperties::getMaxHeadersSize) .asInt(DataSize::toBytes) .to(partEventHttpMessageReader::setMaxHeadersSize); + map.from(multipartProperties::getMaxDiskUsagePerPart) + .as(DataSize::toBytes) + .to(partEventHttpMessageReader::setMaxPartSize); + map.from(multipartProperties::getMaxParts).to(partEventHttpMessageReader::setMaxParts); map.from(multipartProperties::getHeadersCharset).to(partEventHttpMessageReader::setHeadersCharset); } }); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartProperties.java index b1bd6d7854d9..01b4e005a2eb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -37,7 +37,7 @@ public class ReactiveMultipartProperties { /** * Maximum amount of memory allowed per part before it's written to disk. Set to -1 to - * store all contents in memory. Ignored when streaming is enabled. + * store all contents in memory. */ private DataSize maxInMemorySize = DataSize.ofKilobytes(256); @@ -49,7 +49,7 @@ public class ReactiveMultipartProperties { /** * Maximum amount of disk space allowed per part. Default is -1 which enforces no - * limits. Ignored when streaming is enabled. + * limits. */ private DataSize maxDiskUsagePerPart = DataSize.ofBytes(-1); @@ -62,7 +62,7 @@ public class ReactiveMultipartProperties { /** * Directory used to store file parts larger than 'maxInMemorySize'. Default is a * directory named 'spring-multipart' created under the system temporary directory. - * Ignored when streaming is enabled. + * Ignored when using the PartEvent streaming support. */ private String fileStorageDirectory; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfigurationTests.java index d5925346e13f..ae3d0099ce22 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/ReactiveMultipartAutoConfigurationTests.java @@ -84,17 +84,21 @@ void shouldConfigureMultipartPropertiesForDefaultReader() { void shouldConfigureMultipartPropertiesForPartEventReader() { this.contextRunner .withPropertyValues("spring.webflux.multipart.max-in-memory-size=1GB", - "spring.webflux.multipart.max-headers-size=16KB", "spring.webflux.multipart.headers-charset:UTF_16") + "spring.webflux.multipart.max-headers-size=16KB", + "spring.webflux.multipart.max-disk-usage-per-part=3GB", "spring.webflux.multipart.max-parts=7", + "spring.webflux.multipart.headers-charset:UTF_16") .run((context) -> { CodecCustomizer customizer = context.getBean(CodecCustomizer.class); DefaultServerCodecConfigurer configurer = new DefaultServerCodecConfigurer(); customizer.customize(configurer); PartEventHttpMessageReader partReader = getPartEventReader(configurer); + assertThat(partReader).hasFieldOrPropertyWithValue("maxParts", 7); assertThat(partReader).hasFieldOrPropertyWithValue("maxHeadersSize", Math.toIntExact(DataSize.ofKilobytes(16).toBytes())); assertThat(partReader).hasFieldOrPropertyWithValue("headersCharset", StandardCharsets.UTF_16); assertThat(partReader).hasFieldOrPropertyWithValue("maxInMemorySize", Math.toIntExact(DataSize.ofGigabytes(1).toBytes())); + assertThat(partReader).hasFieldOrPropertyWithValue("maxPartSize", DataSize.ofGigabytes(3).toBytes()); }); } From a3458cbf8e03da0f6cad90c784f66052bf285deb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 10 Nov 2023 14:51:59 +0000 Subject: [PATCH 0746/1215] Start building against Micrometer 1.12.0 snapshots See gh-38305 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 25e4b0fadc92..bef9882c0ff0 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -988,7 +988,7 @@ bom { ] } } - library("Micrometer", "1.12.0-RC1") { + library("Micrometer", "1.12.0-SNAPSHOT") { considerSnapshots() group("io.micrometer") { modules = [ From acca3d8697f25bb94e9e8a457ff11fdad228a566 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 10 Nov 2023 14:52:04 +0000 Subject: [PATCH 0747/1215] Start building against Micrometer Tracing 1.2.0 snapshots See gh-38306 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index bef9882c0ff0..c55d04c30051 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1001,7 +1001,7 @@ bom { ] } } - library("Micrometer Tracing", "1.2.0-RC1") { + library("Micrometer Tracing", "1.2.0-SNAPSHOT") { considerSnapshots() calendarName = "Tracing" group("io.micrometer") { From c9f982bb5ac93036be43648a87d05d1a16a45e76 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 10 Nov 2023 14:52:09 +0000 Subject: [PATCH 0748/1215] Start building against Reactor Bom 2023.0.0 snapshots See gh-38307 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c55d04c30051..0a23498e09d8 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1316,7 +1316,7 @@ bom { ] } } - library("Reactor Bom", "2023.0.0-RC1") { + library("Reactor Bom", "2023.0.0-SNAPSHOT") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From 28aa3c012aec395ca3b313289f2fdefc31d69fd8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 10 Nov 2023 14:52:13 +0000 Subject: [PATCH 0749/1215] Start building against Spring AMQP 3.1.0 snapshots See gh-38308 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 0a23498e09d8..aeb1c39b0f19 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1484,7 +1484,7 @@ bom { ] } } - library("Spring AMQP", "3.1.0-RC1") { + library("Spring AMQP", "3.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.amqp") { imports = [ From 259c2c469190ed38a47bcc8b8f6f4dc13c058140 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 10 Nov 2023 14:52:18 +0000 Subject: [PATCH 0750/1215] Start building against Spring Authorization Server 1.2.0 snapshots See gh-38309 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index aeb1c39b0f19..8dd0b08ad9e0 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1492,7 +1492,7 @@ bom { ] } } - library("Spring Authorization Server", "1.2.0-RC1") { + library("Spring Authorization Server", "1.2.0-SNAPSHOT") { considerSnapshots() group("org.springframework.security") { modules = [ From 986d4862b56028878a0ed57a8a6cfab3698060af Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 10 Nov 2023 14:52:23 +0000 Subject: [PATCH 0751/1215] Start building against Spring Batch 5.1.0 snapshots See gh-38310 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 8dd0b08ad9e0..f521f060b48e 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1500,7 +1500,7 @@ bom { ] } } - library("Spring Batch", "5.1.0-RC1") { + library("Spring Batch", "5.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.batch") { imports = [ From 9600bbcf81d11f0e0b2f2d7f77492c90f60d7cd1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 10 Nov 2023 14:52:28 +0000 Subject: [PATCH 0752/1215] Start building against Spring Data Bom 2023.1.0 snapshots See gh-38311 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f521f060b48e..95324ba11876 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1508,7 +1508,7 @@ bom { ] } } - library("Spring Data Bom", "2023.1.0-RC1") { + library("Spring Data Bom", "2023.1.0-SNAPSHOT") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From 482d2d3d48d169b1d11b19b11b093a9f718907bf Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 10 Nov 2023 14:52:33 +0000 Subject: [PATCH 0753/1215] Start building against Spring Framework 6.1.0 snapshots See gh-38312 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 4bcfd9ba55db..fcd518d75261 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ junitJupiterVersion=5.10.0 kotlinVersion=1.9.20 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.28 -springFrameworkVersion=6.1.0-RC2 +springFrameworkVersion=6.1.0-SNAPSHOT tomcatVersion=10.1.15 kotlin.stdlib.default.dependency=false From d10b875d44f5060d3405c9a0c1c0bff951ba0110 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 10 Nov 2023 14:52:37 +0000 Subject: [PATCH 0754/1215] Start building against Spring GraphQL 1.2.4 snapshots See gh-38313 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 95324ba11876..4909c8f42500 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1526,7 +1526,7 @@ bom { ] } } - library("Spring GraphQL", "1.2.3") { + library("Spring GraphQL", "1.2.4-SNAPSHOT") { considerSnapshots() group("org.springframework.graphql") { modules = [ From 18f1cc56f45943ff6d063546227245db5d044615 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 10 Nov 2023 14:52:42 +0000 Subject: [PATCH 0755/1215] Start building against Spring HATEOAS 2.2.0 snapshots See gh-38314 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 4909c8f42500..2160229df45a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1535,7 +1535,7 @@ bom { ] } } - library("Spring HATEOAS", "2.2.0-RC1") { + library("Spring HATEOAS", "2.2.0-SNAPSHOT") { considerSnapshots() group("org.springframework.hateoas") { modules = [ From d5dc1dcea0b48b486d0d6059d0bf6883b5a53a83 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 10 Nov 2023 14:52:47 +0000 Subject: [PATCH 0756/1215] Start building against Spring Integration 6.2.0 snapshots See gh-38315 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 2160229df45a..ee0b29b41079 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1543,7 +1543,7 @@ bom { ] } } - library("Spring Integration", "6.2.0-RC1") { + library("Spring Integration", "6.2.0-SNAPSHOT") { considerSnapshots() group("org.springframework.integration") { imports = [ From 306353908485df4167e428bf4254c2405ece1a1d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 10 Nov 2023 14:52:52 +0000 Subject: [PATCH 0757/1215] Start building against Spring Kafka 3.1.0 snapshots See gh-38316 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index ee0b29b41079..9d9c36df9eab 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1551,7 +1551,7 @@ bom { ] } } - library("Spring Kafka", "3.1.0-RC1") { + library("Spring Kafka", "3.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.kafka") { modules = [ From f7efa71ea62eb4bbefb2bc7883cb0361cf9c2eaf Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 10 Nov 2023 14:52:56 +0000 Subject: [PATCH 0758/1215] Start building against Spring LDAP 3.2.0 snapshots See gh-38317 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 9d9c36df9eab..9944aa58ec8a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1560,7 +1560,7 @@ bom { ] } } - library("Spring LDAP", "3.2.0-RC1") { + library("Spring LDAP", "3.2.0-SNAPSHOT") { considerSnapshots() group("org.springframework.ldap") { modules = [ From 47bf63ae92083ba1a6d686afdece43a18b8b5576 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 10 Nov 2023 14:53:01 +0000 Subject: [PATCH 0759/1215] Start building against Spring Security 6.2.0 snapshots See gh-38318 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 9944aa58ec8a..8a3b335c6090 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1597,7 +1597,7 @@ bom { ] } } - library("Spring Security", "6.2.0-RC2") { + library("Spring Security", "6.2.0-SNAPSHOT") { considerSnapshots() group("org.springframework.security") { imports = [ From 2eaf6726f30cffe53ecc045d5f89394d1e1f0cd2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 10 Nov 2023 14:53:06 +0000 Subject: [PATCH 0760/1215] Start building against Spring Session 3.2.0 snapshots See gh-38319 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 8a3b335c6090..752d313842fd 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1605,7 +1605,7 @@ bom { ] } } - library("Spring Session", "3.2.0-RC1") { + library("Spring Session", "3.2.0-SNAPSHOT") { considerSnapshots() prohibit { startsWith(["Apple-", "Bean-", "Corn-", "Dragonfruit-"]) From e4a0e9d9acf90b2f16656e10211b7e72476acad6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 10 Nov 2023 14:53:10 +0000 Subject: [PATCH 0761/1215] Start building against Spring WS 4.0.8 snapshots See gh-38320 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 752d313842fd..caa7dfd5ecb9 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1617,7 +1617,7 @@ bom { ] } } - library("Spring WS", "4.0.7") { + library("Spring WS", "4.0.8-SNAPSHOT") { considerSnapshots() group("org.springframework.ws") { imports = [ From adb841c45ed8f8a6102614825999dea27932cfda Mon Sep 17 00:00:00 2001 From: Lars Uffmann Date: Sat, 11 Nov 2023 09:15:22 +0100 Subject: [PATCH 0762/1215] Update JobLauncherApplicationRunner to use getIdentifyingParameters See gh-38327 --- .../batch/JobLauncherApplicationRunner.java | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JobLauncherApplicationRunner.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JobLauncherApplicationRunner.java index b4415ac365ef..a343346eb3e6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JobLauncherApplicationRunner.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JobLauncherApplicationRunner.java @@ -19,7 +19,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Properties; @@ -230,7 +229,8 @@ private JobParameters getNextJobParameters(Job job, JobParameters jobParameters) private JobParameters getNextJobParametersForExisting(Job job, JobParameters jobParameters) { JobExecution lastExecution = this.jobRepository.getLastJobExecution(job.getName(), jobParameters); if (isStoppedOrFailed(lastExecution) && job.isRestartable()) { - JobParameters previousIdentifyingParameters = getGetIdentifying(lastExecution.getJobParameters()); + JobParameters previousIdentifyingParameters = new JobParameters( + lastExecution.getJobParameters().getIdentifyingParameters()); return merge(previousIdentifyingParameters, jobParameters); } return jobParameters; @@ -241,16 +241,6 @@ private boolean isStoppedOrFailed(JobExecution execution) { return (status == BatchStatus.STOPPED || status == BatchStatus.FAILED); } - private JobParameters getGetIdentifying(JobParameters parameters) { - HashMap> nonIdentifying = new LinkedHashMap<>(parameters.getParameters().size()); - parameters.getParameters().forEach((key, value) -> { - if (value.isIdentifying()) { - nonIdentifying.put(key, value); - } - }); - return new JobParameters(nonIdentifying); - } - private JobParameters merge(JobParameters parameters, JobParameters additionals) { Map> merged = new LinkedHashMap<>(); merged.putAll(parameters.getParameters()); From 28b4298cdd9ab82462264889cec61de7f92e3952 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 13 Nov 2023 15:57:09 +0000 Subject: [PATCH 0763/1215] Upgrade to Dependency Management Plugin 1.1.4 Closes gh-38346 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index caa7dfd5ecb9..3654388a9b16 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -221,7 +221,7 @@ bom { ] } } - library("Dependency Management Plugin", "1.1.3") { + library("Dependency Management Plugin", "1.1.4") { group("io.spring.gradle") { modules = [ "dependency-management-plugin" From a6c4ea7e8c21df9073b274ec2fe101e0d079b068 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 13 Nov 2023 15:57:09 +0000 Subject: [PATCH 0764/1215] Upgrade to Micrometer 1.12.0 Closes gh-38305 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 3654388a9b16..6b991be7574a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -988,7 +988,7 @@ bom { ] } } - library("Micrometer", "1.12.0-SNAPSHOT") { + library("Micrometer", "1.12.0") { considerSnapshots() group("io.micrometer") { modules = [ From da67ce4a76091398ad336b49f4964a1b210568bb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 13 Nov 2023 15:57:10 +0000 Subject: [PATCH 0765/1215] Upgrade to Micrometer Tracing 1.2.0 Closes gh-38306 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 6b991be7574a..aa1880fc4894 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1001,7 +1001,7 @@ bom { ] } } - library("Micrometer Tracing", "1.2.0-SNAPSHOT") { + library("Micrometer Tracing", "1.2.0") { considerSnapshots() calendarName = "Tracing" group("io.micrometer") { From ba56953ea554cf42f81504ee4d31e819900abb51 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 13 Nov 2023 10:24:06 -0800 Subject: [PATCH 0766/1215] Skip ValueObjectBinder if parameter names cannot be discovered Update `ValueObjectBinder` so that it is skipped if parameter names cannot be discovered. This is much more likely as of Since Spring Framework 6.1 as it no longer performs ASM parsing to discover names. Fixes gh-38201 --- .../properties/bind/ValueObjectBinder.java | 41 ++++++++++------- .../bind/ValueObjectBinderTests.java | 45 +++++++++++++++++++ 2 files changed, 71 insertions(+), 15 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java index 7a68330b7b28..5f51e91f3410 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java @@ -31,6 +31,8 @@ import kotlin.reflect.KFunction; import kotlin.reflect.KParameter; import kotlin.reflect.jvm.ReflectJvmMapping; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeanUtils; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; @@ -43,6 +45,7 @@ import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.convert.ConversionException; +import org.springframework.core.log.LogMessage; import org.springframework.util.Assert; /** @@ -55,6 +58,8 @@ */ class ValueObjectBinder implements DataObjectBinder { + private static final Log logger = LogFactory.getLog(ValueObjectBinder.class); + private final BindConstructorProvider constructorProvider; ValueObjectBinder(BindConstructorProvider constructorProvider) { @@ -261,15 +266,31 @@ private static final class DefaultValueObject extends ValueObject { private final List constructorParameters; - private DefaultValueObject(Constructor constructor, ResolvableType type) { + private DefaultValueObject(Constructor constructor, List constructorParameters) { super(constructor); - this.constructorParameters = parseConstructorParameters(constructor, type); + this.constructorParameters = constructorParameters; + } + + @Override + List getConstructorParameters() { + return this.constructorParameters; + } + + @SuppressWarnings("unchecked") + static ValueObject get(Constructor bindConstructor, ResolvableType type) { + String[] names = PARAMETER_NAME_DISCOVERER.getParameterNames(bindConstructor); + if (names == null) { + logger.debug(LogMessage.format( + "Unable to use value object binding with %s as parameter names cannot be discovered", + bindConstructor)); + return null; + } + List constructorParameters = parseConstructorParameters(bindConstructor, type, names); + return new DefaultValueObject<>((Constructor) bindConstructor, constructorParameters); } private static List parseConstructorParameters(Constructor constructor, - ResolvableType type) { - String[] names = PARAMETER_NAME_DISCOVERER.getParameterNames(constructor); - Assert.state(names != null, () -> "Failed to extract parameter names for " + constructor); + ResolvableType type, String[] names) { Parameter[] parameters = constructor.getParameters(); List result = new ArrayList<>(parameters.length); for (int i = 0; i < parameters.length; i++) { @@ -285,16 +306,6 @@ private static List parseConstructorParameters(Constructor return Collections.unmodifiableList(result); } - @Override - List getConstructorParameters() { - return this.constructorParameters; - } - - @SuppressWarnings("unchecked") - static ValueObject get(Constructor bindConstructor, ResolvableType type) { - return new DefaultValueObject<>((Constructor) bindConstructor, type); - } - } /** diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java index aeffd12431fb..a47f30aa0fcb 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java @@ -26,11 +26,13 @@ import java.util.Objects; import java.util.Optional; +import com.jayway.jsonpath.JsonPath; import org.junit.jupiter.api.Test; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.context.properties.source.ConfigurationPropertySource; import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; +import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.core.test.tools.SourceFile; @@ -391,6 +393,25 @@ public record RecordProperties( }); } + @Test // gh-38201 + void bindWithNonExtractableParameterNamesAndNonIterablePropertySource() throws Exception { + verifyJsonPathParametersCannotBeResolved(); + MockConfigurationPropertySource source = new MockConfigurationPropertySource(); + source.put("test.value", "test"); + this.sources.add(source.nonIterable()); + Bindable target = Bindable.of(NonExtractableParameterName.class); + NonExtractableParameterName bound = this.binder.bindOrCreate("test", target); + assertThat(bound.getValue()).isEqualTo("test"); + } + + private void verifyJsonPathParametersCannotBeResolved() throws NoSuchFieldException { + Class jsonPathClass = NonExtractableParameterName.class.getDeclaredField("jsonPath").getType(); + Constructor[] constructors = jsonPathClass.getDeclaredConstructors(); + assertThat(constructors).hasSize(1); + constructors[0].setAccessible(true); + assertThat(new DefaultParameterNameDiscoverer().getParameterNames(constructors[0])).isNull(); + } + private void noConfigurationProperty(BindException ex) { assertThat(ex.getProperty()).isNull(); } @@ -845,4 +866,28 @@ String getImportName() { } + static class NonExtractableParameterName { + + private String value; + + private JsonPath jsonPath; + + String getValue() { + return this.value; + } + + void setValue(String value) { + this.value = value; + } + + JsonPath getJsonPath() { + return this.jsonPath; + } + + void setJsonPath(JsonPath jsonPath) { + this.jsonPath = jsonPath; + } + + } + } From 80210e93d337be686b01b222a41e718e8ea22c9b Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 13 Nov 2023 10:40:24 -0800 Subject: [PATCH 0767/1215] Make LogCorrelationPropertySource an EnumerablePropertySource Change `LogCorrelationPropertySource` to an `EnumerablePropertySource` to reduce the likelihood of `Binder` errors. Closes gh-38349 --- .../LogCorrelationEnvironmentPostProcessor.java | 8 +++++++- .../LogCorrelationEnvironmentPostProcessorTests.java | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessor.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessor.java index 50a92939f4f5..b0479bd29c3f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessor.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessor.java @@ -20,6 +20,7 @@ import org.springframework.boot.env.EnvironmentPostProcessor; import org.springframework.boot.logging.LoggingSystem; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.Environment; import org.springframework.core.env.PropertySource; import org.springframework.util.ClassUtils; @@ -45,7 +46,7 @@ public void postProcessEnvironment(ConfigurableEnvironment environment, SpringAp /** * Log correlation {@link PropertySource}. */ - private static class LogCorrelationPropertySource extends PropertySource { + private static class LogCorrelationPropertySource extends EnumerablePropertySource { private static final String NAME = "logCorrelation"; @@ -56,6 +57,11 @@ private static class LogCorrelationPropertySource extends PropertySource this.environment = environment; } + @Override + public String[] getPropertyNames() { + return new String[] { LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY }; + } + @Override public Object getProperty(String name) { if (name.equals(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY)) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessorTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessorTests.java index 4bbfa4a0e8d9..3cf84a10db6f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessorTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/LogCorrelationEnvironmentPostProcessorTests.java @@ -23,6 +23,8 @@ import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.boot.testsupport.classpath.ClassPathExclusions; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.core.env.PropertySource; import org.springframework.core.env.StandardEnvironment; import static org.assertj.core.api.Assertions.assertThat; @@ -64,4 +66,13 @@ void getExpectCorrelationIdPropertyWhenTracingDisabledReturnsFalse() { .isFalse(); } + @Test + void postProcessEnvironmentAddsEnumerablePropertySource() { + this.postProcessor.postProcessEnvironment(this.environment, this.application); + PropertySource propertySource = this.environment.getPropertySources().get("logCorrelation"); + assertThat(propertySource).isInstanceOf(EnumerablePropertySource.class); + assertThat(((EnumerablePropertySource) propertySource).getPropertyNames()) + .containsExactly(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY); + } + } From e88bab69f6520ab48036f5762cb60d76d8d16bc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Tue, 14 Nov 2023 09:09:54 +0100 Subject: [PATCH 0768/1215] Document Liberica JDK with CRaC See gh-38350 --- .../src/docs/asciidoc/deployment/efficient.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc index be9b80f178c5..1ecbedd6af9c 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc @@ -72,7 +72,7 @@ To learn more about ahead-of-time processing, please see the < Date: Tue, 14 Nov 2023 20:47:00 +0000 Subject: [PATCH 0769/1215] Upgrade to Reactor Bom 2023.0.0 Closes gh-38307 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 518c0389b6cf..7b617df4110b 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1316,7 +1316,7 @@ bom { ] } } - library("Reactor Bom", "2023.0.0-SNAPSHOT") { + library("Reactor Bom", "2023.0.0") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From d6f67b02f7febad2834f46f088604a106cd843fe Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 15 Nov 2023 09:13:15 +0100 Subject: [PATCH 0770/1215] Clarify which tracing components are disabled when using tracing in tests See gh-33975 --- .../src/docs/asciidoc/actuator/tracing.adoc | 2 +- .../src/docs/asciidoc/features/testing.adoc | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc index 827126b38bb1..b3034f104629 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc @@ -202,5 +202,5 @@ For the example above, setting this property to `baggage1` results in an MDC ent [[actuator.micrometer-tracing.tests]] === Tests -Tracing is not auto-configured when using `@SpringBootTest`. +Tracing components which are reporting data are not auto-configured when using `@SpringBootTest`. See <> for more details. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc index 32cebc48fbf9..d39d902b31bd 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc @@ -266,9 +266,11 @@ If you need to export metrics to a different backend as part of an integration t [[features.testing.spring-boot-applications.tracing]] ==== Using Tracing -Regardless of your classpath, tracing is not auto-configured when using `@SpringBootTest`. +Regardless of your classpath, tracing components which are reporting data are not auto-configured when using `@SpringBootTest`. -If you need tracing as part of an integration test, annotate it with `@AutoConfigureObservability`. +If you need those components as part of an integration test, annotate the test with `@AutoConfigureObservability`. + +If you have created your own reporting components (e.g. a custom `SpanExporter` or `SpanHandler`) and you don't want them to be active in tests, you can use the `@ConditionalOnEnabledTracing` annotation to disable them. From f62c1188a19100727254b1c8c4708f1760e43005 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 15 Nov 2023 10:12:11 +0100 Subject: [PATCH 0771/1215] Close meter registries early in the shutdown process Closes gh-38240 Co-authored-by: Phillip Webb --- .../metrics/MetricsAutoConfiguration.java | 47 +++++++++++++++++++ .../MetricsAutoConfigurationTests.java | 20 ++++++++ 2 files changed, 67 insertions(+) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.java index 16eaab791d98..fc695471f7d2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.java @@ -16,8 +16,11 @@ package org.springframework.boot.actuate.autoconfigure.metrics; +import java.util.List; + import io.micrometer.core.annotation.Timed; import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.binder.MeterBinder; import io.micrometer.core.instrument.config.MeterFilter; @@ -28,6 +31,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; +import org.springframework.context.SmartLifecycle; import org.springframework.context.annotation.Bean; import org.springframework.core.annotation.Order; @@ -36,6 +40,8 @@ * * @author Jon Schneider * @author Stephane Nicoll + * @author Phil Webb + * @author Moritz Halbritter * @since 2.0.0 */ @AutoConfiguration(before = CompositeMeterRegistryAutoConfiguration.class) @@ -64,4 +70,45 @@ public PropertiesMeterFilter propertiesMeterFilter(MetricsProperties properties) return new PropertiesMeterFilter(properties); } + @Bean + MeterRegistryLifecycle meterRegistryLifecycle(ObjectProvider meterRegistries) { + return new MeterRegistryLifecycle(meterRegistries.orderedStream().toList()); + } + + /** + * Ensures that {@link MeterRegistry meter registries} are closed early in the + * shutdown process. + */ + static class MeterRegistryLifecycle implements SmartLifecycle { + + private volatile boolean running; + + private final List meterRegistries; + + MeterRegistryLifecycle(List meterRegistries) { + this.meterRegistries = meterRegistries; + } + + @Override + public void start() { + this.running = true; + } + + @Override + public void stop() { + this.running = false; + this.meterRegistries.forEach((registry) -> { + if (!registry.isClosed()) { + registry.close(); + } + }); + } + + @Override + public boolean isRunning() { + return this.running; + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java index c04c31435a72..cd7a84d4550f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java @@ -16,15 +16,19 @@ package org.springframework.boot.actuate.autoconfigure.metrics; +import java.util.List; + import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.binder.MeterBinder; +import io.micrometer.core.instrument.composite.CompositeMeterRegistry; import io.micrometer.core.instrument.config.MeterFilter; import io.micrometer.core.instrument.config.MeterFilterReply; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration.MeterRegistryLifecycle; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -40,6 +44,7 @@ * Tests for {@link MetricsAutoConfiguration}. * * @author Andy Wilkinson + * @author Moritz Halbritter */ class MetricsAutoConfigurationTests { @@ -72,6 +77,21 @@ void configuresMeterRegistries() { }); } + @Test + void shouldSupplyMeterRegistryLifecycle() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(MeterRegistryLifecycle.class)); + } + + @Test + void meterRegistryLifecycleShouldCloseRegistryOnShutdown() { + MeterRegistry meterRegistry = new CompositeMeterRegistry(); + assertThat(meterRegistry.isClosed()).isFalse(); + MeterRegistryLifecycle lifecycle = new MeterRegistryLifecycle(List.of(meterRegistry)); + lifecycle.start(); + lifecycle.stop(); + assertThat(meterRegistry.isClosed()).isTrue(); + } + @Configuration(proxyBeanMethods = false) static class CustomClockConfiguration { From 7a8a3931540a5686b9d6aa2ffea5801fd1b739f1 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 15 Nov 2023 14:17:07 +0100 Subject: [PATCH 0772/1215] Revert "Close meter registries early in the shutdown process" This reverts commit f62c1188a19100727254b1c8c4708f1760e43005. --- .../metrics/MetricsAutoConfiguration.java | 47 ------------------- .../MetricsAutoConfigurationTests.java | 20 -------- 2 files changed, 67 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.java index fc695471f7d2..16eaab791d98 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.java @@ -16,11 +16,8 @@ package org.springframework.boot.actuate.autoconfigure.metrics; -import java.util.List; - import io.micrometer.core.annotation.Timed; import io.micrometer.core.instrument.Clock; -import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.binder.MeterBinder; import io.micrometer.core.instrument.config.MeterFilter; @@ -31,7 +28,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; -import org.springframework.context.SmartLifecycle; import org.springframework.context.annotation.Bean; import org.springframework.core.annotation.Order; @@ -40,8 +36,6 @@ * * @author Jon Schneider * @author Stephane Nicoll - * @author Phil Webb - * @author Moritz Halbritter * @since 2.0.0 */ @AutoConfiguration(before = CompositeMeterRegistryAutoConfiguration.class) @@ -70,45 +64,4 @@ public PropertiesMeterFilter propertiesMeterFilter(MetricsProperties properties) return new PropertiesMeterFilter(properties); } - @Bean - MeterRegistryLifecycle meterRegistryLifecycle(ObjectProvider meterRegistries) { - return new MeterRegistryLifecycle(meterRegistries.orderedStream().toList()); - } - - /** - * Ensures that {@link MeterRegistry meter registries} are closed early in the - * shutdown process. - */ - static class MeterRegistryLifecycle implements SmartLifecycle { - - private volatile boolean running; - - private final List meterRegistries; - - MeterRegistryLifecycle(List meterRegistries) { - this.meterRegistries = meterRegistries; - } - - @Override - public void start() { - this.running = true; - } - - @Override - public void stop() { - this.running = false; - this.meterRegistries.forEach((registry) -> { - if (!registry.isClosed()) { - registry.close(); - } - }); - } - - @Override - public boolean isRunning() { - return this.running; - } - - } - } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java index cd7a84d4550f..c04c31435a72 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java @@ -16,19 +16,15 @@ package org.springframework.boot.actuate.autoconfigure.metrics; -import java.util.List; - import io.micrometer.core.instrument.Clock; import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.binder.MeterBinder; -import io.micrometer.core.instrument.composite.CompositeMeterRegistry; import io.micrometer.core.instrument.config.MeterFilter; import io.micrometer.core.instrument.config.MeterFilterReply; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.Test; -import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration.MeterRegistryLifecycle; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -44,7 +40,6 @@ * Tests for {@link MetricsAutoConfiguration}. * * @author Andy Wilkinson - * @author Moritz Halbritter */ class MetricsAutoConfigurationTests { @@ -77,21 +72,6 @@ void configuresMeterRegistries() { }); } - @Test - void shouldSupplyMeterRegistryLifecycle() { - this.contextRunner.run((context) -> assertThat(context).hasSingleBean(MeterRegistryLifecycle.class)); - } - - @Test - void meterRegistryLifecycleShouldCloseRegistryOnShutdown() { - MeterRegistry meterRegistry = new CompositeMeterRegistry(); - assertThat(meterRegistry.isClosed()).isFalse(); - MeterRegistryLifecycle lifecycle = new MeterRegistryLifecycle(List.of(meterRegistry)); - lifecycle.start(); - lifecycle.stop(); - assertThat(meterRegistry.isClosed()).isTrue(); - } - @Configuration(proxyBeanMethods = false) static class CustomClockConfiguration { From 51f13404a5e4795a3aead9941015cf395fcc16b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edd=C3=BA=20Mel=C3=A9ndez?= Date: Mon, 13 Nov 2023 20:24:37 -0600 Subject: [PATCH 0773/1215] Use KafkaContainer in smoke test for Kafka with SSL See gh-38359 --- .../spring-boot-dependencies/build.gradle | 2 +- .../spring-boot-smoke-test-kafka/build.gradle | 1 + .../ssl/SampleKafkaSslApplicationTests.java | 42 +++++++++++++------ .../src/test/resources/docker-compose.yml | 30 ------------- 4 files changed, 31 insertions(+), 44 deletions(-) delete mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/resources/docker-compose.yml diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 7b617df4110b..4a0aff703dc0 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1632,7 +1632,7 @@ bom { ] } } - library("Testcontainers", "1.19.1") { + library("Testcontainers", "1.19.2") { group("org.testcontainers") { imports = [ "testcontainers-bom" diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/build.gradle index 426a515062e1..c50177f4ca0e 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/build.gradle @@ -15,4 +15,5 @@ dependencies { exclude group: "commons-logging", module: "commons-logging" } testImplementation("org.testcontainers:junit-jupiter") + testImplementation("org.testcontainers:kafka") } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/java/smoketest/kafka/ssl/SampleKafkaSslApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/java/smoketest/kafka/ssl/SampleKafkaSslApplicationTests.java index 8ffe00221173..2a5a60469a81 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/java/smoketest/kafka/ssl/SampleKafkaSslApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/java/smoketest/kafka/ssl/SampleKafkaSslApplicationTests.java @@ -16,21 +16,23 @@ package smoketest.kafka.ssl; -import java.io.File; import java.time.Duration; import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; -import org.testcontainers.containers.DockerComposeContainer; -import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.containers.KafkaContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; import smoketest.kafka.Consumer; import smoketest.kafka.Producer; import smoketest.kafka.SampleMessage; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.empty; @@ -38,23 +40,37 @@ @Testcontainers(disabledWithoutDocker = true) @SpringBootTest(classes = { SampleKafkaSslApplication.class, Producer.class, Consumer.class }, - properties = { "spring.kafka.security.protocol=SSL", "spring.kafka.bootstrap-servers=localhost:9093", - "spring.kafka.ssl.bundle=client", + properties = { "spring.kafka.security.protocol=SSL", + "spring.kafka.properties.ssl.endpoint.identification.algorithm=", "spring.kafka.ssl.bundle=client", "spring.ssl.bundle.jks.client.keystore.location=classpath:ssl/test-client.p12", "spring.ssl.bundle.jks.client.keystore.password=password", "spring.ssl.bundle.jks.client.truststore.location=classpath:ssl/test-ca.p12", "spring.ssl.bundle.jks.client.truststore.password=password" }) class SampleKafkaSslApplicationTests { - private static final File KAFKA_COMPOSE_FILE = new File("src/test/resources/docker-compose.yml"); - - private static final String KAFKA_COMPOSE_SERVICE = "kafka"; - - private static final int KAFKA_SSL_PORT = 9093; - @Container - public DockerComposeContainer container = new DockerComposeContainer<>(KAFKA_COMPOSE_FILE) - .withExposedService(KAFKA_COMPOSE_SERVICE, KAFKA_SSL_PORT, Wait.forListeningPorts(KAFKA_SSL_PORT)); + public static KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.4.0")) + .withEnv("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", "PLAINTEXT:SSL,BROKER:PLAINTEXT") + .withEnv("KAFKA_AUTO_CREATE_TOPICS_ENABLE", "true") + .withEnv("KAFKA_SSL_CLIENT_AUTH", "required") + .withEnv("KAFKA_SSL_KEYSTORE_LOCATION", "/etc/kafka/secrets/certs/test-server.p12") + .withEnv("KAFKA_SSL_KEYSTORE_PASSWORD", "password") + .withEnv("KAFKA_SSL_KEY_PASSWORD", "password") + .withEnv("KAFKA_SSL_TRUSTSTORE_LOCATION", "/etc/kafka/secrets/certs/test-ca.p12") + .withEnv("KAFKA_SSL_TRUSTSTORE_PASSWORD", "password") + .withEnv("KAFKA_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM", "") + .withCopyFileToContainer(MountableFile.forClasspathResource("ssl/test-server.p12"), + "/etc/kafka/secrets/certs/test-server.p12") + .withCopyFileToContainer(MountableFile.forClasspathResource("ssl/credentials"), + "/etc/kafka/secrets/certs/credentials") + .withCopyFileToContainer(MountableFile.forClasspathResource("ssl/test-ca.p12"), + "/etc/kafka/secrets/certs/test-ca.p12"); + + @DynamicPropertySource + static void kafkaProperties(DynamicPropertyRegistry registry) { + registry.add("spring.kafka.bootstrap-servers", + () -> String.format("%s:%s", kafka.getHost(), kafka.getMappedPort(9093))); + } @Autowired private Producer producer; diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/resources/docker-compose.yml b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/resources/docker-compose.yml deleted file mode 100644 index 326e5a30c030..000000000000 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/resources/docker-compose.yml +++ /dev/null @@ -1,30 +0,0 @@ ---- -version: '2' -services: - zookeeper: - image: confluentinc/cp-zookeeper:7.4.0 - environment: - ZOOKEEPER_CLIENT_PORT: 2181 - ZOOKEEPER_TICK_TIME: 2000 - - kafka: - image: confluentinc/cp-kafka:7.4.0 - depends_on: - - zookeeper - ports: - - "9092:9092" - - "9093:9093" - environment: - KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,SSL://localhost:9093 - KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true' - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - KAFKA_SSL_CLIENT_AUTH: "required" - KAFKA_SSL_KEYSTORE_FILENAME: '/certs/test-server.p12' - KAFKA_SSL_KEYSTORE_CREDENTIALS: '/certs/credentials' - KAFKA_SSL_KEY_CREDENTIALS: '/certs/credentials' - KAFKA_SSL_TRUSTSTORE_FILENAME: '/certs/test-ca.p12' - KAFKA_SSL_TRUSTSTORE_CREDENTIALS: '/certs/credentials' - volumes: - - ./ssl/:/etc/kafka/secrets/certs From 586bb26eff74e12d16e409da474552e689f3a71f Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Wed, 15 Nov 2023 15:25:20 -0600 Subject: [PATCH 0774/1215] Polish "Use KafkaContainer in smoke test for Kafka with SSL" See gh-38359 --- .../spring-boot-smoke-test-kafka/build.gradle | 1 + .../kafka/ssl/SampleKafkaSslApplicationTests.java | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/build.gradle index c50177f4ca0e..24d8f33d79d1 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/build.gradle @@ -10,6 +10,7 @@ dependencies { implementation("org.springframework.kafka:spring-kafka") testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) + testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) testImplementation("org.awaitility:awaitility") testImplementation("org.springframework.kafka:spring-kafka-test") { exclude group: "commons-logging", module: "commons-logging" diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/java/smoketest/kafka/ssl/SampleKafkaSslApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/java/smoketest/kafka/ssl/SampleKafkaSslApplicationTests.java index 2a5a60469a81..433c9e0a0f9b 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/java/smoketest/kafka/ssl/SampleKafkaSslApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-kafka/src/test/java/smoketest/kafka/ssl/SampleKafkaSslApplicationTests.java @@ -23,7 +23,6 @@ import org.testcontainers.containers.KafkaContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; import smoketest.kafka.Consumer; import smoketest.kafka.Producer; @@ -31,6 +30,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; @@ -38,6 +38,12 @@ import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.not; +/** + * Smoke tests for Apache Kafka with SSL. + * + * @author Scott Frederick + * @author Eddú Meléndez + */ @Testcontainers(disabledWithoutDocker = true) @SpringBootTest(classes = { SampleKafkaSslApplication.class, Producer.class, Consumer.class }, properties = { "spring.kafka.security.protocol=SSL", @@ -49,7 +55,7 @@ class SampleKafkaSslApplicationTests { @Container - public static KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.4.0")) + public static KafkaContainer kafka = new KafkaContainer(DockerImageNames.kafka()) .withEnv("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", "PLAINTEXT:SSL,BROKER:PLAINTEXT") .withEnv("KAFKA_AUTO_CREATE_TOPICS_ENABLE", "true") .withEnv("KAFKA_SSL_CLIENT_AUTH", "required") From d8ce9010111444250a530e9aaeb718a25074f9b2 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 16 Nov 2023 09:05:14 +0100 Subject: [PATCH 0775/1215] Close meter registries early in the shutdown process Closes gh-38240 --- .../metrics/MetricsAutoConfiguration.java | 34 +++++++++++++++++++ .../MetricsAutoConfigurationTests.java | 17 ++++++++++ 2 files changed, 51 insertions(+) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.java index 16eaab791d98..dfb7e73a5f61 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfiguration.java @@ -16,8 +16,11 @@ package org.springframework.boot.actuate.autoconfigure.metrics; +import java.util.List; + import io.micrometer.core.annotation.Timed; import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.binder.MeterBinder; import io.micrometer.core.instrument.config.MeterFilter; @@ -28,7 +31,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; +import org.springframework.context.event.ContextClosedEvent; import org.springframework.core.annotation.Order; /** @@ -36,6 +41,7 @@ * * @author Jon Schneider * @author Stephane Nicoll + * @author Moritz Halbritter * @since 2.0.0 */ @AutoConfiguration(before = CompositeMeterRegistryAutoConfiguration.class) @@ -64,4 +70,32 @@ public PropertiesMeterFilter propertiesMeterFilter(MetricsProperties properties) return new PropertiesMeterFilter(properties); } + @Bean + MeterRegistryCloser meterRegistryCloser(ObjectProvider meterRegistries) { + return new MeterRegistryCloser(meterRegistries.orderedStream().toList()); + } + + /** + * Ensures that {@link MeterRegistry meter registries} are closed early in the + * shutdown process. + */ + static class MeterRegistryCloser implements ApplicationListener { + + private final List meterRegistries; + + MeterRegistryCloser(List meterRegistries) { + this.meterRegistries = meterRegistries; + } + + @Override + public void onApplicationEvent(ContextClosedEvent event) { + for (MeterRegistry meterRegistry : this.meterRegistries) { + if (!meterRegistry.isClosed()) { + meterRegistry.close(); + } + } + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java index c04c31435a72..b7f4876bb3fe 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java @@ -25,6 +25,7 @@ import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration.MeterRegistryCloser; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -40,6 +41,7 @@ * Tests for {@link MetricsAutoConfiguration}. * * @author Andy Wilkinson + * @author Moritz Halbritter */ class MetricsAutoConfigurationTests { @@ -72,6 +74,21 @@ void configuresMeterRegistries() { }); } + @Test + void shouldSupplyMeterRegistryCloser() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(MeterRegistryCloser.class)); + } + + @Test + void meterRegistryCloserShouldCloseRegistryOnShutdown() { + this.contextRunner.withUserConfiguration(MeterRegistryConfiguration.class).run((context) -> { + MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); + assertThat(meterRegistry.isClosed()).isFalse(); + context.close(); + assertThat(meterRegistry.isClosed()).isTrue(); + }); + } + @Configuration(proxyBeanMethods = false) static class CustomClockConfiguration { From e5933c8e0e141a0c26010e31fb57dfbec07aa2b8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 16 Nov 2023 15:05:35 +0000 Subject: [PATCH 0776/1215] Upgrade to Spring Framework 6.1.0 Closes gh-38312 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index fcd518d75261..e2f0c09fd5c6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ junitJupiterVersion=5.10.0 kotlinVersion=1.9.20 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.28 -springFrameworkVersion=6.1.0-SNAPSHOT +springFrameworkVersion=6.1.0 tomcatVersion=10.1.15 kotlin.stdlib.default.dependency=false From 31e8af4709120897105433d7513e2db831a16039 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 16 Nov 2023 16:00:10 +0000 Subject: [PATCH 0777/1215] Upgrade to Spring HATEOAS 2.2.0 Closes gh-38314 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 4a0aff703dc0..e91c3a03c531 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1535,7 +1535,7 @@ bom { ] } } - library("Spring HATEOAS", "2.2.0-SNAPSHOT") { + library("Spring HATEOAS", "2.2.0") { considerSnapshots() group("org.springframework.hateoas") { modules = [ From 3b3bfd8a1b697538725cbfd328b69ab0d263ac95 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 16 Nov 2023 19:38:29 +0000 Subject: [PATCH 0778/1215] Upgrade to Spring LDAP 3.2.0 Closes gh-38317 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e91c3a03c531..1432506b6b69 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1560,7 +1560,7 @@ bom { ] } } - library("Spring LDAP", "3.2.0-SNAPSHOT") { + library("Spring LDAP", "3.2.0") { considerSnapshots() group("org.springframework.ldap") { modules = [ From 6065f219b34104e3e4f50999498b81d2b8ef02a9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 16 Nov 2023 20:09:07 +0000 Subject: [PATCH 0779/1215] Provide dependency management for org.crac:crac Closes gh-38378 --- spring-boot-project/spring-boot-dependencies/build.gradle | 7 +++++++ spring-boot-project/spring-boot-parent/build.gradle | 7 ------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 1432506b6b69..374028a560f1 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -214,6 +214,13 @@ bom { ] } } + library("Crac", "1.4.0") { + group("org.crac") { + modules = [ + "crac" + ] + } + } library("DB2 JDBC", "11.5.8.0") { group("com.ibm.db2") { modules = [ diff --git a/spring-boot-project/spring-boot-parent/build.gradle b/spring-boot-project/spring-boot-parent/build.gradle index a457cd8d17e6..09564f5bfd55 100644 --- a/spring-boot-project/spring-boot-parent/build.gradle +++ b/spring-boot-project/spring-boot-parent/build.gradle @@ -48,13 +48,6 @@ bom { ] } } - library("Crac", "1.4.0") { - group("org.crac") { - modules = [ - "crac" - ] - } - } library("Jakarta Inject", "2.0.1") { group("jakarta.inject") { modules = [ From cd0894edeee76862e5a4bdfebd7aef2011301d0d Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 17:47:48 -0800 Subject: [PATCH 0780/1215] Upgrade to Byte Buddy 1.14.10 Closes gh-38407 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 374028a560f1..cf96979e3ea4 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -123,7 +123,7 @@ bom { ] } } - library("Byte Buddy", "1.14.9") { + library("Byte Buddy", "1.14.10") { group("net.bytebuddy") { modules = [ "byte-buddy", From 5141ad568c7b45b4d739987a7cc7952d35f0dc32 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 17:47:52 -0800 Subject: [PATCH 0781/1215] Upgrade to DB2 JDBC 11.5.9.0 Closes gh-38408 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index cf96979e3ea4..4132eee09e62 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -221,7 +221,7 @@ bom { ] } } - library("DB2 JDBC", "11.5.8.0") { + library("DB2 JDBC", "11.5.9.0") { group("com.ibm.db2") { modules = [ "jcc" From 593b9f7c2bb447326ae49a78e755de7176f84402 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 17:47:56 -0800 Subject: [PATCH 0782/1215] Upgrade to Dropwizard Metrics 4.2.22 Closes gh-38409 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 4132eee09e62..a97d238ee803 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -247,7 +247,7 @@ bom { ] } } - library("Dropwizard Metrics", "4.2.21") { + library("Dropwizard Metrics", "4.2.22") { group("io.dropwizard.metrics") { imports = [ "metrics-bom" From 4f285b40bfb1ea16022d83eb4c8624fd38eeaae8 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 17:48:01 -0800 Subject: [PATCH 0783/1215] Upgrade to Hazelcast 5.3.6 Closes gh-38410 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index a97d238ee803..9f137736f618 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -369,7 +369,7 @@ bom { ] } } - library("Hazelcast", "5.3.5") { + library("Hazelcast", "5.3.6") { group("com.hazelcast") { modules = [ "hazelcast", From 67c4a989fb2a5403f3e20b165e08a729017e3d0d Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 17:48:06 -0800 Subject: [PATCH 0784/1215] Upgrade to Infinispan 14.0.21.Final Closes gh-38411 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 9f137736f618..90c2990152b0 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -465,7 +465,7 @@ bom { ] } } - library("Infinispan", "14.0.19.Final") { + library("Infinispan", "14.0.21.Final") { group("org.infinispan") { imports = [ "infinispan-bom" From c76329cb436a4ff2e11bd89726c6ddcfc3abbd90 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 17:48:10 -0800 Subject: [PATCH 0785/1215] Upgrade to JUnit Jupiter 5.10.1 Closes gh-38412 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e2f0c09fd5c6..cad6f187f2ea 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ assertjVersion=3.24.2 commonsCodecVersion=1.16.0 hamcrestVersion=2.2 jacksonVersion=2.15.3 -junitJupiterVersion=5.10.0 +junitJupiterVersion=5.10.1 kotlinVersion=1.9.20 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.28 From c84880e0e19584b68b33667e886a61466c711dc4 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 17:48:14 -0800 Subject: [PATCH 0786/1215] Upgrade to Kotlin Serialization 1.6.1 Closes gh-38413 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 90c2990152b0..2b2205cb5ceb 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -814,7 +814,7 @@ bom { ] } } - library("Kotlin Serialization", "1.6.0") { + library("Kotlin Serialization", "1.6.1") { group("org.jetbrains.kotlinx") { imports = [ "kotlinx-serialization-bom" From b2338f23c8692e65a179f9f0db09ff5506cf6999 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 17:48:19 -0800 Subject: [PATCH 0787/1215] Upgrade to Lettuce 6.3.0.RELEASE Closes gh-38414 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 2b2205cb5ceb..de631c3de9cd 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -821,7 +821,7 @@ bom { ] } } - library("Lettuce", "6.2.6.RELEASE") { + library("Lettuce", "6.3.0.RELEASE") { group("io.lettuce") { modules = [ "lettuce-core" From 4e76563b9f01dbcdfcd27962018c64712012ff76 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 17:48:23 -0800 Subject: [PATCH 0788/1215] Upgrade to Maven Javadoc Plugin 3.6.2 Closes gh-38415 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index de631c3de9cd..b416e6e2f8d3 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -953,7 +953,7 @@ bom { ] } } - library("Maven Javadoc Plugin", "3.6.0") { + library("Maven Javadoc Plugin", "3.6.2") { group("org.apache.maven.plugins") { plugins = [ "maven-javadoc-plugin" From 498b54b4f039db82452cdc03785dba37eaa46e05 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 17:48:28 -0800 Subject: [PATCH 0789/1215] Upgrade to MongoDB 4.11.1 Closes gh-38416 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index b416e6e2f8d3..2958da623432 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1024,7 +1024,7 @@ bom { ] } } - library("MongoDB", "4.11.0") { + library("MongoDB", "4.11.1") { group("org.mongodb") { modules = [ "bson", From eff1e5b5e27dcc9c05f77aa82240032c016f54d1 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 17:48:32 -0800 Subject: [PATCH 0790/1215] Upgrade to Netty 4.1.101.Final Closes gh-38417 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 2958da623432..11cb990da99d 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1077,7 +1077,7 @@ bom { ] } } - library("Netty", "4.1.100.Final") { + library("Netty", "4.1.101.Final") { group("io.netty") { imports = [ "netty-bom" From d6157d1fe33fd1a34847b9ba1129a8540fbf0170 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 17:48:36 -0800 Subject: [PATCH 0791/1215] Upgrade to Pooled JMS 3.1.5 Closes gh-38418 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 11cb990da99d..36e3ba8bd112 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1112,7 +1112,7 @@ bom { ] } } - library("Pooled JMS", "3.1.4") { + library("Pooled JMS", "3.1.5") { group("org.messaginghub") { modules = [ "pooled-jms" From e5ff5f56eaf2a439ae072216b37749e206a61d98 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 17:48:37 -0800 Subject: [PATCH 0792/1215] Upgrade to Spring AMQP 3.1.0 Closes gh-38308 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 36e3ba8bd112..6820b8f2f78b 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1491,7 +1491,7 @@ bom { ] } } - library("Spring AMQP", "3.1.0-SNAPSHOT") { + library("Spring AMQP", "3.1.0") { considerSnapshots() group("org.springframework.amqp") { imports = [ From d00ca7dd4da74923802de6f8e65c3907b5ba3326 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 17:48:37 -0800 Subject: [PATCH 0793/1215] Upgrade to Spring Data Bom 2023.1.0 Closes gh-38311 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 6820b8f2f78b..fcd2bebd9189 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1515,7 +1515,7 @@ bom { ] } } - library("Spring Data Bom", "2023.1.0-SNAPSHOT") { + library("Spring Data Bom", "2023.1.0") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From 5a89ddb1fd5091e0eef287f874db87af9a6610b1 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 17:48:37 -0800 Subject: [PATCH 0794/1215] Upgrade to Spring Kafka 3.1.0 Closes gh-38316 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index fcd2bebd9189..cae406c92063 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1558,7 +1558,7 @@ bom { ] } } - library("Spring Kafka", "3.1.0-SNAPSHOT") { + library("Spring Kafka", "3.1.0") { considerSnapshots() group("org.springframework.kafka") { modules = [ From cd56affdf3af09d894a03d8124977b6dc4b6a514 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 17:48:41 -0800 Subject: [PATCH 0795/1215] Upgrade to Spring Pulsar 1.0.0 Closes gh-38419 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index cae406c92063..009b38992fe0 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1578,7 +1578,7 @@ bom { ] } } - library("Spring Pulsar", "1.0.0-RC1") { + library("Spring Pulsar", "1.0.0") { group("org.springframework.pulsar") { modules = [ "spring-pulsar", From 5bb05e6c7a65e6947bfb8b8bc4c76e46e8b8efc0 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 17:48:46 -0800 Subject: [PATCH 0796/1215] Upgrade to Spring RESTDocs 3.0.1 Closes gh-38420 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 009b38992fe0..28c517ef3405 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1588,7 +1588,7 @@ bom { ] } } - library("Spring RESTDocs", "3.0.0") { + library("Spring RESTDocs", "3.0.1") { considerSnapshots() group("org.springframework.restdocs") { imports = [ From c3949dd1c9f8caf81d501208967901ecc9020af5 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 17:48:46 -0800 Subject: [PATCH 0797/1215] Upgrade to Spring Security 6.2.0 Closes gh-38318 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 28c517ef3405..5b8280df0d64 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1604,7 +1604,7 @@ bom { ] } } - library("Spring Security", "6.2.0-SNAPSHOT") { + library("Spring Security", "6.2.0") { considerSnapshots() group("org.springframework.security") { imports = [ From b92dac5207e61d8ac9a2809237841f08f1f603d2 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 17:48:46 -0800 Subject: [PATCH 0798/1215] Upgrade to Spring WS 4.0.8 Closes gh-38320 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5b8280df0d64..4beb9e1e2532 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1624,7 +1624,7 @@ bom { ] } } - library("Spring WS", "4.0.8-SNAPSHOT") { + library("Spring WS", "4.0.8") { considerSnapshots() group("org.springframework.ws") { imports = [ From bd53a3f2603d12bab4eab9a92c7adc01a7b408c6 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 17:48:50 -0800 Subject: [PATCH 0799/1215] Upgrade to Tomcat 10.1.16 Closes gh-38421 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index cad6f187f2ea..4dfd5e6d8815 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,6 +13,6 @@ kotlinVersion=1.9.20 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.28 springFrameworkVersion=6.1.0 -tomcatVersion=10.1.15 +tomcatVersion=10.1.16 kotlin.stdlib.default.dependency=false From 261ea6ce44a14544a90b37daaa50b96997726488 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 17:48:55 -0800 Subject: [PATCH 0800/1215] Upgrade to Versions Maven Plugin 2.16.2 Closes gh-38422 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 4beb9e1e2532..28b192cf303a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1708,7 +1708,7 @@ bom { ] } } - library("Versions Maven Plugin", "2.16.1") { + library("Versions Maven Plugin", "2.16.2") { group("org.codehaus.mojo") { plugins = [ "versions-maven-plugin" From b296ff890e3dfe76062071d74dbde694e5268f96 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 18:48:24 -0800 Subject: [PATCH 0801/1215] Upgrade to Pulsar Reactive 0.5.0 Closes gh-38406 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 28b192cf303a..8612197b52ad 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1212,7 +1212,7 @@ bom { ] } } - library("Pulsar Reactive", "0.4.0") { + library("Pulsar Reactive", "0.5.0") { group("org.apache.pulsar") { modules = [ "pulsar-client-reactive-adapter", From 6f3b3fa6f64907365aa90e5fc1ef90fb80880c9a Mon Sep 17 00:00:00 2001 From: abdullah-jaffer Date: Mon, 20 Nov 2023 20:05:03 -0800 Subject: [PATCH 0802/1215] Replace Function with UnaryOperator See gh-38390 --- .../boot/logging/CorrelationIdFormatter.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/CorrelationIdFormatter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/CorrelationIdFormatter.java index 1388aecca0b6..e701fba05cff 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/CorrelationIdFormatter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/CorrelationIdFormatter.java @@ -22,8 +22,8 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; -import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.UnaryOperator; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -84,7 +84,7 @@ private CorrelationIdFormatter(List parts) { * @param resolver the resolver used to resolve named values * @return a formatted correlation id */ - public String format(Function resolver) { + public String format(UnaryOperator resolver) { StringBuilder result = new StringBuilder(); formatTo(resolver, result); return result.toString(); @@ -96,7 +96,7 @@ public String format(Function resolver) { * @param resolver the resolver used to resolve named values * @param appendable the appendable for the formatted correlation id */ - public void formatTo(Function resolver, Appendable appendable) { + public void formatTo(UnaryOperator resolver, Appendable appendable) { Predicate canResolve = (part) -> StringUtils.hasLength(resolver.apply(part.name())); try { if (this.parts.stream().anyMatch(canResolve)) { @@ -169,7 +169,7 @@ record Part(String name, int length) { private static final Pattern pattern = Pattern.compile("^(.+?)\\((\\d+)\\)$"); - String resolve(Function resolver) { + String resolve(UnaryOperator resolver) { String resolved = resolver.apply(name()); if (resolved == null) { return blank(); From 59493e8306b34e3b8647fb684c9618b8f041360e Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 20 Nov 2023 21:03:17 -0800 Subject: [PATCH 0803/1215] Fix failing tests following version upgrades --- .../wavefront/WavefrontPropertiesConfigAdapterTests.java | 2 +- .../org/springframework/boot/json/GsonJsonParserTests.java | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapterTests.java index 9c4439f538ef..2acd34bbe5bc 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapterTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/export/wavefront/WavefrontPropertiesConfigAdapterTests.java @@ -66,7 +66,7 @@ void whenPropertiesGlobalPrefixIsSetAdapterGlobalPrefixReturnsIt() { protected void whenPropertiesBatchSizeIsSetAdapterBatchSizeReturnsIt() { WavefrontProperties properties = new WavefrontProperties(); properties.getSender().setBatchSize(10042); - assertThat(createConfigAdapter(properties.getMetrics().getExport()).batchSize()).isEqualTo(10042); + assertThat(new WavefrontPropertiesConfigAdapter(properties).batchSize()).isEqualTo(10042); } @Test diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/json/GsonJsonParserTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/json/GsonJsonParserTests.java index 757a2d3b0da6..3a7b7f04aa84 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/json/GsonJsonParserTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/json/GsonJsonParserTests.java @@ -18,8 +18,6 @@ import java.io.IOException; -import org.junit.jupiter.api.Disabled; - /** * Tests for {@link GsonJsonParser}. * @@ -33,9 +31,8 @@ protected JsonParser getParser() { } @Override - @Disabled("Gson does not protect against deeply nested JSON") void listWithRepeatedOpenArray() throws IOException { - super.listWithRepeatedOpenArray(); + // Gson does not protect against deeply nested JSON } } From fd5722106f5892cf55b8fa49600f95b5b931d5ea Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 21 Nov 2023 15:30:37 +0000 Subject: [PATCH 0804/1215] Start building against Spring Framework 6.1.1 snapshots See gh-38451 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 4dfd5e6d8815..ff97de233636 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ junitJupiterVersion=5.10.1 kotlinVersion=1.9.20 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.28 -springFrameworkVersion=6.1.0 +springFrameworkVersion=6.1.1-SNAPSHOT tomcatVersion=10.1.16 kotlin.stdlib.default.dependency=false From 4659d14170ff844de54628c42e8f450f512da74c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 21 Nov 2023 16:51:42 +0000 Subject: [PATCH 0805/1215] Upgrade to Spring Authorization Server 1.2.0 Closes gh-38309 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 8612197b52ad..e7971f43e4c8 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1499,7 +1499,7 @@ bom { ] } } - library("Spring Authorization Server", "1.2.0-SNAPSHOT") { + library("Spring Authorization Server", "1.2.0") { considerSnapshots() group("org.springframework.security") { modules = [ From d4a1d10fa1dab9d881f356dfab5ff8e3d46dc761 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 21 Nov 2023 16:51:43 +0000 Subject: [PATCH 0806/1215] Upgrade to Spring GraphQL 1.2.4 Closes gh-38313 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e7971f43e4c8..e5f306c25850 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1533,7 +1533,7 @@ bom { ] } } - library("Spring GraphQL", "1.2.4-SNAPSHOT") { + library("Spring GraphQL", "1.2.4") { considerSnapshots() group("org.springframework.graphql") { modules = [ From 84f0614bdfab7b6b1085712b34851f2ffa5a9205 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 21 Nov 2023 16:51:44 +0000 Subject: [PATCH 0807/1215] Upgrade to Spring Session 3.2.0 Closes gh-38319 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e5f306c25850..94c1f5be6fb1 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1612,7 +1612,7 @@ bom { ] } } - library("Spring Session", "3.2.0-SNAPSHOT") { + library("Spring Session", "3.2.0") { considerSnapshots() prohibit { startsWith(["Apple-", "Bean-", "Corn-", "Dragonfruit-"]) From 0897d752bc2fb489d1a3e86cbffb4d5d2cbe320d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 21 Nov 2023 16:51:50 +0000 Subject: [PATCH 0808/1215] Upgrade to Testcontainers 1.19.3 Closes gh-38471 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 94c1f5be6fb1..cd42ef8ae2a6 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1639,7 +1639,7 @@ bom { ] } } - library("Testcontainers", "1.19.2") { + library("Testcontainers", "1.19.3") { group("org.testcontainers") { imports = [ "testcontainers-bom" From 6c3dec42e0f9ed4e780395def94775e94d82c40c Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 21 Nov 2023 11:46:40 -0800 Subject: [PATCH 0809/1215] Add container support for Oracle Free which replaces Oracle XE Update Docker Compose and Testcontainers support to work with `gvenzl/oracle-free` which replaces `gvenzl/oracle-xe`. Closes gh-38476 --- .../connection/ConnectionNamePredicate.java | 15 ++-- ...DockerComposeConnectionDetailsFactory.java | 10 +++ .../connection/oracle/OracleEnvironment.java | 2 + ...DockerComposeConnectionDetailsFactory.java | 2 +- ...DockerComposeConnectionDetailsFactory.java | 2 +- .../ConnectionNamePredicateTests.java | 9 +- ...nectionDetailsFactoryIntegrationTests.java | 71 +++++++++++++++ ...nectionDetailsFactoryIntegrationTests.java | 68 +++++++++++++++ ...ectionDetailsFactoryIntegrationTests.java} | 4 +- ...ectionDetailsFactoryIntegrationTests.java} | 4 +- .../asciidoc/features/docker-compose.adoc | 4 +- .../spring-boot-testcontainers/build.gradle | 1 + ...2dbcContainerConnectionDetailsFactory.java | 63 ++++++++++++++ ...dbcContainerConnectionDetailsFactory.java} | 4 +- .../main/resources/META-INF/spring.factories | 3 +- ...ontainerConnectionDetailsFactoryTests.java | 86 +++++++++++++++++++ ...ntainerConnectionDetailsFactoryTests.java} | 4 +- .../testcontainers/DockerImageNames.java | 10 +++ 18 files changed, 343 insertions(+), 19 deletions(-) create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java rename spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/{OracleJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java => OracleXeJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java} (93%) rename spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/{OracleR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java => OracleXeR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java} (93%) create mode 100644 spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/r2dbc/OracleFreeR2dbcContainerConnectionDetailsFactory.java rename spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/r2dbc/{OracleR2dbcContainerConnectionDetailsFactory.java => OracleXeR2dbcContainerConnectionDetailsFactory.java} (95%) create mode 100644 spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/r2dbc/OracleFreeR2dbcContainerConnectionDetailsFactoryTests.java rename spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/r2dbc/{OracleR2dbcContainerConnectionDetailsFactoryTests.java => OracleXeR2dbcContainerConnectionDetailsFactoryTests.java} (96%) diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ConnectionNamePredicate.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ConnectionNamePredicate.java index 626c3262bdaf..1990f9b27134 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ConnectionNamePredicate.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ConnectionNamePredicate.java @@ -16,28 +16,33 @@ package org.springframework.boot.docker.compose.service.connection; +import java.util.Arrays; +import java.util.Set; import java.util.function.Predicate; +import java.util.stream.Collectors; import org.springframework.boot.docker.compose.core.ImageReference; import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.util.Assert; /** - * {@link Predicate} that matches against connection names. + * {@link Predicate} that matches against connection name. * * @author Phillip Webb */ class ConnectionNamePredicate implements Predicate { - private final String required; + private final Set required; - ConnectionNamePredicate(String required) { - this.required = asCanonicalName(required); + ConnectionNamePredicate(String... required) { + Assert.notEmpty(required, "Required must not be empty"); + this.required = Arrays.stream(required).map(this::asCanonicalName).collect(Collectors.toSet()); } @Override public boolean test(DockerComposeConnectionSource source) { String actual = getActual(source.getRunningService()); - return this.required.equals(actual); + return this.required.contains(actual); } private String getActual(RunningService service) { diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionDetailsFactory.java index 6d29c8bb0247..302a3ba35817 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionDetailsFactory.java @@ -54,6 +54,16 @@ protected DockerComposeConnectionDetailsFactory(String connectionName, String... this(new ConnectionNamePredicate(connectionName), requiredClassNames); } + /** + * Create a new {@link DockerComposeConnectionDetailsFactory} instance. + * @param connectionNames the required connection name + * @param requiredClassNames the names of classes that must be present + * @since 3.2.0 + */ + protected DockerComposeConnectionDetailsFactory(String[] connectionNames, String... requiredClassNames) { + this(new ConnectionNamePredicate(connectionNames), requiredClassNames); + } + /** * Create a new {@link DockerComposeConnectionDetailsFactory} instance. * @param predicate a predicate used to check when a service is accepted diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleEnvironment.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleEnvironment.java index 78011a7d6cfc..0540dd1983c5 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleEnvironment.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleEnvironment.java @@ -28,6 +28,8 @@ */ class OracleEnvironment { + static final String[] CONTAINER_NAMES = { "gvenzl/oracle-xe", "gvenzl/oracle-free" }; + private final String username; private final String password; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleJdbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleJdbcDockerComposeConnectionDetailsFactory.java index 9104a9ff267a..3c7ca450e873 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleJdbcDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleJdbcDockerComposeConnectionDetailsFactory.java @@ -34,7 +34,7 @@ class OracleJdbcDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { protected OracleJdbcDockerComposeConnectionDetailsFactory() { - super("gvenzl/oracle-xe"); + super(OracleEnvironment.CONTAINER_NAMES); } @Override diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleR2dbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleR2dbcDockerComposeConnectionDetailsFactory.java index 5ebf98956e9b..01788dca4989 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleR2dbcDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleR2dbcDockerComposeConnectionDetailsFactory.java @@ -36,7 +36,7 @@ class OracleR2dbcDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { OracleR2dbcDockerComposeConnectionDetailsFactory() { - super("gvenzl/oracle-xe", "io.r2dbc.spi.ConnectionFactoryOptions"); + super(OracleEnvironment.CONTAINER_NAMES, "io.r2dbc.spi.ConnectionFactoryOptions"); } @Override diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ConnectionNamePredicateTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ConnectionNamePredicateTests.java index 183ca38ec177..76b1128232db 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ConnectionNamePredicateTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ConnectionNamePredicateTests.java @@ -74,7 +74,14 @@ void labeled() { .accepts(sourceOf("internalhost:8080/libs/libs/mzipkin", "openzipkin/zipkin")); } - private Predicate predicateOf(String required) { + @Test + void multiple() { + assertThat(predicateOf("elasticsearch1", "elasticsearch2")).accepts(sourceOf("elasticsearch1")) + .accepts(sourceOf("elasticsearch2")); + + } + + private Predicate predicateOf(String... required) { return new ConnectionNamePredicate(required); } diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..9857bcd11c66 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.oracle; + +import java.sql.Driver; +import java.time.Duration; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.OS; + +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.jdbc.DatabaseDriver; +import org.springframework.boot.testsupport.junit.DisabledOnOs; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.SimpleDriverDataSource; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link OracleJdbcDockerComposeConnectionDetailsFactory} + * + * @author Andy Wilkinson + */ +@DisabledOnOs(os = { OS.LINUX, OS.MAC }, architecture = "aarch64", + disabledReason = "The Oracle image has no ARM support") +class OracleFreeJdbcDockerComposeConnectionDetailsFactoryIntegrationTests + extends AbstractDockerComposeIntegrationTests { + + OracleFreeJdbcDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("oracle-compose.yaml", DockerImageNames.oracleFree()); + } + + @Test + @SuppressWarnings("unchecked") + void runCreatesConnectionDetailsThatCanBeUsedToAccessDatabase() throws Exception { + JdbcConnectionDetails connectionDetails = run(JdbcConnectionDetails.class); + assertThat(connectionDetails.getUsername()).isEqualTo("app_user"); + assertThat(connectionDetails.getPassword()).isEqualTo("app_user_secret"); + assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:oracle:thin:@").endsWith("/xepdb1"); + SimpleDriverDataSource dataSource = new SimpleDriverDataSource(); + dataSource.setUrl(connectionDetails.getJdbcUrl()); + dataSource.setUsername(connectionDetails.getUsername()); + dataSource.setPassword(connectionDetails.getPassword()); + dataSource.setDriverClass((Class) ClassUtils.forName(connectionDetails.getDriverClassName(), + getClass().getClassLoader())); + Awaitility.await().atMost(Duration.ofMinutes(1)).ignoreExceptions().untilAsserted(() -> { + JdbcTemplate template = new JdbcTemplate(dataSource); + assertThat(template.queryForObject(DatabaseDriver.ORACLE.getValidationQuery(), String.class)) + .isEqualTo("Hello"); + }); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..49dd298f4e84 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.oracle; + +import java.time.Duration; + +import io.r2dbc.spi.ConnectionFactories; +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.OS; + +import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.jdbc.DatabaseDriver; +import org.springframework.boot.testsupport.junit.DisabledOnOs; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.r2dbc.core.DatabaseClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link OracleR2dbcDockerComposeConnectionDetailsFactory} + * + * @author Andy Wilkinson + */ +@DisabledOnOs(os = { OS.LINUX, OS.MAC }, architecture = "aarch64", + disabledReason = "The Oracle image has no ARM support") +class OracleFreeR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests + extends AbstractDockerComposeIntegrationTests { + + OracleFreeR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("oracle-compose.yaml", DockerImageNames.oracleFree()); + } + + @Test + void runCreatesConnectionDetailsThatCanBeUsedToAccessDatabase() { + R2dbcConnectionDetails connectionDetails = run(R2dbcConnectionDetails.class); + ConnectionFactoryOptions connectionFactoryOptions = connectionDetails.getConnectionFactoryOptions(); + assertThat(connectionFactoryOptions.toString()).contains("database=xepdb1", "driver=oracle", + "password=REDACTED", "user=app_user"); + assertThat(connectionFactoryOptions.getRequiredValue(ConnectionFactoryOptions.PASSWORD)) + .isEqualTo("app_user_secret"); + Awaitility.await().atMost(Duration.ofMinutes(1)).ignoreExceptions().untilAsserted(() -> { + Object result = DatabaseClient.create(ConnectionFactories.get(connectionFactoryOptions)) + .sql(DatabaseDriver.ORACLE.getValidationQuery()) + .map((row, metadata) -> row.get(0)) + .first() + .block(Duration.ofSeconds(30)); + assertThat(result).isEqualTo("Hello"); + }); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleXeJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 93% rename from spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleXeJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java index 0d3c71cdc5a2..e191174c986d 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleXeJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -41,9 +41,9 @@ */ @DisabledOnOs(os = { OS.LINUX, OS.MAC }, architecture = "aarch64", disabledReason = "The Oracle image has no ARM support") -class OracleJdbcDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { +class OracleXeJdbcDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { - OracleJdbcDockerComposeConnectionDetailsFactoryIntegrationTests() { + OracleXeJdbcDockerComposeConnectionDetailsFactoryIntegrationTests() { super("oracle-compose.yaml", DockerImageNames.oracleXe()); } diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleXeR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 93% rename from spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleXeR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java index 303f027f0c8b..76766e8d12b9 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleXeR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -40,9 +40,9 @@ */ @DisabledOnOs(os = { OS.LINUX, OS.MAC }, architecture = "aarch64", disabledReason = "The Oracle image has no ARM support") -class OracleR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { +class OracleXeR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { - OracleR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests() { + OracleXeR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests() { super("oracle-compose.yaml", DockerImageNames.oracleXe()); } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc index 3cda5797538f..eaef6872f361 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc @@ -74,7 +74,7 @@ The following service connections are currently supported: | Containers named "elasticsearch" | `JdbcConnectionDetails` -| Containers named "gvenzl/oracle-xe", "mariadb", "mssql/server", "mysql", or "postgres" +| Containers named "gvenzl/oracle-free", "gvenzl/oracle-xe", "mariadb", "mssql/server", "mysql", or "postgres" | `MongoConnectionDetails` | Containers named "mongo" @@ -92,7 +92,7 @@ The following service connections are currently supported: | Containers named "apachepulsar/pulsar" | `R2dbcConnectionDetails` -| Containers named "gvenzl/oracle-xe", "mariadb", "mssql/server", "mysql", or "postgres" +| Containers named "gvenzl/oracle-free", "gvenzl/oracle-xe", "mariadb", "mssql/server", "mysql", or "postgres" | `RabbitConnectionDetails` | Containers named "rabbitmq" diff --git a/spring-boot-project/spring-boot-testcontainers/build.gradle b/spring-boot-project/spring-boot-testcontainers/build.gradle index de0546e45075..7d7792b8ade8 100644 --- a/spring-boot-project/spring-boot-testcontainers/build.gradle +++ b/spring-boot-project/spring-boot-testcontainers/build.gradle @@ -29,6 +29,7 @@ dependencies { optional("org.testcontainers:mysql") optional("org.testcontainers:neo4j") optional("org.testcontainers:oracle-xe") + optional("org.testcontainers:oracle-free") optional("org.testcontainers:postgresql") optional("org.testcontainers:pulsar") optional("org.testcontainers:rabbitmq") diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/r2dbc/OracleFreeR2dbcContainerConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/r2dbc/OracleFreeR2dbcContainerConnectionDetailsFactory.java new file mode 100644 index 000000000000..09e381faa0fd --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/r2dbc/OracleFreeR2dbcContainerConnectionDetailsFactory.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2023 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.testcontainers.service.connection.r2dbc; + +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.testcontainers.oracle.OracleContainer; +import org.testcontainers.oracle.OracleR2DBCDatabaseContainer; + +import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; + +/** + * {@link ContainerConnectionDetailsFactory} to create {@link R2dbcConnectionDetails} from + * a {@link ServiceConnection @ServiceConnection}-annotated {@link OracleContainer}. + * + * @author Eddú Meléndez + */ +class OracleFreeR2dbcContainerConnectionDetailsFactory + extends ContainerConnectionDetailsFactory { + + OracleFreeR2dbcContainerConnectionDetailsFactory() { + super(ANY_CONNECTION_NAME, "io.r2dbc.spi.ConnectionFactoryOptions"); + } + + @Override + public R2dbcConnectionDetails getContainerConnectionDetails(ContainerConnectionSource source) { + return new R2dbcDatabaseContainerConnectionDetails(source); + } + + /** + * {@link R2dbcConnectionDetails} backed by a {@link ContainerConnectionSource}. + */ + private static final class R2dbcDatabaseContainerConnectionDetails + extends ContainerConnectionDetails implements R2dbcConnectionDetails { + + private R2dbcDatabaseContainerConnectionDetails(ContainerConnectionSource source) { + super(source); + } + + @Override + public ConnectionFactoryOptions getConnectionFactoryOptions() { + return OracleR2DBCDatabaseContainer.getOptions(getContainer()); + } + + } + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/r2dbc/OracleR2dbcContainerConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/r2dbc/OracleXeR2dbcContainerConnectionDetailsFactory.java similarity index 95% rename from spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/r2dbc/OracleR2dbcContainerConnectionDetailsFactory.java rename to spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/r2dbc/OracleXeR2dbcContainerConnectionDetailsFactory.java index 783e83a77115..a5f21c796a46 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/r2dbc/OracleR2dbcContainerConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/r2dbc/OracleXeR2dbcContainerConnectionDetailsFactory.java @@ -31,10 +31,10 @@ * * @author Eddú Meléndez */ -class OracleR2dbcContainerConnectionDetailsFactory +class OracleXeR2dbcContainerConnectionDetailsFactory extends ContainerConnectionDetailsFactory { - OracleR2dbcContainerConnectionDetailsFactory() { + OracleXeR2dbcContainerConnectionDetailsFactory() { super(ANY_CONNECTION_NAME, "io.r2dbc.spi.ConnectionFactoryOptions"); } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories index 53afedfc51e1..386f2a1a3553 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories @@ -24,7 +24,8 @@ org.springframework.boot.testcontainers.service.connection.otlp.OpenTelemetryTra org.springframework.boot.testcontainers.service.connection.pulsar.PulsarContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.r2dbc.MariaDbR2dbcContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.r2dbc.MySqlR2dbcContainerConnectionDetailsFactory,\ -org.springframework.boot.testcontainers.service.connection.r2dbc.OracleR2dbcContainerConnectionDetailsFactory,\ +org.springframework.boot.testcontainers.service.connection.r2dbc.OracleFreeR2dbcContainerConnectionDetailsFactory,\ +org.springframework.boot.testcontainers.service.connection.r2dbc.OracleXeR2dbcContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.r2dbc.PostgresR2dbcContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.r2dbc.SqlServerR2dbcContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.redis.RedisContainerConnectionDetailsFactory,\ diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/r2dbc/OracleFreeR2dbcContainerConnectionDetailsFactoryTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/r2dbc/OracleFreeR2dbcContainerConnectionDetailsFactoryTests.java new file mode 100644 index 000000000000..500910648a54 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/r2dbc/OracleFreeR2dbcContainerConnectionDetailsFactoryTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2023 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.testcontainers.service.connection.r2dbc; + +import java.time.Duration; + +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.OS; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.oracle.OracleContainer; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; +import org.springframework.boot.jdbc.DatabaseDriver; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactoryHints; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.junit.DisabledOnOs; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.context.annotation.Configuration; +import org.springframework.r2dbc.core.DatabaseClient; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link OracleFreeR2dbcContainerConnectionDetailsFactory}. + * + * @author Andy Wilkinson + */ +@SpringJUnitConfig +@Testcontainers(disabledWithoutDocker = true) +@DisabledOnOs(os = { OS.LINUX, OS.MAC }, architecture = "aarch64", + disabledReason = "The Oracle image has no ARM support") +class OracleFreeR2dbcContainerConnectionDetailsFactoryTests { + + @Container + @ServiceConnection + static final OracleContainer oracle = new OracleContainer(DockerImageNames.oracleFree()) + .withStartupTimeout(Duration.ofMinutes(2)); + + @Autowired + ConnectionFactory connectionFactory; + + @Test + void connectionCanBeMadeToOracleContainer() { + Object result = DatabaseClient.create(this.connectionFactory) + .sql(DatabaseDriver.ORACLE.getValidationQuery()) + .map((row, metadata) -> row.get(0)) + .first() + .block(Duration.ofSeconds(30)); + assertThat(result).isEqualTo("Hello"); + } + + @Test + void shouldRegisterHints() { + RuntimeHints hints = ContainerConnectionDetailsFactoryHints.getRegisteredHints(getClass().getClassLoader()); + assertThat(RuntimeHintsPredicates.reflection().onType(ConnectionFactoryOptions.class)).accepts(hints); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(R2dbcAutoConfiguration.class) + static class TestConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/r2dbc/OracleR2dbcContainerConnectionDetailsFactoryTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/r2dbc/OracleXeR2dbcContainerConnectionDetailsFactoryTests.java similarity index 96% rename from spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/r2dbc/OracleR2dbcContainerConnectionDetailsFactoryTests.java rename to spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/r2dbc/OracleXeR2dbcContainerConnectionDetailsFactoryTests.java index 5aff194a1bfd..aa40d6204e70 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/r2dbc/OracleR2dbcContainerConnectionDetailsFactoryTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/r2dbc/OracleXeR2dbcContainerConnectionDetailsFactoryTests.java @@ -43,7 +43,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link OracleR2dbcContainerConnectionDetailsFactory}. + * Tests for {@link OracleXeR2dbcContainerConnectionDetailsFactory}. * * @author Andy Wilkinson */ @@ -51,7 +51,7 @@ @Testcontainers(disabledWithoutDocker = true) @DisabledOnOs(os = { OS.LINUX, OS.MAC }, architecture = "aarch64", disabledReason = "The Oracle image has no ARM support") -class OracleR2dbcContainerConnectionDetailsFactoryTests { +class OracleXeR2dbcContainerConnectionDetailsFactoryTests { @Container @ServiceConnection diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java index 3089127fc417..4bb98d22018d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java @@ -48,6 +48,8 @@ public final class DockerImageNames { private static final String NEO4J_VERSION = "4.4.11"; + private static final String ORACLE_FREE_VERSION = "23.3-slim"; + private static final String ORACLE_XE_VERSION = "18.4.0-slim"; private static final String OPENTELEMETRY_VERSION = "0.75.0"; @@ -149,6 +151,14 @@ public static DockerImageName neo4j() { return DockerImageName.parse("neo4j").withTag(NEO4J_VERSION); } + /** + * Return a {@link DockerImageName} suitable for running the Oracle database. + * @return a docker image name for running the Oracle database + */ + public static DockerImageName oracleFree() { + return DockerImageName.parse("gvenzl/oracle-free").withTag(ORACLE_FREE_VERSION); + } + /** * Return a {@link DockerImageName} suitable for running the Oracle database. * @return a docker image name for running the Oracle database From ece763c44ff875589dfaf9bcf30d166c426d3a3a Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 21 Nov 2023 12:19:44 -0800 Subject: [PATCH 0810/1215] Upgrade to GraphQL Java 21.3 Closes gh-38478 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index cd42ef8ae2a6..e075d1a664f0 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -328,7 +328,7 @@ bom { ] } } - library("GraphQL Java", "21.2") { + library("GraphQL Java", "21.3") { prohibit { startsWith(["2018-", "2019-", "2020-", "2021-", "230521-"]) because "These are snapshots that we don't want to see" From 30208588a01c21e44e1286f8b5b2ff532e82824e Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 21 Nov 2023 12:19:48 -0800 Subject: [PATCH 0811/1215] Upgrade to Mockito 5.7.0 Closes gh-38479 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e075d1a664f0..d319c9fdcd80 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1017,7 +1017,7 @@ bom { ] } } - library("Mockito", "5.6.0") { + library("Mockito", "5.7.0") { group("org.mockito") { imports = [ "mockito-bom" From 31955603852c180a77814bcf195bb30a1010eaeb Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 21 Nov 2023 12:19:53 -0800 Subject: [PATCH 0812/1215] Upgrade to Rabbit Stream Client 0.14.0 Closes gh-38480 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d319c9fdcd80..bc77c2cfae74 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1309,7 +1309,7 @@ bom { ] } } - library("Rabbit Stream Client", "0.13.0") { + library("Rabbit Stream Client", "0.14.0") { group("com.rabbitmq") { modules = [ "stream-client" From f37d6c9294788720fbea27c925f55d5ce52b4fcb Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 21 Nov 2023 12:19:57 -0800 Subject: [PATCH 0813/1215] Upgrade to WebJars Locator Core 0.55 Closes gh-38481 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index bc77c2cfae74..01adb3570c32 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1715,7 +1715,7 @@ bom { ] } } - library("WebJars Locator Core", "0.53") { + library("WebJars Locator Core", "0.55") { group("org.webjars") { modules = [ "webjars-locator-core" From 969e142c347c2313f5e56ab8cb8e503b3af623e5 Mon Sep 17 00:00:00 2001 From: Georg Pirklbauer Date: Wed, 15 Nov 2023 18:21:55 +0100 Subject: [PATCH 0814/1215] Update Dynatrace docs with info about the meter metadata toggle See gh-38368 --- .../spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc index bb42b97df771..af495e2ac885 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc @@ -230,6 +230,8 @@ If tags with the same key are specified with Micrometer, they overwrite the defa In Micrometer 1.9.x, this was fixed by introducing Dynatrace-specific summary instruments. Setting this toggle to `false` forces Micrometer to fall back to the behavior that was the default before 1.9.x. It should only be used when encountering problems while migrating from Micrometer 1.8.x to 1.9.x. +* Export meter metadata: Starting from Micrometer 1.12.0, the Dynatrace exporter will also export meter metadata, such as unit and description by default. +Use the `export-meter-metadata` toggle to turn this feature off. It is possible to not specify a URI and API token, as shown in the following example. In this scenario, the automatically configured endpoint is used: @@ -248,6 +250,7 @@ In this scenario, the automatically configured endpoint is used: key1: "value1" key2: "value2" use-dynatrace-summary-instruments: true # (default: true) + export-meter-metadata: true # (default: true) ---- From 175b6473c703b88f5e62f3f7358593a66f774004 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Sun, 19 Nov 2023 00:19:30 +0900 Subject: [PATCH 0815/1215] Polish See gh-38389 --- .../WavefrontSenderConfigurationTests.java | 13 ++++--------- .../boot/actuate/mail/MailHealthIndicatorTests.java | 6 +++--- ...ork.boot.autoconfigure.AutoConfiguration.imports | 2 +- .../jackson/JacksonAutoConfigurationTests.java | 2 +- .../client/RestTemplateAutoConfigurationTests.java | 4 ++-- .../src/docs/asciidoc/howto/docker-compose.adoc | 2 +- .../src/docs/asciidoc/howto/spring-mvc.adoc | 2 +- .../src/docs/asciidoc/io/rest-client.adoc | 2 +- .../src/docs/asciidoc/web/reactive.adoc | 4 ++-- .../src/docs/asciidoc/web/servlet.adoc | 2 +- spring-boot-project/spring-boot-parent/build.gradle | 4 ++-- .../SpringBootMockMvcBuilderCustomizerTests.java | 5 ----- .../springframework/boot/loader/zip/ZipContent.java | 4 ++-- .../org/springframework/boot/SpringApplication.java | 3 ++- 14 files changed, 23 insertions(+), 32 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontSenderConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontSenderConfigurationTests.java index 6091baf221a7..512fd8311741 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontSenderConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/wavefront/WavefrontSenderConfigurationTests.java @@ -30,7 +30,6 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.as; import static org.assertj.core.api.Assertions.assertThat; @@ -129,8 +128,7 @@ void shouldApplyTokenTypeWavefrontApiToken() { "management.wavefront.api-token=abcde") .run((context) -> { WavefrontSender sender = context.getBean(WavefrontSender.class); - Object tokenService = ReflectionTestUtils.getField(sender, "tokenService"); - assertThat(tokenService).isInstanceOf(WavefrontTokenService.class); + assertThat(sender).extracting("tokenService").isInstanceOf(WavefrontTokenService.class); }); } @@ -141,8 +139,7 @@ void shouldApplyTokenTypeCspApiToken() { "management.wavefront.api-token=abcde") .run((context) -> { WavefrontSender sender = context.getBean(WavefrontSender.class); - Object tokenService = ReflectionTestUtils.getField(sender, "tokenService"); - assertThat(tokenService).isInstanceOf(CSPTokenService.class); + assertThat(sender).extracting("tokenService").isInstanceOf(CSPTokenService.class); }); } @@ -153,8 +150,7 @@ void shouldApplyTokenTypeCspClientCredentials() { "management.wavefront.api-token=clientid=cid,clientsecret=csec") .run((context) -> { WavefrontSender sender = context.getBean(WavefrontSender.class); - Object tokenService = ReflectionTestUtils.getField(sender, "tokenService"); - assertThat(tokenService).isInstanceOf(CSPTokenService.class); + assertThat(sender).extracting("tokenService").isInstanceOf(CSPTokenService.class); }); } @@ -162,8 +158,7 @@ void shouldApplyTokenTypeCspClientCredentials() { void shouldApplyTokenTypeNoToken() { this.contextRunner.withPropertyValues("management.wavefront.api-token-type=NO_TOKEN").run((context) -> { WavefrontSender sender = context.getBean(WavefrontSender.class); - Object tokenService = ReflectionTestUtils.getField(sender, "tokenService"); - assertThat(tokenService).isInstanceOf(NoopProxyTokenService.class); + assertThat(sender).extracting("tokenService").isInstanceOf(NoopProxyTokenService.class); }); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/mail/MailHealthIndicatorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/mail/MailHealthIndicatorTests.java index 88fda8cead08..9abfbedd88da 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/mail/MailHealthIndicatorTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/mail/MailHealthIndicatorTests.java @@ -81,7 +81,7 @@ void smtpOnDefaultHostAndPortIsDown() throws MessagingException { assertThat(health.getDetails()).doesNotContainKey("location"); Object errorMessage = health.getDetails().get("error"); assertThat(errorMessage).isNotNull(); - assertThat(errorMessage.toString().contains("A test exception")).isTrue(); + assertThat(errorMessage.toString()).contains("A test exception"); } @Test @@ -104,7 +104,7 @@ void smtpOnDefaultHostAndCustomPortIsDown() throws MessagingException { assertThat(health.getDetails().get("location")).isEqualTo(":1234"); Object errorMessage = health.getDetails().get("error"); assertThat(errorMessage).isNotNull(); - assertThat(errorMessage.toString().contains("A test exception")).isTrue(); + assertThat(errorMessage.toString()).contains("A test exception"); } @Test @@ -125,7 +125,7 @@ void smtpOnDefaultPortIsDown() throws MessagingException { assertThat(health.getDetails()).containsEntry("location", "smtp.acme.org"); Object errorMessage = health.getDetails().get("error"); assertThat(errorMessage).isNotNull(); - assertThat(errorMessage.toString().contains("A test exception")).isTrue(); + assertThat(errorMessage.toString()).contains("A test exception"); } @Test 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 3a2a6ab1e79c..38fe003d37fd 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 @@ -97,9 +97,9 @@ org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration org.springframework.boot.autoconfigure.pulsar.PulsarAutoConfiguration org.springframework.boot.autoconfigure.pulsar.PulsarReactiveAutoConfiguration org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration -org.springframework.boot.autoconfigure.reactor.ReactorAutoConfiguration org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration org.springframework.boot.autoconfigure.r2dbc.R2dbcTransactionManagerAutoConfiguration +org.springframework.boot.autoconfigure.reactor.ReactorAutoConfiguration org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java index 0cade879b6e0..3e8dcf01c8f3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java @@ -304,7 +304,7 @@ void enableEnumFeature() { @Test void disableJsonNodeFeature() { - this.contextRunner.withPropertyValues("spring.jackson.datatype.jsonnode.write-null-properties:false") + this.contextRunner.withPropertyValues("spring.jackson.datatype.json-node.write-null-properties:false") .run((context) -> { ObjectMapper mapper = context.getBean(ObjectMapper.class); assertThat(JsonNodeFeature.WRITE_NULL_PROPERTIES.enabledByDefault()).isTrue(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java index 8e576929f2ce..582752925976 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/client/RestTemplateAutoConfigurationTests.java @@ -68,7 +68,7 @@ void restTemplateBuilderConfigurerShouldBeLazilyDefined() { @Test void shouldFailOnCustomRestTemplateBuilderConfigurer() { - this.contextRunner.withUserConfiguration(RestTemplateCustomConfigurerConfig.class) + this.contextRunner.withUserConfiguration(RestTemplateBuilderConfigurerConfig.class) .run((context) -> assertThat(context).getFailure() .isInstanceOf(BeanDefinitionOverrideException.class) .hasMessageContaining("with name 'restTemplateBuilderConfigurer'")); @@ -273,7 +273,7 @@ RestTemplateRequestCustomizer restTemplateRequestCustomizer() { } @Configuration(proxyBeanMethods = false) - static class RestTemplateCustomConfigurerConfig { + static class RestTemplateBuilderConfigurerConfig { @Bean RestTemplateBuilderConfigurer restTemplateBuilderConfigurer() { diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/docker-compose.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/docker-compose.adoc index 9b59fadc5635..6d85a95109fc 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/docker-compose.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/docker-compose.adoc @@ -35,4 +35,4 @@ With this Docker Compose file in place, the JDBC URL used is `jdbc:postgresql:// If you want to share services between multiple applications, create the `compose.yaml` file in one of the applications and then use the configuration property configprop:spring.docker.compose.file[] in the other applications to reference the `compose.yaml` file. You should also set configprop:spring.docker.compose.lifecycle-management[] to `start-only`, as it defaults to `start-and-stop` and stopping one application would shut down the shared services for the other still running applications, too. Setting it to `start-only` won't stop the shared services on application stop, but a caveat is that if you shut down all applications, the services stay running. -You can stop the services manually by running `docker compose stop` on the commandline in the directory which contains the `compose.yaml` file. +You can stop the services manually by running `docker compose stop` on the command line in the directory which contains the `compose.yaml` file. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/spring-mvc.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/spring-mvc.adoc index bebdf8562dcd..1a137f05a2a6 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/spring-mvc.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/spring-mvc.adoc @@ -75,7 +75,7 @@ These features are described in several enums (in Jackson) that map onto propert | `true`, `false` | `com.fasterxml.jackson.databind.cfg.JsonNodeFeature` -| `spring.jackson.datatype.jsonnode.` +| `spring.jackson.datatype.json-node.` | `true`, `false` | `com.fasterxml.jackson.databind.DeserializationFeature` diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/rest-client.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/rest-client.adoc index 1c980225069b..6ec0da3ca536 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/rest-client.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/rest-client.adoc @@ -12,7 +12,7 @@ If you have Spring WebFlux on your classpath we recommend that you use `WebClien The `WebClient` interface provides a functional style API and is fully reactive. You can learn more about the `WebClient` in the dedicated {spring-framework-docs}/web/webflux-webclient.html[section in the Spring Framework docs]. -TIP: If you are not writing a reactive Spring WebFlux application you can use the a <> instead of a `WebClient`. +TIP: If you are not writing a reactive Spring WebFlux application you can use the <> instead of a `WebClient`. This provides a similar functional API, but is blocking rather than reactive. Spring Boot creates and pre-configures a prototype `WebClient.Builder` bean for you. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc index 534c7df8b806..82292dbe64aa 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc @@ -271,7 +271,7 @@ Usually, you would define the properties in your `application.properties` or `ap Common server settings include: -* Network settings: Listen port for incoming HTTP requests (`server.port`), interface address to bind to `server.address`, and so on. +* Network settings: Listen port for incoming HTTP requests (`server.port`), interface address to bind to (`server.address`), and so on. * Error management: Location of the error page (`server.error.path`) and so on. * <> * <> @@ -291,7 +291,7 @@ The following example shows programmatically setting the port: include::code:MyWebServerFactoryCustomizer[] -`JettyReactiveWebServerFactory`, `NettyReactiveWebServerFactory`, `TomcatReactiveWebServerFactory`, and `UndertowServletWebServerFactory` are dedicated variants of `ConfigurableReactiveWebServerFactory` that have additional customization setter methods for Jetty, Reactor Netty, Tomcat, and Undertow respectively. +`JettyReactiveWebServerFactory`, `NettyReactiveWebServerFactory`, `TomcatReactiveWebServerFactory`, and `UndertowReactiveWebServerFactory` are dedicated variants of `ConfigurableReactiveWebServerFactory` that have additional customization setter methods for Jetty, Reactor Netty, Tomcat, and Undertow respectively. The following example shows how to customize `NettyReactiveWebServerFactory` that provides access to Reactor Netty-specific configuration options: include::code:MyNettyWebServerFactoryCustomizer[] diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc index 6171b71ee32e..538487db2660 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc @@ -575,7 +575,7 @@ Usually, you would define the properties in your `application.properties` or `ap Common server settings include: -* Network settings: Listen port for incoming HTTP requests (`server.port`), interface address to bind to `server.address`, and so on. +* Network settings: Listen port for incoming HTTP requests (`server.port`), interface address to bind to (`server.address`), and so on. * Session settings: Whether the session is persistent (`server.servlet.session.persistent`), session timeout (`server.servlet.session.timeout`), location of session data (`server.servlet.session.store-dir`), and session-cookie configuration (`server.servlet.session.cookie.*`). * Error management: Location of the error page (`server.error.path`) and so on. * <> diff --git a/spring-boot-project/spring-boot-parent/build.gradle b/spring-boot-project/spring-boot-parent/build.gradle index 09564f5bfd55..4484d617520c 100644 --- a/spring-boot-project/spring-boot-parent/build.gradle +++ b/spring-boot-project/spring-boot-parent/build.gradle @@ -30,7 +30,7 @@ bom { library("C3P0", "0.9.5.5") { group("com.mchange") { modules = [ - "c3p0" + "c3p0" ] } } @@ -141,7 +141,7 @@ bom { library("Micrometer Context Propagation", "1.0.5") { group("io.micrometer") { modules = [ - "context-propagation" + "context-propagation" ] } } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java index 207be549873c..87c5cee78d66 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/servlet/SpringBootMockMvcBuilderCustomizerTests.java @@ -210,9 +210,4 @@ public void destroy() { } - static record RegisteredFilter(Filter filter, Map initParameters, - EnumSet dispatcherTypes) { - - } - } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java index 2130b0fc1e29..cc980b915b3b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java @@ -40,8 +40,8 @@ * contains. Unlike {@link java.util.zip.ZipFile}, this implementation can load content * from a zip file nested inside another file as long as the entry is not compressed. *

    - * In order to reduce memory consumption, this implementation stores only the the hash of - * the entry names, the central directory offsets and the original positions. Entries are + * In order to reduce memory consumption, this implementation stores only the hash of the + * entry names, the central directory offsets and the original positions. Entries are * stored internally in {@code hashCode} order so that a binary search can be used to * quickly find an entry by name or determine if the zip file doesn't have a given entry. *

    diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index 1cbd34264655..b542dc464962 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -1300,7 +1300,8 @@ public boolean isKeepAlive() { } /** - * Whether to keep the application alive even if there are no more non-daemon threads. + * Set whether to keep the application alive even if there are no more non-daemon + * threads. * @param keepAlive whether to keep the application alive even if there are no more * non-daemon threads * @since 3.2.0 From a7571cf667d9aad9d5ae0c10a45afe6b7d8b50c3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 22 Nov 2023 10:48:35 +0000 Subject: [PATCH 0816/1215] Use different default database names for Oracle XE and Oracle Free Closes gh-38476 --- .../connection/oracle/OracleContainer.java | 47 +++++++++++++++++++ .../connection/oracle/OracleEnvironment.java | 6 +-- ...DockerComposeConnectionDetailsFactory.java | 34 ++++++++++++++ ...DockerComposeConnectionDetailsFactory.java | 34 ++++++++++++++ ...DockerComposeConnectionDetailsFactory.java | 21 +++++---- ...DockerComposeConnectionDetailsFactory.java | 19 ++++---- ...DockerComposeConnectionDetailsFactory.java | 34 ++++++++++++++ ...DockerComposeConnectionDetailsFactory.java | 34 ++++++++++++++ .../main/resources/META-INF/spring.factories | 6 ++- .../oracle/OracleEnvironmentTests.java | 29 +++++++----- ...nectionDetailsFactoryIntegrationTests.java | 4 +- ...nectionDetailsFactoryIntegrationTests.java | 4 +- ...nectionDetailsFactoryIntegrationTests.java | 2 +- ...nectionDetailsFactoryIntegrationTests.java | 2 +- 14 files changed, 234 insertions(+), 42 deletions(-) create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleContainer.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeJdbcDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeR2dbcDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleXeJdbcDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleXeR2dbcDockerComposeConnectionDetailsFactory.java diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleContainer.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleContainer.java new file mode 100644 index 000000000000..55776fd3a132 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleContainer.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.oracle; + +/** + * Enumeration of supported Oracle containers. + * + * @author Andy Wilkinson + */ +enum OracleContainer { + + FREE("gvenzl/oracle-free", "freepdb1"), + + XE("gvenzl/oracle-xe", "xepdb1"); + + private final String imageName; + + private final String defaultDatabase; + + OracleContainer(String imageName, String defaultDatabase) { + this.imageName = imageName; + this.defaultDatabase = defaultDatabase; + } + + String getImageName() { + return this.imageName; + } + + String getDefaultDatabase() { + return this.defaultDatabase; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleEnvironment.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleEnvironment.java index 0540dd1983c5..c38b595263c6 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleEnvironment.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleEnvironment.java @@ -28,18 +28,16 @@ */ class OracleEnvironment { - static final String[] CONTAINER_NAMES = { "gvenzl/oracle-xe", "gvenzl/oracle-free" }; - private final String username; private final String password; private final String database; - OracleEnvironment(Map env) { + OracleEnvironment(Map env, String defaultDatabase) { this.username = env.getOrDefault("APP_USER", "system"); this.password = extractPassword(env); - this.database = env.getOrDefault("ORACLE_DATABASE", "xepdb1"); + this.database = env.getOrDefault("ORACLE_DATABASE", defaultDatabase); } private String extractPassword(Map env) { diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeJdbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeJdbcDockerComposeConnectionDetailsFactory.java new file mode 100644 index 000000000000..85e017a47d74 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeJdbcDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.oracle; + +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link JdbcConnectionDetails} + * for an {@link OracleContainer#FREE} service. + * + * @author Andy Wilkinson + */ +class OracleFreeJdbcDockerComposeConnectionDetailsFactory extends OracleJdbcDockerComposeConnectionDetailsFactory { + + protected OracleFreeJdbcDockerComposeConnectionDetailsFactory() { + super(OracleContainer.FREE); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeR2dbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeR2dbcDockerComposeConnectionDetailsFactory.java new file mode 100644 index 000000000000..3e4ae171b928 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeR2dbcDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.oracle; + +import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link R2dbcConnectionDetails} + * for an {@link OracleContainer#FREE} service. + * + * @author Andy Wilkinson + */ +class OracleFreeR2dbcDockerComposeConnectionDetailsFactory extends OracleR2dbcDockerComposeConnectionDetailsFactory { + + protected OracleFreeR2dbcDockerComposeConnectionDetailsFactory() { + super(OracleContainer.FREE); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleJdbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleJdbcDockerComposeConnectionDetailsFactory.java index 3c7ca450e873..924195ab4d90 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleJdbcDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleJdbcDockerComposeConnectionDetailsFactory.java @@ -23,27 +23,30 @@ import org.springframework.util.StringUtils; /** - * {@link DockerComposeConnectionDetailsFactory} to create {@link JdbcConnectionDetails} - * for an {@code oracle-xe} service. + * Base class for a {@link DockerComposeConnectionDetailsFactory} to create + * {@link JdbcConnectionDetails} for an {@code oracle-free} or {@code oracle-xe} service. * * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb */ -class OracleJdbcDockerComposeConnectionDetailsFactory +abstract class OracleJdbcDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { - protected OracleJdbcDockerComposeConnectionDetailsFactory() { - super(OracleEnvironment.CONTAINER_NAMES); + private final String defaultDatabase; + + protected OracleJdbcDockerComposeConnectionDetailsFactory(OracleContainer container) { + super(container.getImageName()); + this.defaultDatabase = container.getDefaultDatabase(); } @Override protected JdbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { - return new OracleJdbcDockerComposeConnectionDetails(source.getRunningService()); + return new OracleJdbcDockerComposeConnectionDetails(source.getRunningService(), this.defaultDatabase); } /** - * {@link JdbcConnectionDetails} backed by an {@code oracle-xe} + * {@link JdbcConnectionDetails} backed by an {@code oracle-xe} or {@code oracle-free} * {@link RunningService}. */ static class OracleJdbcDockerComposeConnectionDetails extends DockerComposeConnectionDetails @@ -55,9 +58,9 @@ static class OracleJdbcDockerComposeConnectionDetails extends DockerComposeConne private final String jdbcUrl; - OracleJdbcDockerComposeConnectionDetails(RunningService service) { + OracleJdbcDockerComposeConnectionDetails(RunningService service, String defaultDatabase) { super(service); - this.environment = new OracleEnvironment(service.env()); + this.environment = new OracleEnvironment(service.env(), defaultDatabase); this.jdbcUrl = "jdbc:oracle:thin:@" + service.host() + ":" + service.ports().get(1521) + "/" + this.environment.getDatabase() + getParameters(service); } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleR2dbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleR2dbcDockerComposeConnectionDetailsFactory.java index 01788dca4989..9d54bee83f67 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleR2dbcDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleR2dbcDockerComposeConnectionDetailsFactory.java @@ -25,23 +25,26 @@ import org.springframework.boot.docker.compose.service.connection.r2dbc.ConnectionFactoryOptionsBuilder; /** - * {@link DockerComposeConnectionDetailsFactory} to create {@link R2dbcConnectionDetails} - * for an {@code oracle-xe} service. + * Base class for a {@link DockerComposeConnectionDetailsFactory} to create + * {@link R2dbcConnectionDetails} for an {@code oracle-free} or {@code oracle-xe} service. * * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb */ -class OracleR2dbcDockerComposeConnectionDetailsFactory +abstract class OracleR2dbcDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { - OracleR2dbcDockerComposeConnectionDetailsFactory() { - super(OracleEnvironment.CONTAINER_NAMES, "io.r2dbc.spi.ConnectionFactoryOptions"); + private final String defaultDatabase; + + OracleR2dbcDockerComposeConnectionDetailsFactory(OracleContainer container) { + super(container.getImageName(), "io.r2dbc.spi.ConnectionFactoryOptions"); + this.defaultDatabase = container.getDefaultDatabase(); } @Override protected R2dbcConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { - return new OracleDbR2dbcDockerComposeConnectionDetails(source.getRunningService()); + return new OracleDbR2dbcDockerComposeConnectionDetails(source.getRunningService(), this.defaultDatabase); } /** @@ -56,9 +59,9 @@ static class OracleDbR2dbcDockerComposeConnectionDetails extends DockerComposeCo private final ConnectionFactoryOptions connectionFactoryOptions; - OracleDbR2dbcDockerComposeConnectionDetails(RunningService service) { + OracleDbR2dbcDockerComposeConnectionDetails(RunningService service, String defaultDatabase) { super(service); - OracleEnvironment environment = new OracleEnvironment(service.env()); + OracleEnvironment environment = new OracleEnvironment(service.env(), defaultDatabase); this.connectionFactoryOptions = connectionFactoryOptionsBuilder.build(service, environment.getDatabase(), environment.getUsername(), environment.getPassword()); } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleXeJdbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleXeJdbcDockerComposeConnectionDetailsFactory.java new file mode 100644 index 000000000000..75da136d567e --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleXeJdbcDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.oracle; + +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link JdbcConnectionDetails} + * for an {@link OracleContainer#XE} service. + * + * @author Andy Wilkinson + */ +class OracleXeJdbcDockerComposeConnectionDetailsFactory extends OracleJdbcDockerComposeConnectionDetailsFactory { + + protected OracleXeJdbcDockerComposeConnectionDetailsFactory() { + super(OracleContainer.XE); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleXeR2dbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleXeR2dbcDockerComposeConnectionDetailsFactory.java new file mode 100644 index 000000000000..f5b02edde660 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleXeR2dbcDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.oracle; + +import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link R2dbcConnectionDetails} + * for an {@link OracleContainer#XE} service. + * + * @author Andy Wilkinson + */ +class OracleXeR2dbcDockerComposeConnectionDetailsFactory extends OracleR2dbcDockerComposeConnectionDetailsFactory { + + protected OracleXeR2dbcDockerComposeConnectionDetailsFactory() { + super(OracleContainer.XE); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories index f28a89272d9e..f00fc1938155 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories @@ -16,8 +16,10 @@ org.springframework.boot.docker.compose.service.connection.mongo.MongoDockerComp org.springframework.boot.docker.compose.service.connection.mysql.MySqlJdbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.mysql.MySqlR2dbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.neo4j.Neo4jDockerComposeConnectionDetailsFactory,\ -org.springframework.boot.docker.compose.service.connection.oracle.OracleJdbcDockerComposeConnectionDetailsFactory,\ -org.springframework.boot.docker.compose.service.connection.oracle.OracleR2dbcDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.oracle.OracleFreeJdbcDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.oracle.OracleXeJdbcDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.oracle.OracleFreeR2dbcDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.oracle.OracleXeR2dbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.otlp.OpenTelemetryMetricsDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.otlp.OpenTelemetryTracingDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.postgres.PostgresJdbcDockerComposeConnectionDetailsFactory,\ diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleEnvironmentTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleEnvironmentTests.java index 4b9f37fca7bc..b66b55196736 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleEnvironmentTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleEnvironmentTests.java @@ -35,77 +35,80 @@ class OracleEnvironmentTests { @Test void getUsernameWhenHasAppUser() { OracleEnvironment environment = new OracleEnvironment( - Map.of("APP_USER", "alice", "APP_USER_PASSWORD", "secret")); + Map.of("APP_USER", "alice", "APP_USER_PASSWORD", "secret"), "defaultDb"); assertThat(environment.getUsername()).isEqualTo("alice"); } @Test void getUsernameWhenHasNoAppUser() { - OracleEnvironment environment = new OracleEnvironment(Map.of("ORACLE_PASSWORD", "secret")); + OracleEnvironment environment = new OracleEnvironment(Map.of("ORACLE_PASSWORD", "secret"), "defaultDb"); assertThat(environment.getUsername()).isEqualTo("system"); } @Test void getPasswordWhenHasAppPassword() { OracleEnvironment environment = new OracleEnvironment( - Map.of("APP_USER", "alice", "APP_USER_PASSWORD", "secret")); + Map.of("APP_USER", "alice", "APP_USER_PASSWORD", "secret"), "defaultDb"); assertThat(environment.getPassword()).isEqualTo("secret"); } @Test void getPasswordWhenHasOraclePassword() { - OracleEnvironment environment = new OracleEnvironment(Map.of("ORACLE_PASSWORD", "secret")); + OracleEnvironment environment = new OracleEnvironment(Map.of("ORACLE_PASSWORD", "secret"), "defaultDb"); assertThat(environment.getPassword()).isEqualTo("secret"); } @Test void createWhenRandomPasswordAndAppPasswordDoesNotThrow() { assertThatNoException().isThrownBy(() -> new OracleEnvironment( - Map.of("APP_USER", "alice", "APP_USER_PASSWORD", "secret", "ORACLE_RANDOM_PASSWORD", "true"))); + Map.of("APP_USER", "alice", "APP_USER_PASSWORD", "secret", "ORACLE_RANDOM_PASSWORD", "true"), + "defaultDb")); } @Test void createWhenRandomPasswordThrowsException() { assertThatIllegalStateException() - .isThrownBy(() -> new OracleEnvironment(Map.of("ORACLE_RANDOM_PASSWORD", "true"))) + .isThrownBy(() -> new OracleEnvironment(Map.of("ORACLE_RANDOM_PASSWORD", "true"), "defaultDb")) .withMessage("ORACLE_RANDOM_PASSWORD is not supported without APP_USER and APP_USER_PASSWORD"); } @Test void createWhenAppUserAndNoAppPasswordThrowsException() { - assertThatIllegalStateException().isThrownBy(() -> new OracleEnvironment(Map.of("APP_USER", "alice"))) + assertThatIllegalStateException() + .isThrownBy(() -> new OracleEnvironment(Map.of("APP_USER", "alice"), "defaultDb")) .withMessage("No Oracle app password found"); } @Test void createWhenAppUserAndEmptyAppPasswordThrowsException() { assertThatIllegalStateException() - .isThrownBy(() -> new OracleEnvironment(Map.of("APP_USER", "alice", "APP_USER_PASSWORD", ""))) + .isThrownBy(() -> new OracleEnvironment(Map.of("APP_USER", "alice", "APP_USER_PASSWORD", ""), "defaultDb")) .withMessage("No Oracle app password found"); } @Test void createWhenHasNoPasswordThrowsException() { - assertThatIllegalStateException().isThrownBy(() -> new OracleEnvironment(Collections.emptyMap())) + assertThatIllegalStateException().isThrownBy(() -> new OracleEnvironment(Collections.emptyMap(), "defaultDb")) .withMessage("No Oracle password found"); } @Test void createWhenHasEmptyPasswordThrowsException() { - assertThatIllegalStateException().isThrownBy(() -> new OracleEnvironment(Map.of("ORACLE_PASSWORD", ""))) + assertThatIllegalStateException() + .isThrownBy(() -> new OracleEnvironment(Map.of("ORACLE_PASSWORD", ""), "defaultDb")) .withMessage("No Oracle password found"); } @Test void getDatabaseWhenHasNoOracleDatabase() { - OracleEnvironment environment = new OracleEnvironment(Map.of("ORACLE_PASSWORD", "secret")); - assertThat(environment.getDatabase()).isEqualTo("xepdb1"); + OracleEnvironment environment = new OracleEnvironment(Map.of("ORACLE_PASSWORD", "secret"), "defaultDb"); + assertThat(environment.getDatabase()).isEqualTo("defaultDb"); } @Test void getDatabaseWhenHasOracleDatabase() { OracleEnvironment environment = new OracleEnvironment( - Map.of("ORACLE_PASSWORD", "secret", "ORACLE_DATABASE", "db")); + Map.of("ORACLE_PASSWORD", "secret", "ORACLE_DATABASE", "db"), "defaultDb"); assertThat(environment.getDatabase()).isEqualTo("db"); } diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java index 9857bcd11c66..127fe0f57467 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -35,7 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link OracleJdbcDockerComposeConnectionDetailsFactory} + * Integration tests for {@link OracleFreeJdbcDockerComposeConnectionDetailsFactory} * * @author Andy Wilkinson */ @@ -54,7 +54,7 @@ void runCreatesConnectionDetailsThatCanBeUsedToAccessDatabase() throws Exception JdbcConnectionDetails connectionDetails = run(JdbcConnectionDetails.class); assertThat(connectionDetails.getUsername()).isEqualTo("app_user"); assertThat(connectionDetails.getPassword()).isEqualTo("app_user_secret"); - assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:oracle:thin:@").endsWith("/xepdb1"); + assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:oracle:thin:@").endsWith("/freepdb1"); SimpleDriverDataSource dataSource = new SimpleDriverDataSource(); dataSource.setUrl(connectionDetails.getJdbcUrl()); dataSource.setUsername(connectionDetails.getUsername()); diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java index 49dd298f4e84..dea004833c4c 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleFreeR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -34,7 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link OracleR2dbcDockerComposeConnectionDetailsFactory} + * Integration tests for {@link OracleFreeR2dbcDockerComposeConnectionDetailsFactory} * * @author Andy Wilkinson */ @@ -51,7 +51,7 @@ class OracleFreeR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests void runCreatesConnectionDetailsThatCanBeUsedToAccessDatabase() { R2dbcConnectionDetails connectionDetails = run(R2dbcConnectionDetails.class); ConnectionFactoryOptions connectionFactoryOptions = connectionDetails.getConnectionFactoryOptions(); - assertThat(connectionFactoryOptions.toString()).contains("database=xepdb1", "driver=oracle", + assertThat(connectionFactoryOptions.toString()).contains("database=freepdb1", "driver=oracle", "password=REDACTED", "user=app_user"); assertThat(connectionFactoryOptions.getRequiredValue(ConnectionFactoryOptions.PASSWORD)) .isEqualTo("app_user_secret"); diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleXeJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleXeJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java index e191174c986d..19ebd9262605 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleXeJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleXeJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -35,7 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link OracleJdbcDockerComposeConnectionDetailsFactory} + * Integration tests for {@link OracleXeJdbcDockerComposeConnectionDetailsFactory} * * @author Andy Wilkinson */ diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleXeR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleXeR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java index 76766e8d12b9..2b044d3fb36b 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleXeR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oracle/OracleXeR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -34,7 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link OracleR2dbcDockerComposeConnectionDetailsFactory} + * Integration tests for {@link OracleXeR2dbcDockerComposeConnectionDetailsFactory} * * @author Andy Wilkinson */ From 4d33676c045141f7bad1202d89d163426308c02d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 22 Nov 2023 18:19:46 +0000 Subject: [PATCH 0817/1215] Upgrade to Spring Batch 5.1.0 Closes gh-38310 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 01adb3570c32..a3b22ebc61e1 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1507,7 +1507,7 @@ bom { ] } } - library("Spring Batch", "5.1.0-SNAPSHOT") { + library("Spring Batch", "5.1.0") { considerSnapshots() group("org.springframework.batch") { imports = [ From fc00c4006aab015f211fc67c47a2e0451162ef63 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 22 Nov 2023 18:19:47 +0000 Subject: [PATCH 0818/1215] Upgrade to Spring Integration 6.2.0 Closes gh-38315 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index a3b22ebc61e1..1768b5b755a0 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1550,7 +1550,7 @@ bom { ] } } - library("Spring Integration", "6.2.0-SNAPSHOT") { + library("Spring Integration", "6.2.0") { considerSnapshots() group("org.springframework.integration") { imports = [ From 9c68a2ab876e5011718dbe6abb8f5902f7031deb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 22 Nov 2023 19:19:38 +0000 Subject: [PATCH 0819/1215] Integrate child management context with parent context's lifecycle Previously, the child management context was created when the parent context's web server was initialized and it wasn't stopped or closed until the parent context was closed. This resulted in the child context being left running when the parent context was stopped. This would then cause a failure when the parent context was started again as another web server initialized event would be received and a second child management context would be started. This commit updates the initialization of the child management context to integrate it with the lifecycle of the parent context. The management context is now created the first time the parent context is started. It is stopped when the parent context is stopped and restarted if the parent context is started again. This lifecycle management is done using a phase that ensures that the child context is not started until the parent context's web server has been started. Fixes gh-38502 --- .../ChildManagementContextInitializer.java | 33 ++++++++++++++++--- ...nagementContextAutoConfigurationTests.java | 15 +++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ChildManagementContextInitializer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ChildManagementContextInitializer.java index 38a6a68f36e5..579e7d7126d5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ChildManagementContextInitializer.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ChildManagementContextInitializer.java @@ -33,12 +33,13 @@ import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.context.event.ApplicationFailedEvent; import org.springframework.boot.web.context.ConfigurableWebServerApplicationContext; -import org.springframework.boot.web.context.WebServerInitializedEvent; +import org.springframework.boot.web.context.WebServerGracefulShutdownLifecycle; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.SmartLifecycle; import org.springframework.context.annotation.AnnotationConfigRegistry; import org.springframework.context.aot.ApplicationContextAotGenerator; import org.springframework.context.event.ContextClosedEvent; @@ -55,8 +56,7 @@ * @author Andy Wilkinson * @author Phillip Webb */ -class ChildManagementContextInitializer - implements ApplicationListener, BeanRegistrationAotProcessor { +class ChildManagementContextInitializer implements BeanRegistrationAotProcessor, SmartLifecycle { private final ManagementContextFactory managementContextFactory; @@ -64,6 +64,8 @@ class ChildManagementContextInitializer private final ApplicationContextInitializer applicationContextInitializer; + private volatile ConfigurableApplicationContext managementContext; + ChildManagementContextInitializer(ManagementContextFactory managementContextFactory, ApplicationContext parentContext) { this(managementContextFactory, parentContext, null); @@ -79,14 +81,35 @@ private ChildManagementContextInitializer(ManagementContextFactory managementCon } @Override - public void onApplicationEvent(WebServerInitializedEvent event) { - if (event.getApplicationContext().equals(this.parentContext)) { + public void start() { + if (this.managementContext == null) { ConfigurableApplicationContext managementContext = createManagementContext(); registerBeans(managementContext); managementContext.refresh(); + this.managementContext = managementContext; + } + else { + this.managementContext.start(); + } + } + + @Override + public void stop() { + if (this.managementContext != null) { + this.managementContext.stop(); } } + @Override + public boolean isRunning() { + return this.managementContext != null && this.managementContext.isRunning(); + } + + @Override + public int getPhase() { + return WebServerGracefulShutdownLifecycle.SMART_LIFECYCLE_PHASE + 512; + } + @Override public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { Assert.isInstanceOf(ConfigurableApplicationContext.class, this.parentContext); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementContextAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementContextAutoConfigurationTests.java index 1d4e7c731983..52fca8b17ae8 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementContextAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementContextAutoConfigurationTests.java @@ -55,6 +55,21 @@ void childManagementContextShouldStartForEmbeddedServer(CapturedOutput output) { .run((context) -> assertThat(output).satisfies(numberOfOccurrences("Tomcat started on port", 2))); } + @Test + void childManagementContextShouldRestartWhenParentIsStoppedThenStarted(CapturedOutput output) { + WebApplicationContextRunner contextRunner = new WebApplicationContextRunner( + AnnotationConfigServletWebServerApplicationContext::new) + .withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class, + ServletWebServerFactoryAutoConfiguration.class, ServletManagementContextAutoConfiguration.class, + WebEndpointAutoConfiguration.class, EndpointAutoConfiguration.class)); + contextRunner.withPropertyValues("server.port=0", "management.server.port=0").run((context) -> { + assertThat(output).satisfies(numberOfOccurrences("Tomcat started on port", 2)); + context.getSourceApplicationContext().stop(); + context.getSourceApplicationContext().start(); + assertThat(output).satisfies(numberOfOccurrences("Tomcat started on port", 4)); + }); + } + @Test void givenSamePortManagementServerWhenManagementServerAddressIsConfiguredThenContextRefreshFails() { WebApplicationContextRunner contextRunner = new WebApplicationContextRunner( From f613ab89b9cb83ec19bc35fb63068f31e22c86a0 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 23 Nov 2023 09:28:09 +0100 Subject: [PATCH 0820/1215] Auto-configure observations for RestClients Closes gh-38500 --- ...tpClientObservationsAutoConfiguration.java | 7 +- .../RestClientObservationConfiguration.java | 53 ++++++ .../RestTemplateObservationConfiguration.java | 3 +- ...stClientObservationConfigurationTests.java | 175 ++++++++++++++++++ ...ationConfigurationWithoutMetricsTests.java | 75 ++++++++ .../ObservationRestClientCustomizer.java | 58 ++++++ .../ObservationRestClientCustomizerTests.java | 54 ++++++ .../src/docs/asciidoc/actuator/metrics.adoc | 7 +- 8 files changed, 425 insertions(+), 7 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfigurationWithoutMetricsTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/ObservationRestClientCustomizer.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/client/ObservationRestClientCustomizerTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/HttpClientObservationsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/HttpClientObservationsAutoConfiguration.java index 563a805e654c..60595014ef39 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/HttpClientObservationsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/HttpClientObservationsAutoConfiguration.java @@ -31,6 +31,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.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; @@ -48,13 +49,15 @@ * @author Stephane Nicoll * @author Raheela Aslam * @author Brian Clozel + * @author Moritz Halbritter * @since 3.0.0 */ @AutoConfiguration(after = { ObservationAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, - RestTemplateAutoConfiguration.class, WebClientAutoConfiguration.class }) + RestTemplateAutoConfiguration.class, WebClientAutoConfiguration.class, RestClientAutoConfiguration.class }) @ConditionalOnClass(Observation.class) @ConditionalOnBean(ObservationRegistry.class) -@Import({ RestTemplateObservationConfiguration.class, WebClientObservationConfiguration.class }) +@Import({ RestTemplateObservationConfiguration.class, WebClientObservationConfiguration.class, + RestClientObservationConfiguration.class }) @EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class }) public class HttpClientObservationsAutoConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfiguration.java new file mode 100644 index 000000000000..6b97d6c65e51 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfiguration.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.observation.web.client; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties; +import org.springframework.boot.actuate.metrics.web.client.ObservationRestClientCustomizer; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.web.client.RestClientCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.observation.ClientRequestObservationConvention; +import org.springframework.http.client.observation.DefaultClientRequestObservationConvention; +import org.springframework.web.client.RestClient; + +/** + * Configure the instrumentation of {@link RestClient}. + * + * @author Moritz Halbritter + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(RestClient.class) +@ConditionalOnBean(RestClient.Builder.class) +class RestClientObservationConfiguration { + + @Bean + RestClientCustomizer observationRestClientCustomizer(ObservationRegistry observationRegistry, + ObjectProvider customConvention, + ObservationProperties observationProperties) { + String name = observationProperties.getHttp().getClient().getRequests().getName(); + ClientRequestObservationConvention observationConvention = customConvention + .getIfAvailable(() -> new DefaultClientRequestObservationConvention(name)); + return new ObservationRestClientCustomizer(observationRegistry, observationConvention); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfiguration.java index a1d2a152de56..81fb154a230a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfiguration.java @@ -19,7 +19,6 @@ import io.micrometer.observation.ObservationRegistry; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties; import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties; import org.springframework.boot.actuate.metrics.web.client.ObservationRestTemplateCustomizer; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -44,7 +43,7 @@ class RestTemplateObservationConfiguration { @Bean ObservationRestTemplateCustomizer observationRestTemplateCustomizer(ObservationRegistry observationRegistry, ObjectProvider customConvention, - ObservationProperties observationProperties, MetricsProperties metricsProperties) { + ObservationProperties observationProperties) { String name = observationProperties.getHttp().getClient().getRequests().getName(); ClientRequestObservationConvention observationConvention = customConvention .getIfAvailable(() -> new DefaultClientRequestObservationConvention(name)); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfigurationTests.java new file mode 100644 index 000000000000..1400c4f6c027 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfigurationTests.java @@ -0,0 +1,175 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.observation.web.client; + +import io.micrometer.common.KeyValues; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistryAssert; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; +import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; +import org.springframework.boot.actuate.metrics.web.client.ObservationRestClientCustomizer; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.boot.test.web.client.MockServerRestClientCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.observation.ClientRequestObservationContext; +import org.springframework.http.client.observation.DefaultClientRequestObservationConvention; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.Builder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; + +/** + * Tests for {@link RestClientObservationConfiguration}. + * + * @author Brian Clozel + * @author Moritz Halbritter + */ +@ExtendWith(OutputCaptureExtension.class) +class RestClientObservationConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withBean(ObservationRegistry.class, TestObservationRegistry::create) + .withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class, RestClientAutoConfiguration.class, + HttpClientObservationsAutoConfiguration.class)); + + @Test + void contributesCustomizerBean() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ObservationRestClientCustomizer.class)); + } + + @Test + void restClientCreatedWithBuilderIsInstrumented() { + this.contextRunner.run((context) -> { + RestClient restClient = buildRestClient(context); + restClient.get().uri("/projects/{project}", "spring-boot").retrieve().toBodilessEntity(); + TestObservationRegistry registry = context.getBean(TestObservationRegistry.class); + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualToIgnoringCase("http.client.requests"); + }); + } + + @Test + void restClientCreatedWithBuilderUsesCustomConventionName() { + final String observationName = "test.metric.name"; + this.contextRunner.withPropertyValues("management.observations.http.client.requests.name=" + observationName) + .run((context) -> { + RestClient restClient = buildRestClient(context); + restClient.get().uri("/projects/{project}", "spring-boot").retrieve().toBodilessEntity(); + TestObservationRegistry registry = context.getBean(TestObservationRegistry.class); + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualToIgnoringCase(observationName); + }); + } + + @Test + void restClientCreatedWithBuilderUsesCustomConvention() { + this.contextRunner.withUserConfiguration(CustomConvention.class).run((context) -> { + RestClient restClient = buildRestClient(context); + restClient.get().uri("/projects/{project}", "spring-boot").retrieve().toBodilessEntity(); + TestObservationRegistry registry = context.getBean(TestObservationRegistry.class); + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualTo("http.client.requests") + .that() + .hasLowCardinalityKeyValue("project", "spring-boot"); + }); + } + + @Test + void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) { + this.contextRunner.with(MetricsRun.simple()) + .withPropertyValues("management.metrics.web.client.max-uri-tags=2") + .run((context) -> { + RestClientWithMockServer restClientWithMockServer = buildRestClientAndMockServer(context); + MockRestServiceServer server = restClientWithMockServer.mockServer(); + RestClient restClient = restClientWithMockServer.restClient(); + for (int i = 0; i < 3; i++) { + server.expect(requestTo("/test/" + i)).andRespond(withStatus(HttpStatus.OK)); + } + for (int i = 0; i < 3; i++) { + restClient.get().uri("/test/" + i, String.class).retrieve().toBodilessEntity(); + } + TestObservationRegistry registry = context.getBean(TestObservationRegistry.class); + TestObservationRegistryAssert.assertThat(registry) + .hasNumberOfObservationsWithNameEqualTo("http.client.requests", 3); + MeterRegistry meterRegistry = context.getBean(MeterRegistry.class); + assertThat(meterRegistry.find("http.client.requests").timers()).hasSize(2); + assertThat(output).contains("Reached the maximum number of URI tags for 'http.client.requests'.") + .contains("Are you using 'uriVariables'?"); + }); + } + + @Test + void backsOffWhenRestClientBuilderIsMissing() { + new ApplicationContextRunner().with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class, + HttpClientObservationsAutoConfiguration.class)) + .run((context) -> assertThat(context).doesNotHaveBean(ObservationRestClientCustomizer.class)); + } + + private RestClient buildRestClient(AssertableApplicationContext context) { + RestClientWithMockServer restClientWithMockServer = buildRestClientAndMockServer(context); + restClientWithMockServer.mockServer() + .expect(requestTo("/projects/spring-boot")) + .andRespond(withStatus(HttpStatus.OK)); + return restClientWithMockServer.restClient(); + } + + private RestClientWithMockServer buildRestClientAndMockServer(AssertableApplicationContext context) { + Builder builder = context.getBean(Builder.class); + MockServerRestClientCustomizer customizer = new MockServerRestClientCustomizer(); + customizer.customize(builder); + return new RestClientWithMockServer(builder.build(), customizer.getServer()); + } + + private record RestClientWithMockServer(RestClient restClient, MockRestServiceServer mockServer) { + } + + @Configuration(proxyBeanMethods = false) + static class CustomConventionConfiguration { + + @Bean + CustomConvention customConvention() { + return new CustomConvention(); + } + + } + + static class CustomConvention extends DefaultClientRequestObservationConvention { + + @Override + public KeyValues getLowCardinalityKeyValues(ClientRequestObservationContext context) { + return super.getLowCardinalityKeyValues(context).and("project", "spring-boot"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfigurationWithoutMetricsTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfigurationWithoutMetricsTests.java new file mode 100644 index 000000000000..3aa82c08c26b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestClientObservationConfigurationWithoutMetricsTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.observation.web.client; + +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistryAssert; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.boot.test.web.client.MockServerRestClientCustomizer; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; +import org.springframework.http.HttpStatus; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.Builder; + +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; + +/** + * Tests for {@link RestClientObservationConfiguration} without Micrometer Metrics. + * + * @author Brian Clozel + * @author Andy Wilkinson + * @author Moritz Halbritter + */ +@ExtendWith(OutputCaptureExtension.class) +@ClassPathExclusions("micrometer-core-*.jar") +class RestClientObservationConfigurationWithoutMetricsTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withBean(ObservationRegistry.class, TestObservationRegistry::create) + .withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class, RestClientAutoConfiguration.class, + HttpClientObservationsAutoConfiguration.class)); + + @Test + void restClientCreatedWithBuilderIsInstrumented() { + this.contextRunner.run((context) -> { + RestClient restClient = buildRestClient(context); + restClient.get().uri("/projects/{project}", "spring-boot").retrieve().toBodilessEntity(); + TestObservationRegistry registry = context.getBean(TestObservationRegistry.class); + TestObservationRegistryAssert.assertThat(registry) + .hasObservationWithNameEqualToIgnoringCase("http.client.requests"); + }); + } + + private RestClient buildRestClient(AssertableApplicationContext context) { + Builder builder = context.getBean(Builder.class); + MockServerRestClientCustomizer customizer = new MockServerRestClientCustomizer(); + customizer.customize(builder); + customizer.getServer().expect(requestTo("/projects/spring-boot")).andRespond(withStatus(HttpStatus.OK)); + return builder.build(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/ObservationRestClientCustomizer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/ObservationRestClientCustomizer.java new file mode 100644 index 000000000000..904e94260340 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/client/ObservationRestClientCustomizer.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2023 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.actuate.metrics.web.client; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.boot.web.client.RestClientCustomizer; +import org.springframework.http.client.observation.ClientRequestObservationConvention; +import org.springframework.util.Assert; +import org.springframework.web.client.RestClient.Builder; + +/** + * {@link RestClientCustomizer} that configures the {@link Builder RestClient builder} to + * record request observations. + * + * @author Moritz Halbritter + * @since 3.2.0 + */ +public class ObservationRestClientCustomizer implements RestClientCustomizer { + + private final ObservationRegistry observationRegistry; + + private final ClientRequestObservationConvention observationConvention; + + /** + * Create a new {@link ObservationRestClientCustomizer}. + * @param observationRegistry the observation registry + * @param observationConvention the observation convention + */ + public ObservationRestClientCustomizer(ObservationRegistry observationRegistry, + ClientRequestObservationConvention observationConvention) { + Assert.notNull(observationConvention, "ObservationConvention must not be null"); + Assert.notNull(observationRegistry, "ObservationRegistry must not be null"); + this.observationRegistry = observationRegistry; + this.observationConvention = observationConvention; + } + + @Override + public void customize(Builder restClientBuilder) { + restClientBuilder.observationRegistry(this.observationRegistry); + restClientBuilder.observationConvention(this.observationConvention); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/client/ObservationRestClientCustomizerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/client/ObservationRestClientCustomizerTests.java new file mode 100644 index 000000000000..b945b4d7f596 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/client/ObservationRestClientCustomizerTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2023 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.actuate.metrics.web.client; + +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistry; +import org.junit.jupiter.api.Test; + +import org.springframework.http.client.observation.DefaultClientRequestObservationConvention; +import org.springframework.web.client.RestClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ObservationRestClientCustomizer}. + * + * @author Brian Clozel + * @author Moritz Halbritter + */ +class ObservationRestClientCustomizerTests { + + private static final String TEST_METRIC_NAME = "http.test.metric.name"; + + private final ObservationRegistry observationRegistry = TestObservationRegistry.create(); + + private final RestClient.Builder restClientBuilder = RestClient.builder(); + + private final ObservationRestClientCustomizer customizer = new ObservationRestClientCustomizer( + this.observationRegistry, new DefaultClientRequestObservationConvention(TEST_METRIC_NAME)); + + @Test + void shouldCustomizeObservationConfiguration() { + this.customizer.customize(this.restClientBuilder); + assertThat(this.restClientBuilder).hasFieldOrPropertyWithValue("observationRegistry", this.observationRegistry); + assertThat(this.restClientBuilder).extracting("observationConvention") + .isInstanceOf(DefaultClientRequestObservationConvention.class) + .hasFieldOrPropertyWithValue("name", TEST_METRIC_NAME); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc index af495e2ac885..9248822b142e 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc @@ -822,20 +822,21 @@ To customize the tags, provide a `@Bean` that implements `JerseyTagsProvider`. [[actuator.metrics.supported.http-clients]] ==== HTTP Client Metrics -Spring Boot Actuator manages the instrumentation of both `RestTemplate` and `WebClient`. +Spring Boot Actuator manages the instrumentation of `RestTemplate`, `WebClient` and `RestClient`. For that, you have to inject the auto-configured builder and use it to create instances: * `RestTemplateBuilder` for `RestTemplate` * `WebClient.Builder` for `WebClient` +* `RestClient.Builder` for `RestClient` -You can also manually apply the customizers responsible for this instrumentation, namely `ObservationRestTemplateCustomizer` and `ObservationWebClientCustomizer`. +You can also manually apply the customizers responsible for this instrumentation, namely `ObservationRestTemplateCustomizer`, `ObservationWebClientCustomizer` and `ObservationRestClientCustomizer`. By default, metrics are generated with the name, `http.client.requests`. You can customize the name by setting the configprop:management.observations.http.client.requests.name[] property. See the {spring-framework-docs}/integration/observability.html#observability.http-client[Spring Framework reference documentation for more information on produced observations]. -To customize the tags when using `RestTemplate`, provide a `@Bean` that implements `ClientRequestObservationConvention` from the `org.springframework.http.client.observation` package. +To customize the tags when using `RestTemplate` or `RestClient`, provide a `@Bean` that implements `ClientRequestObservationConvention` from the `org.springframework.http.client.observation` package. To customize the tags when using `WebClient`, provide a `@Bean` that implements `ClientRequestObservationConvention` from the `org.springframework.web.reactive.function.client` package. From f9f73aa14684f96b2e6fb699939b1857daeda2f7 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Thu, 23 Nov 2023 08:43:16 +0900 Subject: [PATCH 0821/1215] Polish See gh-38508 --- .../ConcurrentKafkaListenerContainerFactoryConfigurer.java | 2 +- .../boot/autoconfigure/kafka/KafkaAutoConfiguration.java | 2 +- .../boot/autoconfigure/kafka/KafkaProperties.java | 4 ++-- .../spring-boot-docs/src/docs/asciidoc/howto/data-access.adoc | 2 +- .../spring-boot-docs/src/docs/asciidoc/web/servlet.adoc | 2 +- .../org/springframework/boot/loader/jar/NestedJarFile.java | 2 +- .../boot/loader/net/protocol/jar/UrlJarFileFactory.java | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java index 95108c275e0d..86cfd258ee93 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/ConcurrentKafkaListenerContainerFactoryConfigurer.java @@ -236,7 +236,7 @@ private void configureContainer(ContainerProperties container) { map.from(properties::getLogContainerConfig).to(container::setLogContainerConfig); map.from(properties::isMissingTopicsFatal).to(container::setMissingTopicsFatal); map.from(properties::isImmediateStop).to(container::setStopImmediate); - map.from(properties::getObservationEnabled).to(container::setObservationEnabled); + map.from(properties::isObservationEnabled).to(container::setObservationEnabled); map.from(this.transactionManager).to(container::setTransactionManager); map.from(this.rebalanceListener).to(container::setConsumerRebalanceListener); map.from(this.listenerTaskExecutor).to(container::setListenerTaskExecutor); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfiguration.java index b9d2edc045f7..18e02adb854a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfiguration.java @@ -98,7 +98,7 @@ PropertiesKafkaConnectionDetails kafkaConnectionDetails(KafkaProperties properti map.from(kafkaProducerListener).to(kafkaTemplate::setProducerListener); map.from(this.properties.getTemplate().getDefaultTopic()).to(kafkaTemplate::setDefaultTopic); map.from(this.properties.getTemplate().getTransactionIdPrefix()).to(kafkaTemplate::setTransactionIdPrefix); - map.from(this.properties.getTemplate().getObservationEnabled()).to(kafkaTemplate::setObservationEnabled); + map.from(this.properties.getTemplate().isObservationEnabled()).to(kafkaTemplate::setObservationEnabled); return kafkaTemplate; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java index f1ad2df87e0e..20085141e501 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java @@ -1005,7 +1005,7 @@ public void setTransactionIdPrefix(String transactionIdPrefix) { this.transactionIdPrefix = transactionIdPrefix; } - public boolean getObservationEnabled() { + public boolean isObservationEnabled() { return this.observationEnabled; } @@ -1279,7 +1279,7 @@ public void setChangeConsumerThreadName(Boolean changeConsumerThreadName) { this.changeConsumerThreadName = changeConsumerThreadName; } - public boolean getObservationEnabled() { + public boolean isObservationEnabled() { return this.observationEnabled; } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-access.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-access.adoc index 50d30a2c2d96..5c5894d83724 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-access.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-access.adoc @@ -150,7 +150,7 @@ Note that each `configuration` sub namespace provides advanced settings based on [[howto.data-access.spring-data-repositories]] === Use Spring Data Repositories Spring Data can create implementations of `@Repository` interfaces of various flavors. -Spring Boot handles all of that for you, as long as those `@Repositories` are included in one of the <>, typically the package (or a sub-package) of your main application class that is annotated with `@SpringBootApplication` or `@EnableAutoConfiguration`. +Spring Boot handles all of that for you, as long as those `@Repository` annotations are included in one of the <>, typically the package (or a sub-package) of your main application class that is annotated with `@SpringBootApplication` or `@EnableAutoConfiguration`. For many applications, all you need is to put the right Spring Data dependencies on your classpath. There is a `spring-boot-starter-data-jpa` for JPA, `spring-boot-starter-data-mongodb` for Mongodb, and various other starters for supported technologies. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc index 538487db2660..19503eab0796 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc @@ -624,7 +624,7 @@ include::code:MySameSiteConfiguration[] The character encoding behavior of the embedded servlet container for request and response handling can be configured using the `server.servlet.encoding.*` configuration properties. When a request's `Accept-Language` header indicates a locale for the request it will be automatically mapped to a charset by the servlet container. -Each containers providers default locale to charset mappings and you should verify that they meet your application's needs. +Each container provides default locale to charset mappings and you should verify that they meet your application's needs. When they do not, use the configprop:server.servlet.encoding.mapping[] configuration property to customize the mappings, as shown in the following example: [source,yaml,indent=0,subs="verbatim",configprops,configblocks] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java index bddd274e22b9..401157b17d95 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java @@ -814,7 +814,7 @@ private class RawZipDataInputStream extends FilterInputStream { private volatile boolean closed; - protected RawZipDataInputStream(InputStream in) { + RawZipDataInputStream(InputStream in) { super(in); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java index b35bc2435c45..208c5e9478fa 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/UrlJarFileFactory.java @@ -60,7 +60,7 @@ JarFile createJarFile(URL jarFileUrl, Consumer closeAction) throws IOEx private Runtime.Version getVersion(URL url) { // The standard JDK handler uses #runtime to indicate that the runtime version // should be used. This unfortunately doesn't work for us as - // jdk.internal.loaderURLClassPath only adds the runtime fragment when the URL + // jdk.internal.loader.URLClassPath only adds the runtime fragment when the URL // is using the internal JDK handler. We need to flip the default to use // the runtime version. See gh-38050 return "base".equals(url.getRef()) ? JarFile.baseVersion() : JarFile.runtimeVersion(); From 1514d6fd57c48f2ea1931c023ef9cdf2bf87b006 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 23 Nov 2023 12:11:48 +0000 Subject: [PATCH 0822/1215] Upgrade to Spring Framework 6.1.1 Closes gh-38451 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ff97de233636..bbcd41271608 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ junitJupiterVersion=5.10.1 kotlinVersion=1.9.20 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.28 -springFrameworkVersion=6.1.1-SNAPSHOT +springFrameworkVersion=6.1.1 tomcatVersion=10.1.16 kotlin.stdlib.default.dependency=false From f3d48e64a5f4a310e694d6e83987f6db17af7632 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 23 Nov 2023 12:12:57 +0000 Subject: [PATCH 0823/1215] Set LATEST_GA to true for Homebrew and SDKMan Closes gh-38512 --- ci/pipeline.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/pipeline.yml b/ci/pipeline.yml index 46bc9e777450..d661686d422c 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -589,7 +589,7 @@ jobs: <<: *sdkman-task-params RELEASE_TYPE: RELEASE BRANCH: ((branch)) - LATEST_GA: false + LATEST_GA: true - name: update-homebrew-tap serial: true plan: @@ -605,7 +605,7 @@ jobs: image: ci-image file: git-repo/ci/tasks/update-homebrew-tap.yml params: - LATEST_GA: false + LATEST_GA: true - put: homebrew-tap-repo params: repository: updated-homebrew-tap-repo From 3fae5516c97fd735f200a64200685ec1851b28e6 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Thu, 23 Nov 2023 14:05:52 +0000 Subject: [PATCH 0824/1215] Next development version (v3.2.1-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index bbcd41271608..1dfc138f24b1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.2.0-SNAPSHOT +version=3.2.1-SNAPSHOT org.gradle.caching=true org.gradle.parallel=true From 0856e10443c2ca4f977b0fb0dee457415e38fe54 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 23 Nov 2023 14:35:04 -0800 Subject: [PATCH 0825/1215] Fix IndexOutOfBoundsException exception from parseUrl with empty spec Update jar `Handler` code so that the `parseUrl` method can accept an empty `spec`. Prior to this commit, a `classLoader.getResource("")` call would result in a `null` result. This breaks a number of things including `ClassPathResource` and `PathMatchingResourcePatternResolver`. Fixes gh-38524 --- .../boot/loader/net/protocol/jar/Handler.java | 2 +- .../boot/loader/net/protocol/jar/HandlerTests.java | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/Handler.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/Handler.java index 2778beeccaf3..facc6fe6a5d8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/Handler.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/Handler.java @@ -89,7 +89,7 @@ private String extractRelativePath(URL url, String spec, int start, int limit) { private String extractContextPath(URL url, String spec, int start) { String contextPath = url.getPath(); - if (spec.charAt(start) == '/') { + if (spec.regionMatches(false, start, "/", 0, 1)) { int indexOfContextPathSeparator = indexOfSeparator(contextPath); if (indexOfContextPathSeparator == -1) { throw new IllegalStateException("malformed context url:%s: no !/".formatted(url)); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/HandlerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/HandlerTests.java index 8d695721158b..6e8254805229 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/HandlerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/HandlerTests.java @@ -142,6 +142,15 @@ void parseUrlWhenAnchorOnly() throws MalformedURLException { assertThat(url.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt#runtime"); } + @Test // gh-38524 + void parseUrlWhenSpecIsEmpty() throws MalformedURLException { + URL url = createJarUrl("nested:gh-38524.jar/!BOOT-INF/classes/!/"); + String spec = ""; + this.handler.parseURL(url, spec, 0, 0); + assertThat(url.toExternalForm()).isEqualTo("jar:nested:gh-38524.jar/!BOOT-INF/classes/!/"); + + } + @Test void hashCodeGeneratesHashCode() throws MalformedURLException { URL url = createJarUrl("file:example.jar!/entry.txt"); From 86c2f28cb483539f4f650fde2754cf2af088c756 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Mon, 27 Nov 2023 13:24:23 +0100 Subject: [PATCH 0826/1215] Prevent keep alive thread from blocking the AOT processing Instead of creating the thread directly in the constructor, the thread is now created when the context is refreshed and stopped when the context is closed. As AOT processing never refreshes the context, the thread is never started and can't block the AOT processing task. Closes gh-38531 --- .../boot/SpringApplication.java | 57 ++++++++++++------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index b542dc464962..ba60d32a9874 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -32,6 +32,7 @@ import java.util.Properties; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import org.apache.commons.logging.Log; @@ -67,7 +68,9 @@ import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; import org.springframework.context.annotation.ConfigurationClassPostProcessor; import org.springframework.context.aot.AotApplicationContextInitializer; +import org.springframework.context.event.ApplicationContextEvent; import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.GenericTypeResolver; @@ -417,9 +420,7 @@ private void prepareContext(DefaultBootstrapContext bootstrapContext, Configurab context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor()); } if (this.keepAlive) { - KeepAlive keepAlive = new KeepAlive(); - keepAlive.start(); - context.addApplicationListener(keepAlive); + context.addApplicationListener(new KeepAlive()); } context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context)); if (!AotDetector.useGeneratedArtifacts()) { @@ -1635,31 +1636,47 @@ public SpringApplicationRunListener getRunListener(SpringApplication springAppli } /** - * A non-daemon thread to keep the JVM alive. Reacts to {@link ContextClosedEvent} to - * stop itself when the application context is closed. + * Starts a non-daemon thread to keep the JVM alive on {@link ContextRefreshedEvent}. + * Stops the thread on {@link ContextClosedEvent}. */ - private static final class KeepAlive extends Thread implements ApplicationListener { + private static final class KeepAlive implements ApplicationListener { - KeepAlive() { - setName("keep-alive"); - setDaemon(false); - } + private final AtomicReference thread = new AtomicReference<>(); @Override - public void onApplicationEvent(ContextClosedEvent event) { - interrupt(); + public void onApplicationEvent(ApplicationContextEvent event) { + if (event instanceof ContextRefreshedEvent) { + startKeepAliveThread(); + } + else if (event instanceof ContextClosedEvent) { + stopKeepAliveThread(); + } } - @Override - public void run() { - while (true) { - try { - Thread.sleep(Long.MAX_VALUE); - } - catch (InterruptedException ex) { - break; + private void startKeepAliveThread() { + Thread thread = new Thread(() -> { + while (true) { + try { + Thread.sleep(Long.MAX_VALUE); + } + catch (InterruptedException ex) { + break; + } } + }); + if (this.thread.compareAndSet(null, thread)) { + thread.setDaemon(false); + thread.setName("keep-alive"); + thread.start(); + } + } + + private void stopKeepAliveThread() { + Thread thread = this.thread.getAndSet(null); + if (thread == null) { + return; } + thread.interrupt(); } } From 8c7e8778a64d254f591430ad9c2ecc9f29c1f2ab Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 27 Nov 2023 23:33:15 -0800 Subject: [PATCH 0827/1215] Fix NegativeArraySizeException caused by missing unsigned conversion Update `ZipContent` so that `eocd.totalNumberOfCentralDirectoryEntries` is converted from a short to an unsigned int to prevent a negative number from being used. This commit also updates the code to consistently use `X.toUnsigned...` helper methods rather than using bitwise operators. Fixed gh-38572 --- .../boot/loader/zip/VirtualZipDataBlock.java | 12 ++++++------ .../zip/ZipCentralDirectoryFileHeaderRecord.java | 14 +++++++------- .../boot/loader/zip/ZipContent.java | 11 +++++++---- .../springframework/boot/loader/zip/ZipString.java | 4 ++-- 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualZipDataBlock.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualZipDataBlock.java index 6ba095c74193..7b2541f4e072 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualZipDataBlock.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualZipDataBlock.java @@ -53,8 +53,8 @@ class VirtualZipDataBlock extends VirtualDataBlock implements CloseableDataBlock long centralRecordPos = centralRecordPositions[i]; DataBlock name = new DataPart( centralRecordPos + ZipCentralDirectoryFileHeaderRecord.FILE_NAME_OFFSET + nameOffset, - (centralRecord.fileNameLength() & 0xFFFF) - nameOffset); - long localRecordPos = centralRecord.offsetToLocalHeader() & 0xFFFFFFFF; + Short.toUnsignedLong(centralRecord.fileNameLength()) - nameOffset); + long localRecordPos = Integer.toUnsignedLong(centralRecord.offsetToLocalHeader()); ZipLocalFileHeaderRecord localRecord = ZipLocalFileHeaderRecord.load(this.data, localRecordPos); DataBlock content = new DataPart(localRecordPos + localRecord.size(), centralRecord.compressedSize()); boolean hasDescriptorRecord = ZipDataDescriptorRecord.isPresentBasedOnFlag(centralRecord); @@ -74,8 +74,8 @@ private long addToCentral(List parts, ZipCentralDirectoryFileHeaderRe long originalRecordPos, DataBlock name, int offsetToLocalHeader) throws IOException { ZipCentralDirectoryFileHeaderRecord record = originalRecord.withFileNameLength((short) (name.size() & 0xFFFF)) .withOffsetToLocalHeader(offsetToLocalHeader); - int originalExtraFieldLength = originalRecord.extraFieldLength() & 0xFFFF; - int originalFileCommentLength = originalRecord.fileCommentLength() & 0xFFFF; + int originalExtraFieldLength = Short.toUnsignedInt(originalRecord.extraFieldLength()); + int originalFileCommentLength = Short.toUnsignedInt(originalRecord.fileCommentLength()); DataBlock extraFieldAndComment = new DataPart( originalRecordPos + originalRecord.size() - originalExtraFieldLength - originalFileCommentLength, originalExtraFieldLength + originalFileCommentLength); @@ -89,8 +89,8 @@ private long addToLocal(List parts, ZipCentralDirectoryFileHeaderReco ZipLocalFileHeaderRecord originalRecord, ZipDataDescriptorRecord dataDescriptorRecord, DataBlock name, DataBlock content) throws IOException { ZipLocalFileHeaderRecord record = originalRecord.withFileNameLength((short) (name.size() & 0xFFFF)); - long originalRecordPos = centralRecord.offsetToLocalHeader() & 0xFFFFFFFF; - int extraFieldLength = originalRecord.extraFieldLength() & 0xFFFF; + long originalRecordPos = Integer.toUnsignedLong(centralRecord.offsetToLocalHeader()); + int extraFieldLength = Short.toUnsignedInt(originalRecord.extraFieldLength()); parts.add(new ByteArrayDataBlock(record.asByteArray())); parts.add(name); parts.add(new DataPart(originalRecordPos + originalRecord.size() - extraFieldLength, extraFieldLength)); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipCentralDirectoryFileHeaderRecord.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipCentralDirectoryFileHeaderRecord.java index 27f03587ae84..a9ca0a54ad85 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipCentralDirectoryFileHeaderRecord.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipCentralDirectoryFileHeaderRecord.java @@ -83,14 +83,14 @@ long size() { * @throws IOException on I/O error */ void copyTo(DataBlock dataBlock, long pos, ZipEntry zipEntry) throws IOException { - int fileNameLength = fileNameLength() & 0xFFFF; - int extraLength = extraFieldLength() & 0xFFFF; - int commentLength = fileCommentLength() & 0xFFFF; - zipEntry.setMethod(compressionMethod() & 0xFFFF); + int fileNameLength = Short.toUnsignedInt(fileNameLength()); + int extraLength = Short.toUnsignedInt(extraFieldLength()); + int commentLength = Short.toUnsignedInt(fileCommentLength()); + zipEntry.setMethod(Short.toUnsignedInt(compressionMethod())); zipEntry.setTime(decodeMsDosFormatDateTime(lastModFileDate(), lastModFileTime())); - zipEntry.setCrc(crc32() & 0xFFFFFFFFL); - zipEntry.setCompressedSize(compressedSize() & 0xFFFFFFFFL); - zipEntry.setSize(uncompressedSize() & 0xFFFFFFFFL); + zipEntry.setCrc(Integer.toUnsignedLong(crc32())); + zipEntry.setCompressedSize(Integer.toUnsignedLong(compressedSize())); + zipEntry.setSize(Integer.toUnsignedLong(uncompressedSize())); if (extraLength > 0) { long extraPos = pos + MINIMUM_SIZE + fileNameLength; ByteBuffer buffer = ByteBuffer.allocate(extraLength); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java index cc980b915b3b..f63a17b9e859 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java @@ -556,13 +556,16 @@ private static ZipContent loadContent(Source source, FileChannelDataBlock data) Zip64EndOfCentralDirectoryRecord zip64Eocd = Zip64EndOfCentralDirectoryRecord.load(data, zip64Locator); data = data.slice(getStartOfZipContent(data, eocd, zip64Eocd)); long centralDirectoryPos = (zip64Eocd != null) ? zip64Eocd.offsetToStartOfCentralDirectory() - : eocd.offsetToStartOfCentralDirectory(); + : Integer.toUnsignedLong(eocd.offsetToStartOfCentralDirectory()); long numberOfEntries = (zip64Eocd != null) ? zip64Eocd.totalNumberOfCentralDirectoryEntries() - : eocd.totalNumberOfCentralDirectoryEntries(); - if (numberOfEntries > 0xFFFFFFFFL) { + : Short.toUnsignedInt(eocd.totalNumberOfCentralDirectoryEntries()); + if (numberOfEntries < 0) { + throw new IllegalStateException("Invalid number of zip entries in " + source); + } + if (numberOfEntries > Integer.MAX_VALUE) { throw new IllegalStateException("Too many zip entries in " + source); } - Loader loader = new Loader(source, null, data, centralDirectoryPos, (int) (numberOfEntries & 0xFFFFFFFFL)); + Loader loader = new Loader(source, null, data, centralDirectoryPos, (int) numberOfEntries); ByteBuffer signatureNameSuffixBuffer = ByteBuffer.allocate(SIGNATURE_SUFFIX.length); boolean hasJarSignatureFile = false; long pos = centralDirectoryPos; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipString.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipString.java index bf246e0c7d60..6ffc4d7d68eb 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipString.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipString.java @@ -286,7 +286,7 @@ private static int readInBuffer(DataBlock dataBlock, long pos, ByteBuffer buffer } private static int getCodePointSize(byte[] bytes, int i) { - int b = bytes[i] & 0xFF; + int b = Byte.toUnsignedInt(bytes[i]); if ((b & 0b1_0000000) == 0b0_0000000) { return 1; } @@ -300,7 +300,7 @@ private static int getCodePointSize(byte[] bytes, int i) { } private static int getCodePoint(byte[] bytes, int i, int codePointSize) { - int codePoint = bytes[i] & 0xFF; + int codePoint = Byte.toUnsignedInt(bytes[i]); codePoint &= INITIAL_BYTE_BITMASK[codePointSize - 1]; for (int j = 1; j < codePointSize; j++) { codePoint = (codePoint << 6) + (bytes[i + j] & SUBSEQUENT_BYTE_BITMASK); From 34018b1982118aa66800b92b27b4042bd68433bd Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 28 Nov 2023 11:13:59 +0100 Subject: [PATCH 0828/1215] Reinstate testAndDevelopmentOnly in Testcontainers documentation Closes gh-38571 --- .../src/docs/asciidoc/features/testcontainers.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testcontainers.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testcontainers.adoc index a7c6b751ee88..826db3cb7e31 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testcontainers.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testcontainers.adoc @@ -89,5 +89,5 @@ This is especially useful for Testcontainer `Container` beans, as they keep thei include::code:MyContainersConfiguration[] -WARNING: If you're using Gradle and want to use this feature, you need to change the configuration of the `spring-boot-devtools` dependency from `developmentOnly` to `testImplementation`. +WARNING: If you're using Gradle and want to use this feature, you need to change the configuration of the `spring-boot-devtools` dependency from `developmentOnly` to `testAndDevelopmentOnly`. With the default scope of `developmentOnly`, the `bootTestRun` task will not pick up changes in your code, as the devtools are not active. From 3e4e59a8f03bc5ca5d7cad90e0e58e8bcf6a277d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 28 Nov 2023 12:15:42 +0000 Subject: [PATCH 0829/1215] Restore compatibility with Liquibase 4.23 Closes gh-38522 --- .../liquibase/LiquibaseAutoConfiguration.java | 11 ++- .../liquibase/LiquibaseProperties.java | 70 ++++++++++++++++--- ...itional-spring-configuration-metadata.json | 8 +++ .../Liquibase423AutoConfigurationTests.java | 65 +++++++++++++++++ .../LiquibaseAutoConfigurationTests.java | 7 +- .../liquibase/LiquibasePropertiesTests.java | 52 ++++++++++++++ 6 files changed, 198 insertions(+), 15 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/Liquibase423AutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibasePropertiesTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java index 8beb5ffcae18..979053511255 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java @@ -18,6 +18,8 @@ import javax.sql.DataSource; +import liquibase.UpdateSummaryEnum; +import liquibase.UpdateSummaryOutputEnum; import liquibase.change.DatabaseChange; import liquibase.integration.spring.SpringLiquibase; @@ -113,8 +115,13 @@ public SpringLiquibase liquibase(ObjectProvider dataSource, liquibase.setRollbackFile(properties.getRollbackFile()); liquibase.setTestRollbackOnUpdate(properties.isTestRollbackOnUpdate()); liquibase.setTag(properties.getTag()); - liquibase.setShowSummary(properties.getShowSummary()); - liquibase.setShowSummaryOutput(properties.getShowSummaryOutput()); + if (properties.getShowSummary() != null) { + liquibase.setShowSummary(UpdateSummaryEnum.valueOf(properties.getShowSummary().name())); + } + if (properties.getShowSummaryOutput() != null) { + liquibase + .setShowSummaryOutput(UpdateSummaryOutputEnum.valueOf(properties.getShowSummaryOutput().name())); + } return liquibase; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java index fa92d5c5694f..7b32afdadfde 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java @@ -138,16 +138,14 @@ public class LiquibaseProperties { private String tag; /** - * Whether to print a summary of the update operation. Values can be 'off', 'summary' - * (default), 'verbose' + * Whether to print a summary of the update operation. */ - private UpdateSummaryEnum showSummary; + private ShowSummary showSummary; /** - * Where to print a summary of the update operation. Values can be 'log' (default), - * 'console', or 'all'. + * Where to print a summary of the update operation. */ - private UpdateSummaryOutputEnum showSummaryOutput; + private ShowSummaryOutput showSummaryOutput; public String getChangeLog() { return this.changeLog; @@ -302,20 +300,72 @@ public void setTag(String tag) { this.tag = tag; } - public UpdateSummaryEnum getShowSummary() { + public ShowSummary getShowSummary() { return this.showSummary; } - public void setShowSummary(UpdateSummaryEnum showSummary) { + public void setShowSummary(ShowSummary showSummary) { this.showSummary = showSummary; } - public UpdateSummaryOutputEnum getShowSummaryOutput() { + public ShowSummaryOutput getShowSummaryOutput() { return this.showSummaryOutput; } - public void setShowSummaryOutput(UpdateSummaryOutputEnum showSummaryOutput) { + public void setShowSummaryOutput(ShowSummaryOutput showSummaryOutput) { this.showSummaryOutput = showSummaryOutput; } + /** + * Enumeration of types of summary to show. Values are the same as those on + * {@link UpdateSummaryEnum}. To maximize backwards compatibility, the Liquibase enum + * is not used directly. + * + * @since 3.2.1 + */ + public enum ShowSummary { + + /** + * Do not show a summary. + */ + OFF, + + /** + * Show a summary. + */ + SUMMARY, + + /** + * Show a verbose summary. + */ + VERBOSE + + } + + /** + * Enumeration of destinations to which the summary should be output. Values are the + * same as those on {@link UpdateSummaryOutputEnum}. To maximize backwards + * compatibility, the Liquibase enum is not used directly. + * + * @since 3.2.1 + */ + public enum ShowSummaryOutput { + + /** + * Log the summary. + */ + LOG, + + /** + * Output the summary to the console. + */ + CONSOLE, + + /** + * Log the summary and output it to the console. + */ + ALL + + } + } 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 7d21dfd05e2d..9308b0862e9e 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 @@ -1922,6 +1922,14 @@ "level": "error" } }, + { + "name": "spring.liquibase.show-summary", + "defaultValue": "summary" + }, + { + "name": "spring.liquibase.show-summary-output", + "defaultValue": "log" + }, { "name": "spring.mail.test-connection", "description": "Whether to test that the mail server is available on startup.", diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/Liquibase423AutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/Liquibase423AutoConfigurationTests.java new file mode 100644 index 000000000000..850a1e66124d --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/Liquibase423AutoConfigurationTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2023 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.liquibase; + +import java.util.function.Consumer; + +import liquibase.integration.spring.SpringLiquibase; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.boot.testsupport.classpath.ClassPathOverrides; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LiquibaseAutoConfiguration} with Liquibase 4.23. + * + * @author Andy Wilkinson + */ +@ClassPathOverrides("org.liquibase:liquibase-core:4.23.1") +class Liquibase423AutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(LiquibaseAutoConfiguration.class)) + .withPropertyValues("spring.datasource.generate-unique-name=true"); + + @Test + void defaultSpringLiquibase() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .run(assertLiquibase((liquibase) -> { + assertThat(liquibase.getChangeLog()).isEqualTo("classpath:/db/changelog/db.changelog-master.yaml"); + assertThat(liquibase.getContexts()).isNull(); + assertThat(liquibase.getDefaultSchema()).isNull(); + assertThat(liquibase.isDropFirst()).isFalse(); + assertThat(liquibase.isClearCheckSums()).isFalse(); + })); + } + + private ContextConsumer assertLiquibase(Consumer consumer) { + return (context) -> { + assertThat(context).hasSingleBean(SpringLiquibase.class); + SpringLiquibase liquibase = context.getBean(SpringLiquibase.class); + consumer.accept(liquibase); + }; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java index 6a9490f5dd5b..ac15c4880e33 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java @@ -31,6 +31,7 @@ import com.zaxxer.hikari.HikariDataSource; import liquibase.UpdateSummaryEnum; import liquibase.UpdateSummaryOutputEnum; +import liquibase.command.core.helpers.ShowSummaryArgument; import liquibase.integration.spring.SpringLiquibase; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -119,9 +120,6 @@ void defaultSpringLiquibase() { assertThat(liquibase.getDefaultSchema()).isNull(); assertThat(liquibase.isDropFirst()).isFalse(); assertThat(liquibase.isClearCheckSums()).isFalse(); - UpdateSummaryOutputEnum showSummaryOutput = (UpdateSummaryOutputEnum) ReflectionTestUtils - .getField(liquibase, "showSummaryOutput"); - assertThat(showSummaryOutput).isEqualTo(UpdateSummaryOutputEnum.LOG); })); } @@ -225,6 +223,9 @@ void defaultValues() { assertThat(liquibase.isDropFirst()).isEqualTo(properties.isDropFirst()); assertThat(liquibase.isClearCheckSums()).isEqualTo(properties.isClearChecksums()); assertThat(liquibase.isTestRollbackOnUpdate()).isEqualTo(properties.isTestRollbackOnUpdate()); + assertThat(liquibase).extracting("showSummary").isNull(); + assertThat(ShowSummaryArgument.SHOW_SUMMARY.getDefaultValue()).isEqualTo(UpdateSummaryEnum.SUMMARY); + assertThat(liquibase).extracting("showSummaryOutput").isEqualTo(UpdateSummaryOutputEnum.LOG); })); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibasePropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibasePropertiesTests.java new file mode 100644 index 000000000000..57f6025f46a8 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibasePropertiesTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2023 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.liquibase; + +import java.util.List; +import java.util.stream.Stream; + +import liquibase.UpdateSummaryEnum; +import liquibase.UpdateSummaryOutputEnum; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties.ShowSummary; +import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties.ShowSummaryOutput; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LiquibaseProperties}. + * + * @author Andy Wilkinson + */ +public class LiquibasePropertiesTests { + + @Test + void valuesOfShowSummaryMatchValuesOfUpdateSummaryEnum() { + assertThat(namesOf(ShowSummary.values())).isEqualTo(namesOf(UpdateSummaryEnum.values())); + } + + @Test + void valuesOfShowSummaryOutputMatchValuesOfUpdateSummaryOutputEnum() { + assertThat(namesOf(ShowSummaryOutput.values())).isEqualTo(namesOf(UpdateSummaryOutputEnum.values())); + } + + private List namesOf(Enum[] input) { + return Stream.of(input).map(Enum::name).toList(); + } + +} From 75a89556599c7a9f9f84d1cbdff5058c8bb14aff Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 27 Nov 2023 12:18:15 +0000 Subject: [PATCH 0830/1215] Only start management context when parent has a web server Fixes gh-38554 --- .../server/ChildManagementContextInitializer.java | 4 ++++ .../ManagementContextAutoConfigurationTests.java | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ChildManagementContextInitializer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ChildManagementContextInitializer.java index 579e7d7126d5..e580d5dfcd1c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ChildManagementContextInitializer.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/server/ChildManagementContextInitializer.java @@ -33,6 +33,7 @@ import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.context.event.ApplicationFailedEvent; import org.springframework.boot.web.context.ConfigurableWebServerApplicationContext; +import org.springframework.boot.web.context.WebServerApplicationContext; import org.springframework.boot.web.context.WebServerGracefulShutdownLifecycle; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextInitializer; @@ -82,6 +83,9 @@ private ChildManagementContextInitializer(ManagementContextFactory managementCon @Override public void start() { + if (!(this.parentContext instanceof WebServerApplicationContext)) { + return; + } if (this.managementContext == null) { ConfigurableApplicationContext managementContext = createManagementContext(); registerBeans(managementContext); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementContextAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementContextAutoConfigurationTests.java index 52fca8b17ae8..a617750be9ed 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementContextAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/server/ManagementContextAutoConfigurationTests.java @@ -55,6 +55,18 @@ void childManagementContextShouldStartForEmbeddedServer(CapturedOutput output) { .run((context) -> assertThat(output).satisfies(numberOfOccurrences("Tomcat started on port", 2))); } + @Test + void childManagementContextShouldNotStartWithoutEmbeddedServer(CapturedOutput output) { + WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class, + ServletWebServerFactoryAutoConfiguration.class, ServletManagementContextAutoConfiguration.class, + WebEndpointAutoConfiguration.class, EndpointAutoConfiguration.class)); + contextRunner.withPropertyValues("server.port=0", "management.server.port=0").run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(output).doesNotContain("Tomcat started"); + }); + } + @Test void childManagementContextShouldRestartWhenParentIsStoppedThenStarted(CapturedOutput output) { WebApplicationContextRunner contextRunner = new WebApplicationContextRunner( From 9a0f95420a85989a9bddb5c011c16094d42f0148 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 28 Nov 2023 14:16:35 -0800 Subject: [PATCH 0831/1215] Update NestedByteChannel.read to read all possible data when Update `NestedByteChannel.read` so that it loops until all remaining data has been read into the buffer. Prior to this commit, it was possible for to read only some bytes into the buffer. Although it looks like this should be OK according to the API documentation, the `ZipFileSystem` relies on all remaining bytes being returned. Fixes gh-38595 --- .../loader/nio/file/NestedByteChannel.java | 11 ++++-- .../nio/file/NestedByteChannelTests.java | 36 +++++++++++++++++++ ...ileChannelDataBlockManagedFileChannel.java | 33 +++++++++++++++++ 3 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockManagedFileChannel.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedByteChannel.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedByteChannel.java index e41b27fd655d..3e51924ed350 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedByteChannel.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedByteChannel.java @@ -81,11 +81,16 @@ public void close() throws IOException { @Override public int read(ByteBuffer dst) throws IOException { assertNotClosed(); - int count = this.resources.getData().read(dst, this.position); - if (count > 0) { + int total = 0; + while (dst.remaining() > 0) { + int count = this.resources.getData().read(dst, this.position); + if (count <= 0) { + return (total != 0) ? 0 : count; + } + total += count; this.position += count; } - return count; + return total; } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedByteChannelTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedByteChannelTests.java index d9ff2e83a1ef..7fef82d35198 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedByteChannelTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedByteChannelTests.java @@ -17,11 +17,16 @@ package org.springframework.boot.loader.nio.file; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.lang.ref.Cleaner.Cleanable; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.NonWritableChannelException; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -32,6 +37,7 @@ import org.springframework.boot.loader.ref.Cleaner; import org.springframework.boot.loader.testsupport.TestJar; import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed; +import org.springframework.boot.loader.zip.FileChannelDataBlockManagedFileChannel; import org.springframework.boot.loader.zip.ZipContent; import static org.assertj.core.api.Assertions.assertThat; @@ -117,6 +123,36 @@ void readReadsBytesAndIncrementsPosition() throws IOException { assertThat(dst.array()).isNotEqualTo(ByteBuffer.allocate(10).array()); } + @Test // gh-38592 + void readReadsAsManyBytesAsPossible() throws Exception { + // ZipFileSystem checks that the number of bytes read matches an expected value + // ...if (readFullyAt(cen, 0, cen.length, cenpos) != end.cenlen + ENDHDR) + // but the readFullyAt assumes that all remaining bytes are attempted to be read + // This doesn't seem to exactly match the contract of ReadableByteChannel.read + // which states "A read operation might not fill the buffer, and in fact it might + // not read any bytes at all", but we need to match ZipFileSystem's expectations + int size = FileChannelDataBlockManagedFileChannel.BUFFER_SIZE * 2; + byte[] data = new byte[size]; + this.file = new File(this.temp, "testread.jar"); + FileOutputStream fileOutputStream = new FileOutputStream(this.file); + try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) { + JarEntry nestedEntry = new JarEntry("data"); + nestedEntry.setSize(size); + nestedEntry.setCompressedSize(size); + CRC32 crc32 = new CRC32(); + crc32.update(data); + nestedEntry.setCrc(crc32.getValue()); + nestedEntry.setMethod(ZipEntry.STORED); + jarOutputStream.putNextEntry(nestedEntry); + jarOutputStream.write(data); + jarOutputStream.closeEntry(); + } + this.channel = new NestedByteChannel(this.file.toPath(), null); + ByteBuffer buffer = ByteBuffer.allocate((int) this.file.length()); + assertThat(this.channel.read(buffer)).isEqualTo(buffer.capacity()); + assertThat(this.file).binaryContent().isEqualTo(buffer.array()); + } + @Test void writeThrowsException() { assertThatExceptionOfType(NonWritableChannelException.class) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockManagedFileChannel.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockManagedFileChannel.java new file mode 100644 index 000000000000..6e945dbd43e8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockManagedFileChannel.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2023 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.loader.zip; + +import org.springframework.boot.loader.zip.FileChannelDataBlock.ManagedFileChannel; + +/** + * Test access to {@link ManagedFileChannel} details. + * + * @author Phillip Webb + */ +public final class FileChannelDataBlockManagedFileChannel { + + private FileChannelDataBlockManagedFileChannel() { + } + + public static int BUFFER_SIZE = FileChannelDataBlock.ManagedFileChannel.BUFFER_SIZE; + +} From 6fd691af589f1b85a492e3d6660dac5d15ba18c6 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 28 Nov 2023 20:49:46 -0800 Subject: [PATCH 0832/1215] Allow FileSystems to be create by splitting URLs Relax the constraint that a `NestedLocation` must have a nested entry name specified so that URLs can be split and rebuilt. Prior to this commit, given a URL of the following form: jar:nested:/myjar.jar!/nested.jar!/my/file It was possible to create a FileSystem from "jar:nested:/myjar.jar!/nested.jar" and from that create a path to "my/file". However, it wasn't possible to create a FileSystem from "jar:nested:/myjar.jar", then create another file system from the path "nested.jar" and then finally create a path to "/nested.jar". This was because `nested:/myjar.jar` was not considered a value URL because it didn't include a nested entry name. Projects such as `JobRunr` were relying on the ability to compose file systems, so it makes sense to remove our somewhat artificial restriction. Fixes gh-38592 --- .../net/protocol/nested/NestedLocation.java | 18 ++++------ .../loader/nio/file/NestedFileSystem.java | 2 +- .../boot/loader/nio/file/NestedPath.java | 19 +++++++---- .../net/protocol/nested/HandlerTests.java | 6 ---- .../protocol/nested/NestedLocationTests.java | 33 +++++++++++-------- .../nested/NestedUrlConnectionTests.java | 9 ----- .../nio/file/NestedFileSystemTests.java | 10 +++--- ...leSystemZipFileSystemIntegrationTests.java | 19 +++++++++++ 8 files changed, 64 insertions(+), 52 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java index c350a34fdbf7..589ef5972831 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java @@ -26,8 +26,8 @@ import org.springframework.boot.loader.net.util.UrlDecoder; /** - * A location obtained from a {@code nested:} {@link URL} consisting of a jar file and a - * nested entry. + * A location obtained from a {@code nested:} {@link URL} consisting of a jar file and an + * optional nested entry. *

    * The syntax of a nested JAR URL is:

      * nestedjar:<path>/!{entry}
    @@ -54,13 +54,12 @@ public record NestedLocation(Path path, String nestedEntryName) {
     
     	private static final Map cache = new ConcurrentHashMap<>();
     
    -	public NestedLocation {
    +	public NestedLocation(Path path, String nestedEntryName) {
     		if (path == null) {
     			throw new IllegalArgumentException("'path' must not be null");
     		}
    -		if (nestedEntryName == null || nestedEntryName.trim().isEmpty()) {
    -			throw new IllegalArgumentException("'nestedEntryName' must not be empty");
    -		}
    +		this.path = path;
    +		this.nestedEntryName = (nestedEntryName != null && !nestedEntryName.isEmpty()) ? nestedEntryName : null;
     	}
     
     	/**
    @@ -94,20 +93,17 @@ static NestedLocation parse(String path) {
     			throw new IllegalArgumentException("'path' must not be empty");
     		}
     		int index = path.lastIndexOf("/!");
    -		if (index == -1) {
    -			throw new IllegalArgumentException("'path' must contain '/!'");
    -		}
     		return cache.computeIfAbsent(path, (l) -> create(index, l));
     	}
     
     	private static NestedLocation create(int index, String location) {
    -		String locationPath = location.substring(0, index);
    +		String locationPath = (index != -1) ? location.substring(0, index) : location;
     		if (isWindows()) {
     			while (locationPath.startsWith("/")) {
     				locationPath = locationPath.substring(1, locationPath.length());
     			}
     		}
    -		String nestedEntryName = location.substring(index + 2);
    +		String nestedEntryName = (index != -1) ? location.substring(index + 2) : null;
     		return new NestedLocation((!locationPath.isEmpty()) ? Path.of(locationPath) : null, nestedEntryName);
     	}
     
    diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystem.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystem.java
    index be38b10cd020..1b4e94871132 100644
    --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystem.java
    +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystem.java
    @@ -177,7 +177,7 @@ public Set supportedFileAttributeViews() {
     	@Override
     	public Path getPath(String first, String... more) {
     		assertNotClosed();
    -		if (first == null || first.isBlank() || more.length != 0) {
    +		if (more.length != 0) {
     			throw new IllegalArgumentException("Nested paths must contain a single element");
     		}
     		return new NestedPath(this, first);
    diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedPath.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedPath.java
    index 163af41784a9..c544a8048310 100644
    --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedPath.java
    +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedPath.java
    @@ -49,11 +49,11 @@ final class NestedPath implements Path {
     	private volatile Boolean entryExists;
     
     	NestedPath(NestedFileSystem fileSystem, String nestedEntryName) {
    -		if (fileSystem == null || nestedEntryName == null || nestedEntryName.isBlank()) {
    -			throw new IllegalArgumentException("'filesSystem' and 'nestedEntryName' are required");
    +		if (fileSystem == null) {
    +			throw new IllegalArgumentException("'filesSystem' must not be null");
     		}
     		this.fileSystem = fileSystem;
    -		this.nestedEntryName = nestedEntryName;
    +		this.nestedEntryName = (nestedEntryName != null && !nestedEntryName.isBlank()) ? nestedEntryName : null;
     	}
     
     	Path getJarPath() {
    @@ -138,8 +138,11 @@ public Path relativize(Path other) {
     	@Override
     	public URI toUri() {
     		try {
    -			String jarFilePath = this.fileSystem.getJarPath().toUri().getPath();
    -			return new URI("nested:" + jarFilePath + "/!" + this.nestedEntryName);
    +			String uri = "nested:" + this.fileSystem.getJarPath().toUri().getPath();
    +			if (this.nestedEntryName != null) {
    +				uri += "/!" + this.nestedEntryName;
    +			}
    +			return new URI(uri);
     		}
     		catch (URISyntaxException ex) {
     			throw new IOError(ex);
    @@ -187,7 +190,11 @@ public int hashCode() {
     
     	@Override
     	public String toString() {
    -		return this.fileSystem.getJarPath() + this.fileSystem.getSeparator() + this.nestedEntryName;
    +		String string = this.fileSystem.getJarPath().toString();
    +		if (this.nestedEntryName != null) {
    +			string += this.fileSystem.getSeparator() + this.nestedEntryName;
    +		}
    +		return string;
     	}
     
     	void assertExists() throws NoSuchFileException {
    diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/HandlerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/HandlerTests.java
    index b6d73394473b..d480c5de6192 100644
    --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/HandlerTests.java
    +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/HandlerTests.java
    @@ -62,12 +62,6 @@ void assertUrlIsNotMalformedWhenUrlIsNotNestedThrowsException() {
     			.withMessageContaining("must use 'nested'");
     	}
     
    -	@Test
    -	void assertUrlIsNotMalformedWhenUrlIsMalformedThrowsException() {
    -		assertThatIllegalArgumentException().isThrownBy(() -> Handler.assertUrlIsNotMalformed("nested:bad"))
    -			.withMessageContaining("'path' must contain '/!'");
    -	}
    -
     	@Test
     	void assertUrlIsNotMalformedWhenUrlIsValidDoesNotThrowException() {
     		String url = "nested:" + this.temp.getAbsolutePath() + "/!nested.jar";
    diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java
    index 55970dfb245a..c1a13b21b0cf 100644
    --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java
    +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java
    @@ -22,6 +22,7 @@
     import java.nio.file.Path;
     
     import org.junit.jupiter.api.BeforeAll;
    +import org.junit.jupiter.api.Disabled;
     import org.junit.jupiter.api.Test;
     import org.junit.jupiter.api.io.TempDir;
     
    @@ -52,15 +53,17 @@ void createWhenPathIsNullThrowsException() {
     	}
     
     	@Test
    -	void createWhenNestedEntryNameIsNullThrowsException() {
    -		assertThatIllegalArgumentException().isThrownBy(() -> new NestedLocation(Path.of("test.jar"), null))
    -			.withMessageContaining("'nestedEntryName' must not be empty");
    +	void createWhenNestedEntryNameIsNull() {
    +		NestedLocation location = new NestedLocation(Path.of("test.jar"), null);
    +		assertThat(location.path().toString()).contains("test.jar");
    +		assertThat(location.nestedEntryName()).isNull();
     	}
     
     	@Test
    -	void createWhenNestedEntryNameIsEmptyThrowsException() {
    -		assertThatIllegalArgumentException().isThrownBy(() -> new NestedLocation(Path.of("test.jar"), null))
    -			.withMessageContaining("'nestedEntryName' must not be empty");
    +	void createWhenNestedEntryNameIsEmpty() {
    +		NestedLocation location = new NestedLocation(Path.of("test.jar"), "");
    +		assertThat(location.path().toString()).contains("test.jar");
    +		assertThat(location.nestedEntryName()).isNull();
     	}
     
     	@Test
    @@ -82,10 +85,11 @@ void fromUrlWhenNoPathThrowsException() {
     	}
     
     	@Test
    -	void fromUrlWhenNoSeparatorThrowsException() {
    -		assertThatIllegalArgumentException()
    -			.isThrownBy(() -> NestedLocation.fromUrl(new URL("nested:test.jar!nested.jar")))
    -			.withMessageContaining("'path' must contain '/!'");
    +	void fromUrlWhenNoSeparator() throws Exception {
    +		File file = new File(this.temp, "test.jar");
    +		NestedLocation location = NestedLocation.fromUrl(new URL("nested:" + file.getAbsolutePath() + "/"));
    +		assertThat(location.path()).isEqualTo(file.toPath());
    +		assertThat(location.nestedEntryName()).isNull();
     	}
     
     	@Test
    @@ -110,10 +114,11 @@ void fromUriWhenNotNestedProtocolThrowsException() {
     	}
     
     	@Test
    -	void fromUriWhenNoSeparatorThrowsException() {
    -		assertThatIllegalArgumentException()
    -			.isThrownBy(() -> NestedLocation.fromUri(new URI("nested:test.jar!nested.jar")))
    -			.withMessageContaining("'path' must contain '/!'");
    +	@Disabled
    +	void fromUriWhenNoSeparator() throws Exception {
    +		NestedLocation location = NestedLocation.fromUri(new URI("nested:test.jar!nested.jar"));
    +		assertThat(location.path().toString()).contains("test.jar!nested.jar");
    +		assertThat(location.nestedEntryName()).isNull();
     	}
     
     	@Test
    diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionTests.java
    index c3a8d8aa566a..b194610486e2 100644
    --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionTests.java
    +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionTests.java
    @@ -21,7 +21,6 @@
     import java.io.IOException;
     import java.io.InputStream;
     import java.lang.ref.Cleaner.Cleanable;
    -import java.net.MalformedURLException;
     import java.net.URL;
     import java.net.URLConnection;
     import java.security.Permission;
    @@ -41,7 +40,6 @@
     import org.springframework.boot.loader.zip.ZipContent;
     
     import static org.assertj.core.api.Assertions.assertThat;
    -import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
     import static org.mockito.ArgumentMatchers.any;
     import static org.mockito.BDDMockito.given;
     import static org.mockito.BDDMockito.then;
    @@ -74,13 +72,6 @@ void setup() throws Exception {
     		this.url = new URL("nested:" + this.jarFile.getAbsolutePath() + "/!nested.jar");
     	}
     
    -	@Test
    -	void createWhenMalformedUrlThrowsException() throws Exception {
    -		URL url = new URL("nested:bad.jar");
    -		assertThatExceptionOfType(MalformedURLException.class).isThrownBy(() -> new NestedUrlConnection(url))
    -			.withMessage("'path' must contain '/!'");
    -	}
    -
     	@Test
     	void getContentLengthWhenContentLengthMoreThanMaxIntReturnsMinusOne() {
     		NestedUrlConnection connection = mock(NestedUrlConnection.class);
    diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemTests.java
    index d8b8825dc8b4..aa56b0b6e1e1 100644
    --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemTests.java
    +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemTests.java
    @@ -128,14 +128,14 @@ void getPathWhenClosedThrowsException() throws Exception {
     
     	@Test
     	void getPathWhenFirstIsNullThrowsException() {
    -		assertThatIllegalArgumentException().isThrownBy(() -> this.fileSystem.getPath(null))
    -			.withMessage("Nested paths must contain a single element");
    +		Path path = this.fileSystem.getPath(null);
    +		assertThat(path.toString()).endsWith("/test.jar");
     	}
     
     	@Test
    -	void getPathWhenFirstIsBlankThrowsException() {
    -		assertThatIllegalArgumentException().isThrownBy(() -> this.fileSystem.getPath(""))
    -			.withMessage("Nested paths must contain a single element");
    +	void getPathWhenFirstIsBlank() {
    +		Path path = this.fileSystem.getPath("");
    +		assertThat(path.toString()).endsWith("/test.jar");
     	}
     
     	@Test
    diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemZipFileSystemIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemZipFileSystemIntegrationTests.java
    index 28e26b7f8d48..9756fda1e5fa 100644
    --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemZipFileSystemIntegrationTests.java
    +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemZipFileSystemIntegrationTests.java
    @@ -23,6 +23,7 @@
     import java.nio.file.Files;
     import java.nio.file.Path;
     import java.util.Collections;
    +import java.util.List;
     
     import org.junit.jupiter.api.Test;
     import org.junit.jupiter.api.io.TempDir;
    @@ -74,4 +75,22 @@ void nestedZipWithoutNewFileSystem() throws Exception {
     		assertThat(Files.readAllBytes(path)).containsExactly(0x3);
     	}
     
    +	@Test // gh-38592
    +	void nestedZipSplitAndRestore() throws Exception {
    +		File file = new File(this.temp, "test.jar");
    +		TestJar.create(file);
    +		URI uri = JarUrl.create(file, "nested.jar", "3.dat").toURI();
    +		String[] components = uri.toString().split("!");
    +		System.out.println(List.of(components));
    +		try (FileSystem rootFs = FileSystems.newFileSystem(URI.create(components[0]), Collections.emptyMap())) {
    +			Path childPath = rootFs.getPath(components[1]);
    +			try (FileSystem childFs = FileSystems.newFileSystem(childPath)) {
    +				Path nestedRoot = childFs.getPath("/");
    +				assertThat(Files.list(nestedRoot)).hasSize(4);
    +				Path path = childFs.getPath(components[2]);
    +				assertThat(Files.readAllBytes(path)).containsExactly(0x3);
    +			}
    +		}
    +	}
    +
     }
    
    From 8de81cb06e7039e95989f6035565f810edeaba4b Mon Sep 17 00:00:00 2001
    From: Andy Wilkinson 
    Date: Wed, 29 Nov 2023 14:02:43 +0000
    Subject: [PATCH 0833/1215] Disable bind on init for all Tomcat connectors
    
    If a connector is bound on init, it won't be unbound when stop()
    is called. This leaves the connector running when it should have
    been stopped. We currently disable bind on init for the main
    connector but not for any additional connectors. This commit
    disables bind on it for all connectors unless it is been
    explicitly enabled through the bindOnInit property.
    
    Closes gh-38564
    
    Co-authored-by: Moritz Halbritter 
    ---
     .../TomcatReactiveWebServerFactory.java       |  2 --
     .../tomcat/TomcatServletWebServerFactory.java |  2 --
     .../web/embedded/tomcat/TomcatWebServer.java  | 24 +++++++++++++++++--
     .../TomcatServletWebServerFactoryTests.java   | 16 +++++++++++--
     4 files changed, 36 insertions(+), 8 deletions(-)
    
    diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java
    index ff6d8e729cc2..4981bb81e9f1 100644
    --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java
    +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java
    @@ -200,8 +200,6 @@ protected void customizeConnector(Connector connector) {
     		if (getUriEncoding() != null) {
     			connector.setURIEncoding(getUriEncoding().name());
     		}
    -		// Don't bind to the socket prematurely if ApplicationContext is slow to start
    -		connector.setProperty("bindOnInit", "false");
     		if (getHttp2() != null && getHttp2().isEnabled()) {
     			connector.addUpgradeProtocol(new Http2Protocol());
     		}
    diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java
    index 5c60b0c7a909..5faed28060ab 100644
    --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java
    +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java
    @@ -342,8 +342,6 @@ protected void customizeConnector(Connector connector) {
     		if (getUriEncoding() != null) {
     			connector.setURIEncoding(getUriEncoding().name());
     		}
    -		// Don't bind to the socket prematurely if ApplicationContext is slow to start
    -		connector.setProperty("bindOnInit", "false");
     		if (getHttp2() != null && getHttp2().isEnabled()) {
     			connector.addUpgradeProtocol(new Http2Protocol());
     		}
    diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java
    index 7d7ce965468e..6aa6a8cefbed 100644
    --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java
    +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java
    @@ -20,6 +20,7 @@
     import java.util.HashMap;
     import java.util.Map;
     import java.util.concurrent.atomic.AtomicInteger;
    +import java.util.function.BiConsumer;
     import java.util.stream.Collectors;
     
     import javax.naming.NamingException;
    @@ -119,6 +120,8 @@ private void initialize() throws WebServerException {
     					}
     				});
     
    +				disableBindOnInit();
    +
     				// Start the server to trigger initialization listeners
     				this.tomcat.start();
     
    @@ -162,12 +165,29 @@ private void addInstanceIdToEngineName() {
     	}
     
     	private void removeServiceConnectors() {
    -		for (Service service : this.tomcat.getServer().findServices()) {
    -			Connector[] connectors = service.findConnectors().clone();
    +		doWithConnectors((service, connectors) -> {
     			this.serviceConnectors.put(service, connectors);
     			for (Connector connector : connectors) {
     				service.removeConnector(connector);
     			}
    +		});
    +	}
    +
    +	private void disableBindOnInit() {
    +		doWithConnectors((service, connectors) -> {
    +			for (Connector connector : connectors) {
    +				Object bindOnInit = connector.getProperty("bindOnInit");
    +				if (bindOnInit == null) {
    +					connector.setProperty("bindOnInit", "false");
    +				}
    +			}
    +		});
    +	}
    +
    +	private void doWithConnectors(BiConsumer consumer) {
    +		for (Service service : this.tomcat.getServer().findServices()) {
    +			Connector[] connectors = service.findConnectors().clone();
    +			consumer.accept(service, connectors);
     		}
     	}
     
    diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java
    index 5dafd69bc45c..b9a9f58e5c7f 100644
    --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java
    +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java
    @@ -237,11 +237,23 @@ void tomcatProtocolHandlerCanBeCustomized() {
     	void tomcatAdditionalConnectors() {
     		TomcatServletWebServerFactory factory = getFactory();
     		Connector[] connectors = new Connector[4];
    -		Arrays.setAll(connectors, (i) -> new Connector());
    +		Arrays.setAll(connectors, (i) -> {
    +			Connector connector = new Connector();
    +			connector.setPort(0);
    +			return connector;
    +		});
     		factory.addAdditionalTomcatConnectors(connectors);
     		this.webServer = factory.getWebServer();
    -		Map connectorsByService = ((TomcatWebServer) this.webServer).getServiceConnectors();
    +		Map connectorsByService = new HashMap<>(
    +				((TomcatWebServer) this.webServer).getServiceConnectors());
     		assertThat(connectorsByService.values().iterator().next()).hasSize(connectors.length + 1);
    +		this.webServer.start();
    +		this.webServer.stop();
    +		connectorsByService.forEach((service, serviceConnectors) -> {
    +			for (Connector connector : serviceConnectors) {
    +				assertThat(connector.getProtocolHandler()).extracting("endpoint.serverSock").isNull();
    +			}
    +		});
     	}
     
     	@Test
    
    From e454470bf9c5b7360b390545b0d4f1508bc6b12c Mon Sep 17 00:00:00 2001
    From: Moritz Halbritter 
    Date: Thu, 30 Nov 2023 09:25:49 +0100
    Subject: [PATCH 0834/1215] Apply awaitTerminationPeriod to
     SimpleAsyncTaskExecutor
    
    Closes gh-38528
    ---
     .../task/TaskExecutorConfigurations.java      |  2 ++
     .../TaskExecutionAutoConfigurationTests.java  |  4 ++-
     .../task/SimpleAsyncTaskExecutorBuilder.java  | 33 ++++++++++++++-----
     .../SimpleAsyncTaskExecutorBuilderTests.java  |  7 ++++
     4 files changed, 37 insertions(+), 9 deletions(-)
    
    diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java
    index ccefd966e49f..3b29c2a0e5c0 100644
    --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java
    +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java
    @@ -177,6 +177,8 @@ private SimpleAsyncTaskExecutorBuilder builder() {
     			builder = builder.taskDecorator(this.taskDecorator.getIfUnique());
     			TaskExecutionProperties.Simple simple = this.properties.getSimple();
     			builder = builder.concurrencyLimit(simple.getConcurrencyLimit());
    +			Shutdown shutdown = this.properties.getShutdown();
    +			builder = builder.taskTerminationTimeout(shutdown.getAwaitTerminationPeriod());
     			return builder;
     		}
     
    diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java
    index 0ae673011165..9bf9412f1c41 100644
    --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java
    +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java
    @@ -112,10 +112,12 @@ void taskExecutorBuilderShouldApplyCustomSettings() {
     	void simpleAsyncTaskExecutorBuilderShouldReadProperties() {
     		this.contextRunner
     			.withPropertyValues("spring.task.execution.thread-name-prefix=mytest-",
    -					"spring.task.execution.simple.concurrency-limit=1")
    +					"spring.task.execution.simple.concurrency-limit=1",
    +					"spring.task.execution.shutdown.await-termination-period=30s")
     			.run(assertSimpleAsyncTaskExecutor((taskExecutor) -> {
     				assertThat(taskExecutor.getConcurrencyLimit()).isEqualTo(1);
     				assertThat(taskExecutor.getThreadNamePrefix()).isEqualTo("mytest-");
    +				assertThat(taskExecutor).hasFieldOrPropertyWithValue("taskTerminationTimeout", 30000L);
     			}));
     	}
     
    diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilder.java
    index b43c8b6d0b73..c6e70bcf7e55 100644
    --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilder.java
    +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilder.java
    @@ -16,6 +16,7 @@
     
     package org.springframework.boot.task;
     
    +import java.time.Duration;
     import java.util.Arrays;
     import java.util.Collections;
     import java.util.LinkedHashSet;
    @@ -54,17 +55,21 @@ public class SimpleAsyncTaskExecutorBuilder {
     
     	private final Set customizers;
     
    +	private final Duration taskTerminationTimeout;
    +
     	public SimpleAsyncTaskExecutorBuilder() {
    -		this(null, null, null, null, null);
    +		this(null, null, null, null, null, null);
     	}
     
     	private SimpleAsyncTaskExecutorBuilder(Boolean virtualThreads, String threadNamePrefix, Integer concurrencyLimit,
    -			TaskDecorator taskDecorator, Set customizers) {
    +			TaskDecorator taskDecorator, Set customizers,
    +			Duration taskTerminationTimeout) {
     		this.virtualThreads = virtualThreads;
     		this.threadNamePrefix = threadNamePrefix;
     		this.concurrencyLimit = concurrencyLimit;
     		this.taskDecorator = taskDecorator;
     		this.customizers = customizers;
    +		this.taskTerminationTimeout = taskTerminationTimeout;
     	}
     
     	/**
    @@ -74,7 +79,7 @@ private SimpleAsyncTaskExecutorBuilder(Boolean virtualThreads, String threadName
     	 */
     	public SimpleAsyncTaskExecutorBuilder threadNamePrefix(String threadNamePrefix) {
     		return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, threadNamePrefix, this.concurrencyLimit,
    -				this.taskDecorator, this.customizers);
    +				this.taskDecorator, this.customizers, this.taskTerminationTimeout);
     	}
     
     	/**
    @@ -84,7 +89,7 @@ public SimpleAsyncTaskExecutorBuilder threadNamePrefix(String threadNamePrefix)
     	 */
     	public SimpleAsyncTaskExecutorBuilder virtualThreads(Boolean virtualThreads) {
     		return new SimpleAsyncTaskExecutorBuilder(virtualThreads, this.threadNamePrefix, this.concurrencyLimit,
    -				this.taskDecorator, this.customizers);
    +				this.taskDecorator, this.customizers, this.taskTerminationTimeout);
     	}
     
     	/**
    @@ -94,7 +99,7 @@ public SimpleAsyncTaskExecutorBuilder virtualThreads(Boolean virtualThreads) {
     	 */
     	public SimpleAsyncTaskExecutorBuilder concurrencyLimit(Integer concurrencyLimit) {
     		return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, concurrencyLimit,
    -				this.taskDecorator, this.customizers);
    +				this.taskDecorator, this.customizers, this.taskTerminationTimeout);
     	}
     
     	/**
    @@ -104,7 +109,18 @@ public SimpleAsyncTaskExecutorBuilder concurrencyLimit(Integer concurrencyLimit)
     	 */
     	public SimpleAsyncTaskExecutorBuilder taskDecorator(TaskDecorator taskDecorator) {
     		return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, this.concurrencyLimit,
    -				taskDecorator, this.customizers);
    +				taskDecorator, this.customizers, this.taskTerminationTimeout);
    +	}
    +
    +	/**
    +	 * Set the task termination timeout.
    +	 * @param taskTerminationTimeout the task termination timeout
    +	 * @return a new builder instance
    +	 * @since 3.2.1
    +	 */
    +	public SimpleAsyncTaskExecutorBuilder taskTerminationTimeout(Duration taskTerminationTimeout) {
    +		return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, this.concurrencyLimit,
    +				this.taskDecorator, this.customizers, taskTerminationTimeout);
     	}
     
     	/**
    @@ -134,7 +150,7 @@ public SimpleAsyncTaskExecutorBuilder customizers(
     			Iterable customizers) {
     		Assert.notNull(customizers, "Customizers must not be null");
     		return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, this.concurrencyLimit,
    -				this.taskDecorator, append(null, customizers));
    +				this.taskDecorator, append(null, customizers), this.taskTerminationTimeout);
     	}
     
     	/**
    @@ -162,7 +178,7 @@ public SimpleAsyncTaskExecutorBuilder additionalCustomizers(
     			Iterable customizers) {
     		Assert.notNull(customizers, "Customizers must not be null");
     		return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, this.concurrencyLimit,
    -				this.taskDecorator, append(this.customizers, customizers));
    +				this.taskDecorator, append(this.customizers, customizers), this.taskTerminationTimeout);
     	}
     
     	/**
    @@ -203,6 +219,7 @@ public  T configure(T taskExecutor) {
     		map.from(this.threadNamePrefix).whenHasText().to(taskExecutor::setThreadNamePrefix);
     		map.from(this.concurrencyLimit).to(taskExecutor::setConcurrencyLimit);
     		map.from(this.taskDecorator).to(taskExecutor::setTaskDecorator);
    +		map.from(this.taskTerminationTimeout).as(Duration::toMillis).to(taskExecutor::setTaskTerminationTimeout);
     		if (!CollectionUtils.isEmpty(this.customizers)) {
     			this.customizers.forEach((customizer) -> customizer.customize(taskExecutor));
     		}
    diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilderTests.java
    index bd6f3607eb36..8ddf5feaf261 100644
    --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilderTests.java
    +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilderTests.java
    @@ -16,6 +16,7 @@
     
     package org.springframework.boot.task;
     
    +import java.time.Duration;
     import java.util.Collections;
     import java.util.Set;
     
    @@ -144,4 +145,10 @@ void additionalCustomizersShouldAddToExisting() {
     		then(customizer2).should().customize(executor);
     	}
     
    +	@Test
    +	void taskTerminationTimeoutShouldApply() {
    +		SimpleAsyncTaskExecutor executor = this.builder.taskTerminationTimeout(Duration.ofSeconds(1)).build();
    +		assertThat(executor).extracting("taskTerminationTimeout").isEqualTo(1000L);
    +	}
    +
     }
    
    From 6744cc2887b986a2cffabfa423ea463a8b6b44b0 Mon Sep 17 00:00:00 2001
    From: Moritz Halbritter 
    Date: Thu, 30 Nov 2023 10:25:08 +0100
    Subject: [PATCH 0835/1215] Apply awaitTerminationPeriod to
     SimpleAsyncTaskScheduler
    
    Closes gh-38530
    ---
     .../task/TaskSchedulingConfigurations.java    |  4 +++
     .../TaskSchedulingAutoConfigurationTests.java |  5 +++-
     .../task/SimpleAsyncTaskSchedulerBuilder.java | 30 ++++++++++++++-----
     .../SimpleAsyncTaskSchedulerBuilderTests.java |  7 +++++
     4 files changed, 38 insertions(+), 8 deletions(-)
    
    diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java
    index d05dc9a91ef7..59bee07b10c1 100644
    --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java
    +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java
    @@ -152,6 +152,10 @@ private SimpleAsyncTaskSchedulerBuilder builder() {
     			builder = builder.customizers(this.taskSchedulerCustomizers.orderedStream()::iterator);
     			TaskSchedulingProperties.Simple simple = this.properties.getSimple();
     			builder = builder.concurrencyLimit(simple.getConcurrencyLimit());
    +			TaskSchedulingProperties.Shutdown shutdown = this.properties.getShutdown();
    +			if (shutdown.isAwaitTermination()) {
    +				builder = builder.taskTerminationTimeout(shutdown.getAwaitTerminationPeriod());
    +			}
     			return builder;
     		}
     
    diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java
    index ca211b30b965..57ed26c15ca9 100644
    --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java
    +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskSchedulingAutoConfigurationTests.java
    @@ -119,13 +119,16 @@ void enableSchedulingWithNoTaskExecutorAutoConfiguresOne() {
     	void simpleAsyncTaskSchedulerBuilderShouldReadProperties() {
     		this.contextRunner
     			.withPropertyValues("spring.task.scheduling.simple.concurrency-limit=1",
    -					"spring.task.scheduling.thread-name-prefix=scheduling-test-")
    +					"spring.task.scheduling.thread-name-prefix=scheduling-test-",
    +					"spring.task.scheduling.shutdown.await-termination=true",
    +					"spring.task.scheduling.shutdown.await-termination-period=30s")
     			.withUserConfiguration(SchedulingConfiguration.class)
     			.run((context) -> {
     				assertThat(context).hasSingleBean(SimpleAsyncTaskSchedulerBuilder.class);
     				SimpleAsyncTaskSchedulerBuilder builder = context.getBean(SimpleAsyncTaskSchedulerBuilder.class);
     				assertThat(builder).hasFieldOrPropertyWithValue("threadNamePrefix", "scheduling-test-");
     				assertThat(builder).hasFieldOrPropertyWithValue("concurrencyLimit", 1);
    +				assertThat(builder).hasFieldOrPropertyWithValue("taskTerminationTimeout", Duration.ofSeconds(30));
     			});
     	}
     
    diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilder.java
    index e5dab60b1e80..4e2f4069bd8c 100644
    --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilder.java
    +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilder.java
    @@ -16,6 +16,7 @@
     
     package org.springframework.boot.task;
     
    +import java.time.Duration;
     import java.util.Arrays;
     import java.util.Collections;
     import java.util.LinkedHashSet;
    @@ -48,16 +49,19 @@ public class SimpleAsyncTaskSchedulerBuilder {
     
     	private final Set customizers;
     
    +	private final Duration taskTerminationTimeout;
    +
     	public SimpleAsyncTaskSchedulerBuilder() {
    -		this(null, null, null, null);
    +		this(null, null, null, null, null);
     	}
     
     	private SimpleAsyncTaskSchedulerBuilder(String threadNamePrefix, Integer concurrencyLimit, Boolean virtualThreads,
    -			Set taskSchedulerCustomizers) {
    +			Set taskSchedulerCustomizers, Duration taskTerminationTimeout) {
     		this.threadNamePrefix = threadNamePrefix;
     		this.concurrencyLimit = concurrencyLimit;
     		this.virtualThreads = virtualThreads;
     		this.customizers = taskSchedulerCustomizers;
    +		this.taskTerminationTimeout = taskTerminationTimeout;
     	}
     
     	/**
    @@ -67,7 +71,7 @@ private SimpleAsyncTaskSchedulerBuilder(String threadNamePrefix, Integer concurr
     	 */
     	public SimpleAsyncTaskSchedulerBuilder threadNamePrefix(String threadNamePrefix) {
     		return new SimpleAsyncTaskSchedulerBuilder(threadNamePrefix, this.concurrencyLimit, this.virtualThreads,
    -				this.customizers);
    +				this.customizers, this.taskTerminationTimeout);
     	}
     
     	/**
    @@ -77,7 +81,7 @@ public SimpleAsyncTaskSchedulerBuilder threadNamePrefix(String threadNamePrefix)
     	 */
     	public SimpleAsyncTaskSchedulerBuilder concurrencyLimit(Integer concurrencyLimit) {
     		return new SimpleAsyncTaskSchedulerBuilder(this.threadNamePrefix, concurrencyLimit, this.virtualThreads,
    -				this.customizers);
    +				this.customizers, this.taskTerminationTimeout);
     	}
     
     	/**
    @@ -87,7 +91,18 @@ public SimpleAsyncTaskSchedulerBuilder concurrencyLimit(Integer concurrencyLimit
     	 */
     	public SimpleAsyncTaskSchedulerBuilder virtualThreads(Boolean virtualThreads) {
     		return new SimpleAsyncTaskSchedulerBuilder(this.threadNamePrefix, this.concurrencyLimit, virtualThreads,
    -				this.customizers);
    +				this.customizers, this.taskTerminationTimeout);
    +	}
    +
    +	/**
    +	 * Set the task termination timeout.
    +	 * @param taskTerminationTimeout the task termination timeout
    +	 * @return a new builder instance
    +	 * @since 3.2.1
    +	 */
    +	public SimpleAsyncTaskSchedulerBuilder taskTerminationTimeout(Duration taskTerminationTimeout) {
    +		return new SimpleAsyncTaskSchedulerBuilder(this.threadNamePrefix, this.concurrencyLimit, this.virtualThreads,
    +				this.customizers, taskTerminationTimeout);
     	}
     
     	/**
    @@ -117,7 +132,7 @@ public SimpleAsyncTaskSchedulerBuilder customizers(
     			Iterable customizers) {
     		Assert.notNull(customizers, "Customizers must not be null");
     		return new SimpleAsyncTaskSchedulerBuilder(this.threadNamePrefix, this.concurrencyLimit, this.virtualThreads,
    -				append(null, customizers));
    +				append(null, customizers), this.taskTerminationTimeout);
     	}
     
     	/**
    @@ -145,7 +160,7 @@ public SimpleAsyncTaskSchedulerBuilder additionalCustomizers(
     			Iterable customizers) {
     		Assert.notNull(customizers, "Customizers must not be null");
     		return new SimpleAsyncTaskSchedulerBuilder(this.threadNamePrefix, this.concurrencyLimit, this.virtualThreads,
    -				append(this.customizers, customizers));
    +				append(this.customizers, customizers), this.taskTerminationTimeout);
     	}
     
     	/**
    @@ -171,6 +186,7 @@ public  T configure(T taskScheduler) {
     		map.from(this.threadNamePrefix).to(taskScheduler::setThreadNamePrefix);
     		map.from(this.concurrencyLimit).to(taskScheduler::setConcurrencyLimit);
     		map.from(this.virtualThreads).to(taskScheduler::setVirtualThreads);
    +		map.from(this.taskTerminationTimeout).as(Duration::toMillis).to(taskScheduler::setTaskTerminationTimeout);
     		if (!CollectionUtils.isEmpty(this.customizers)) {
     			this.customizers.forEach((customizer) -> customizer.customize(taskScheduler));
     		}
    diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilderTests.java
    index 9b4e7da12c88..9cb06c5f3213 100644
    --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilderTests.java
    +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskSchedulerBuilderTests.java
    @@ -16,6 +16,7 @@
     
     package org.springframework.boot.task;
     
    +import java.time.Duration;
     import java.util.Collections;
     import java.util.Set;
     
    @@ -127,4 +128,10 @@ void additionalCustomizersShouldAddToExisting() {
     		then(customizer2).should().customize(scheduler);
     	}
     
    +	@Test
    +	void taskTerminationTimeoutShouldApply() {
    +		SimpleAsyncTaskScheduler scheduler = this.builder.taskTerminationTimeout(Duration.ofSeconds(1)).build();
    +		assertThat(scheduler).extracting("taskTerminationTimeout").isEqualTo(1000L);
    +	}
    +
     }
    
    From fdbd65a2f5586da1789567140da31014b3a13a53 Mon Sep 17 00:00:00 2001
    From: Moritz Halbritter 
    Date: Thu, 30 Nov 2023 10:29:12 +0100
    Subject: [PATCH 0836/1215] Only apply awaitTerminationPeriod if
     awaitTermination is set
    
    See gh-38528
    ---
     .../task/TaskExecutorConfigurations.java              | 11 ++++++-----
     .../task/TaskExecutionAutoConfigurationTests.java     |  1 +
     2 files changed, 7 insertions(+), 5 deletions(-)
    
    diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java
    index 3b29c2a0e5c0..805d5b336ce5 100644
    --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java
    +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java
    @@ -21,7 +21,6 @@
     import org.springframework.beans.factory.ObjectProvider;
     import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
     import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading;
    -import org.springframework.boot.autoconfigure.task.TaskExecutionProperties.Shutdown;
     import org.springframework.boot.autoconfigure.thread.Threading;
     import org.springframework.boot.task.SimpleAsyncTaskExecutorBuilder;
     import org.springframework.boot.task.SimpleAsyncTaskExecutorCustomizer;
    @@ -92,7 +91,7 @@ TaskExecutorBuilder taskExecutorBuilder(TaskExecutionProperties properties,
     			builder = builder.maxPoolSize(pool.getMaxSize());
     			builder = builder.allowCoreThreadTimeOut(pool.isAllowCoreThreadTimeout());
     			builder = builder.keepAlive(pool.getKeepAlive());
    -			Shutdown shutdown = properties.getShutdown();
    +			TaskExecutionProperties.Shutdown shutdown = properties.getShutdown();
     			builder = builder.awaitTermination(shutdown.isAwaitTermination());
     			builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod());
     			builder = builder.threadNamePrefix(properties.getThreadNamePrefix());
    @@ -120,7 +119,7 @@ ThreadPoolTaskExecutorBuilder threadPoolTaskExecutorBuilder(TaskExecutionPropert
     			builder = builder.maxPoolSize(pool.getMaxSize());
     			builder = builder.allowCoreThreadTimeOut(pool.isAllowCoreThreadTimeout());
     			builder = builder.keepAlive(pool.getKeepAlive());
    -			Shutdown shutdown = properties.getShutdown();
    +			TaskExecutionProperties.Shutdown shutdown = properties.getShutdown();
     			builder = builder.awaitTermination(shutdown.isAwaitTermination());
     			builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod());
     			builder = builder.threadNamePrefix(properties.getThreadNamePrefix());
    @@ -177,8 +176,10 @@ private SimpleAsyncTaskExecutorBuilder builder() {
     			builder = builder.taskDecorator(this.taskDecorator.getIfUnique());
     			TaskExecutionProperties.Simple simple = this.properties.getSimple();
     			builder = builder.concurrencyLimit(simple.getConcurrencyLimit());
    -			Shutdown shutdown = this.properties.getShutdown();
    -			builder = builder.taskTerminationTimeout(shutdown.getAwaitTerminationPeriod());
    +			TaskExecutionProperties.Shutdown shutdown = this.properties.getShutdown();
    +			if (shutdown.isAwaitTermination()) {
    +				builder = builder.taskTerminationTimeout(shutdown.getAwaitTerminationPeriod());
    +			}
     			return builder;
     		}
     
    diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java
    index 9bf9412f1c41..88df814860ce 100644
    --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java
    +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java
    @@ -113,6 +113,7 @@ void simpleAsyncTaskExecutorBuilderShouldReadProperties() {
     		this.contextRunner
     			.withPropertyValues("spring.task.execution.thread-name-prefix=mytest-",
     					"spring.task.execution.simple.concurrency-limit=1",
    +					"spring.task.execution.shutdown.await-termination=true",
     					"spring.task.execution.shutdown.await-termination-period=30s")
     			.run(assertSimpleAsyncTaskExecutor((taskExecutor) -> {
     				assertThat(taskExecutor.getConcurrencyLimit()).isEqualTo(1);
    
    From d172b2206443434b8fed8b1146bb031ba953307c Mon Sep 17 00:00:00 2001
    From: Moritz Halbritter 
    Date: Thu, 30 Nov 2023 11:37:25 +0100
    Subject: [PATCH 0837/1215] Escape pipe symbol in properties changelog table
     cells
    
    Closes gh-38515
    ---
     .../changelog/ChangelogWriter.java                | 15 +++++++++++++--
     1 file changed, 13 insertions(+), 2 deletions(-)
    
    diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java
    index 033d5e20977e..92ffcfada218 100644
    --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java
    +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java
    @@ -46,6 +46,7 @@
      * @author Stephane Nicoll
      * @author Andy Wilkinson
      * @author Phillip Webb
    + * @author Moritz Halbritter
      */
     class ChangelogWriter implements AutoCloseable {
     
    @@ -198,8 +199,18 @@ private String monospace(String value) {
     		return (value != null) ? "`%s`".formatted(value) : null;
     	}
     
    -	private void writeCell(String format, Object... args) {
    -		write((format != null) ? "| %s%n".formatted(format) : "|%n", args);
    +	private void writeCell(String content) {
    +		if (content == null) {
    +			write("|%n");
    +		}
    +		else {
    +			String escaped = escapeForTableCell(content);
    +			write("| %s%n".formatted(escaped));
    +		}
    +	}
    +
    +	private String escapeForTableCell(String content) {
    +		return content.replace("|", "\\|");
     	}
     
     	private void write(String format, Object... args) {
    
    From 0321a8a05b30175383ad1337ef31c1d1817438d7 Mon Sep 17 00:00:00 2001
    From: Brian Clozel 
    Date: Fri, 1 Dec 2023 09:36:00 +0100
    Subject: [PATCH 0838/1215] Configure ObservationRegistry on JmsListener
    
    Prior to this commit, we set in gh-37388 the ObservationRegistry on the
    auto-configured JmsTemplate bean. This enables observations and context
    propagation when sending JMS messages.
    
    This commit applies the same to the `DefaultJmsListenerContainerFactory`
    and the `DefaultJmsListenerContainerFactoryConfigurer`, in order to
    enable observations on `@JmsListener` annotated methods.
    
    This commit also refactors the support implemented in gh-37388 to avoid
    relying on a bean post processor and instead set the observation
    registry directly in the main auto-configuration: while Micrometer core
    is an actuator-only dependency, Micrometer Observation API is a compile
    dependnecy for spring-jms itself and there is no need to separate
    concerns there.
    
    Fixes gh-38613
    ---
     ...sTemplateObservationAutoConfiguration.java | 72 -------------------
     .../observation/jms/package-info.java         | 20 ------
     ...ot.autoconfigure.AutoConfiguration.imports |  1 -
     ...lateObservationAutoConfigurationTests.java | 70 ------------------
     ...JmsListenerContainerFactoryConfigurer.java | 13 ++++
     .../jms/JmsAnnotationDrivenConfiguration.java |  8 ++-
     .../jms/JmsAutoConfiguration.java             |  8 ++-
     .../jms/JmsAutoConfigurationTests.java        | 23 ++++++
     .../src/docs/asciidoc/actuator/metrics.adoc   |  7 +-
     src/checkstyle/import-control.xml             |  1 +
     10 files changed, 54 insertions(+), 169 deletions(-)
     delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfiguration.java
     delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/jms/package-info.java
     delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfigurationTests.java
    
    diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfiguration.java
    deleted file mode 100644
    index 9629468e6051..000000000000
    --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfiguration.java
    +++ /dev/null
    @@ -1,72 +0,0 @@
    -/*
    - * Copyright 2012-2023 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.actuate.autoconfigure.observation.jms;
    -
    -import io.micrometer.jakarta9.instrument.jms.JmsPublishObservationContext;
    -import io.micrometer.observation.Observation;
    -import io.micrometer.observation.ObservationRegistry;
    -import jakarta.jms.Message;
    -
    -import org.springframework.beans.BeansException;
    -import org.springframework.beans.factory.ObjectProvider;
    -import org.springframework.beans.factory.config.BeanPostProcessor;
    -import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
    -import org.springframework.boot.autoconfigure.AutoConfiguration;
    -import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
    -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
    -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
    -import org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration;
    -import org.springframework.context.annotation.Bean;
    -import org.springframework.jms.core.JmsTemplate;
    -
    -/**
    - * {@link EnableAutoConfiguration Auto-configuration} for instrumenting
    - * {@link JmsTemplate} beans for Observability.
    - *
    - * @author Brian Clozel
    - * @since 3.2.0
    - */
    -@AutoConfiguration(after = { JmsAutoConfiguration.class, ObservationAutoConfiguration.class })
    -@ConditionalOnBean({ ObservationRegistry.class, JmsTemplate.class })
    -@ConditionalOnClass({ Observation.class, Message.class, JmsTemplate.class, JmsPublishObservationContext.class })
    -public class JmsTemplateObservationAutoConfiguration {
    -
    -	@Bean
    -	static JmsTemplateObservationPostProcessor jmsTemplateObservationPostProcessor(
    -			ObjectProvider observationRegistry) {
    -		return new JmsTemplateObservationPostProcessor(observationRegistry);
    -	}
    -
    -	static class JmsTemplateObservationPostProcessor implements BeanPostProcessor {
    -
    -		private final ObjectProvider observationRegistry;
    -
    -		JmsTemplateObservationPostProcessor(ObjectProvider observationRegistry) {
    -			this.observationRegistry = observationRegistry;
    -		}
    -
    -		@Override
    -		public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    -			if (bean instanceof JmsTemplate jmsTemplate) {
    -				this.observationRegistry.ifAvailable(jmsTemplate::setObservationRegistry);
    -			}
    -			return bean;
    -		}
    -
    -	}
    -
    -}
    diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/jms/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/jms/package-info.java
    deleted file mode 100644
    index 417a73aed67f..000000000000
    --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/jms/package-info.java
    +++ /dev/null
    @@ -1,20 +0,0 @@
    -/*
    - * Copyright 2012-2023 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.
    - */
    -
    -/**
    - * Auto-configuration for JMS observations.
    - */
    -package org.springframework.boot.actuate.autoconfigure.observation.jms;
    diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
    index b1e23fd863fe..f48e8e19f435 100644
    --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
    +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
    @@ -71,7 +71,6 @@ org.springframework.boot.actuate.autoconfigure.metrics.export.statsd.StatsdMetri
     org.springframework.boot.actuate.autoconfigure.metrics.export.wavefront.WavefrontMetricsExportAutoConfiguration
     org.springframework.boot.actuate.autoconfigure.observation.batch.BatchObservationAutoConfiguration
     org.springframework.boot.actuate.autoconfigure.observation.graphql.GraphQlObservationAutoConfiguration
    -org.springframework.boot.actuate.autoconfigure.observation.jms.JmsTemplateObservationAutoConfiguration
     org.springframework.boot.actuate.autoconfigure.metrics.integration.IntegrationMetricsAutoConfiguration
     org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration
     org.springframework.boot.actuate.autoconfigure.metrics.jersey.JerseyServerMetricsAutoConfiguration
    diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfigurationTests.java
    deleted file mode 100644
    index dd097676b2d5..000000000000
    --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/jms/JmsTemplateObservationAutoConfigurationTests.java
    +++ /dev/null
    @@ -1,70 +0,0 @@
    -/*
    - * Copyright 2012-2023 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.actuate.autoconfigure.observation.jms;
    -
    -import jakarta.jms.ConnectionFactory;
    -import org.junit.jupiter.api.Test;
    -
    -import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
    -import org.springframework.boot.autoconfigure.AutoConfigurations;
    -import org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration;
    -import org.springframework.boot.test.context.FilteredClassLoader;
    -import org.springframework.boot.test.context.runner.ApplicationContextRunner;
    -import org.springframework.context.annotation.Bean;
    -import org.springframework.jms.core.JmsTemplate;
    -
    -import static org.assertj.core.api.Assertions.assertThat;
    -import static org.mockito.Mockito.mock;
    -
    -/**
    - * Tests for {@link JmsTemplateObservationAutoConfiguration}.
    - *
    - * @author Brian Clozel
    - */
    -class JmsTemplateObservationAutoConfigurationTests {
    -
    -	ApplicationContextRunner contextRunner = new ApplicationContextRunner()
    -		.withConfiguration(AutoConfigurations.of(JmsAutoConfiguration.class, ObservationAutoConfiguration.class,
    -				JmsTemplateObservationAutoConfiguration.class))
    -		.withUserConfiguration(JmsConnectionConfiguration.class);
    -
    -	@Test
    -	void shouldConfigureObservationRegistryOnTemplate() {
    -		this.contextRunner.run((context) -> {
    -			JmsTemplate jmsTemplate = context.getBean(JmsTemplate.class);
    -			assertThat(jmsTemplate).extracting("observationRegistry").isNotNull();
    -		});
    -	}
    -
    -	@Test
    -	void shouldBackOffWhenMicrometerJakartaIsNotPresent() {
    -		this.contextRunner.withClassLoader(new FilteredClassLoader("io.micrometer.jakarta")).run((context) -> {
    -			JmsTemplate jmsTemplate = context.getBean(JmsTemplate.class);
    -			assertThat(jmsTemplate).extracting("observationRegistry").isNull();
    -		});
    -	}
    -
    -	static class JmsConnectionConfiguration {
    -
    -		@Bean
    -		ConnectionFactory connectionFactory() {
    -			return mock(ConnectionFactory.class);
    -		}
    -
    -	}
    -
    -}
    diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java
    index 2e5f1cc58a0f..26e511dbb9f0 100644
    --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java
    +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java
    @@ -18,6 +18,7 @@
     
     import java.time.Duration;
     
    +import io.micrometer.observation.ObservationRegistry;
     import jakarta.jms.ConnectionFactory;
     import jakarta.jms.ExceptionListener;
     
    @@ -49,6 +50,8 @@ public final class DefaultJmsListenerContainerFactoryConfigurer {
     
     	private JmsProperties jmsProperties;
     
    +	private ObservationRegistry observationRegistry;
    +
     	/**
     	 * Set the {@link DestinationResolver} to use or {@code null} if no destination
     	 * resolver should be associated with the factory by default.
    @@ -93,6 +96,15 @@ void setJmsProperties(JmsProperties jmsProperties) {
     		this.jmsProperties = jmsProperties;
     	}
     
    +	/**
    +	 * Set the {@link ObservationRegistry} to use.
    +	 * @param observationRegistry the {@link ObservationRegistry}
    +	 * @since 3.2.1
    +	 */
    +	public void setObservationRegistry(ObservationRegistry observationRegistry) {
    +		this.observationRegistry = observationRegistry;
    +	}
    +
     	/**
     	 * Configure the specified jms listener container factory. The factory can be further
     	 * tuned and default settings can be overridden.
    @@ -115,6 +127,7 @@ public void configure(DefaultJmsListenerContainerFactory factory, ConnectionFact
     		if (this.transactionManager == null && sessionProperties.getTransacted() == null) {
     			factory.setSessionTransacted(true);
     		}
    +		map.from(this.observationRegistry).to(factory::setObservationRegistry);
     		map.from(sessionProperties::getTransacted).to(factory::setSessionTransacted);
     		map.from(listenerProperties::isAutoStartup).to(factory::setAutoStartup);
     		map.from(listenerProperties::formatConcurrency).to(factory::setConcurrency);
    diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAnnotationDrivenConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAnnotationDrivenConfiguration.java
    index c503fa51842c..508863683403 100644
    --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAnnotationDrivenConfiguration.java
    +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAnnotationDrivenConfiguration.java
    @@ -16,6 +16,7 @@
     
     package org.springframework.boot.autoconfigure.jms;
     
    +import io.micrometer.observation.ObservationRegistry;
     import jakarta.jms.ConnectionFactory;
     import jakarta.jms.ExceptionListener;
     
    @@ -53,15 +54,19 @@ class JmsAnnotationDrivenConfiguration {
     
     	private final ObjectProvider exceptionListener;
     
    +	private final ObjectProvider observationRegistry;
    +
     	private final JmsProperties properties;
     
     	JmsAnnotationDrivenConfiguration(ObjectProvider destinationResolver,
     			ObjectProvider transactionManager, ObjectProvider messageConverter,
    -			ObjectProvider exceptionListener, JmsProperties properties) {
    +			ObjectProvider exceptionListener,
    +			ObjectProvider observationRegistry, JmsProperties properties) {
     		this.destinationResolver = destinationResolver;
     		this.transactionManager = transactionManager;
     		this.messageConverter = messageConverter;
     		this.exceptionListener = exceptionListener;
    +		this.observationRegistry = observationRegistry;
     		this.properties = properties;
     	}
     
    @@ -73,6 +78,7 @@ DefaultJmsListenerContainerFactoryConfigurer jmsListenerContainerFactoryConfigur
     		configurer.setTransactionManager(this.transactionManager.getIfUnique());
     		configurer.setMessageConverter(this.messageConverter.getIfUnique());
     		configurer.setExceptionListener(this.exceptionListener.getIfUnique());
    +		configurer.setObservationRegistry(this.observationRegistry.getIfUnique());
     		configurer.setJmsProperties(this.properties);
     		return configurer;
     	}
    diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java
    index 2c2ba4b5a0b9..44b049a657ed 100644
    --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java
    +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfiguration.java
    @@ -19,6 +19,7 @@
     import java.time.Duration;
     import java.util.List;
     
    +import io.micrometer.observation.ObservationRegistry;
     import jakarta.jms.ConnectionFactory;
     import jakarta.jms.Message;
     
    @@ -74,12 +75,16 @@ protected static class JmsTemplateConfiguration {
     
     		private final ObjectProvider messageConverter;
     
    +		private final ObjectProvider observationRegistry;
    +
     		public JmsTemplateConfiguration(JmsProperties properties,
     				ObjectProvider destinationResolver,
    -				ObjectProvider messageConverter) {
    +				ObjectProvider messageConverter,
    +				ObjectProvider observationRegistry) {
     			this.properties = properties;
     			this.destinationResolver = destinationResolver;
     			this.messageConverter = messageConverter;
    +			this.observationRegistry = observationRegistry;
     		}
     
     		@Bean
    @@ -91,6 +96,7 @@ public JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) {
     			template.setPubSubDomain(this.properties.isPubSubDomain());
     			map.from(this.destinationResolver::getIfUnique).whenNonNull().to(template::setDestinationResolver);
     			map.from(this.messageConverter::getIfUnique).whenNonNull().to(template::setMessageConverter);
    +			map.from(this.observationRegistry::getIfUnique).whenNonNull().to(template::setObservationRegistry);
     			mapTemplateProperties(this.properties.getTemplate(), template);
     			return template;
     		}
    diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java
    index b9b6025bae46..7ea7a0446e16 100644
    --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java
    +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java
    @@ -18,6 +18,7 @@
     
     import java.io.IOException;
     
    +import io.micrometer.observation.ObservationRegistry;
     import jakarta.jms.ConnectionFactory;
     import jakarta.jms.ExceptionListener;
     import jakarta.jms.Session;
    @@ -258,6 +259,17 @@ void testDefaultContainerFactoryWithExceptionListener() {
     			});
     	}
     
    +	@Test
    +	void testDefaultContainerFactoryWithObservationRegistry() {
    +		ObservationRegistry observationRegistry = mock(ObservationRegistry.class);
    +		this.contextRunner.withUserConfiguration(EnableJmsConfiguration.class)
    +			.withBean(ObservationRegistry.class, () -> observationRegistry)
    +			.run((context) -> {
    +				DefaultMessageListenerContainer container = getContainer(context, "jmsListenerContainerFactory");
    +				assertThat(container.getObservationRegistry()).isSameAs(observationRegistry);
    +			});
    +	}
    +
     	@Test
     	void testCustomContainerFactoryWithConfigurer() {
     		this.contextRunner.withUserConfiguration(TestConfiguration9.class, EnableJmsConfiguration.class)
    @@ -290,6 +302,17 @@ void testJmsTemplateWithDestinationResolver() {
     				.isSameAs(context.getBean("myDestinationResolver")));
     	}
     
    +	@Test
    +	void testJmsTemplateWithObservationRegistry() {
    +		ObservationRegistry observationRegistry = mock(ObservationRegistry.class);
    +		this.contextRunner.withUserConfiguration(EnableJmsConfiguration.class)
    +			.withBean(ObservationRegistry.class, () -> observationRegistry)
    +			.run((context) -> {
    +				JmsTemplate jmsTemplate = context.getBean(JmsTemplate.class);
    +				assertThat(jmsTemplate).extracting("observationRegistry").isSameAs(observationRegistry);
    +			});
    +	}
    +
     	@Test
     	void testJmsTemplateFullCustomization() {
     		this.contextRunner.withUserConfiguration(MessageConvertersConfiguration.class)
    diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc
    index ba8ebca1706b..7c654a9438d8 100644
    --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc
    +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc
    @@ -745,10 +745,9 @@ Metrics are tagged by the name of the executor, which is derived from the bean n
     
     [[actuator.metrics.supported.jms]]
     ==== JMS Metrics
    -Auto-configuration enables the instrumentation of all available `JmsTemplate` beans.
    -`JmsMessagingTemplate` instances built with instrumented `JmsTemplate` beans will also record observations.
    -See the {spring-framework-docs}/integration/observability.html#observability.jms.publish[Spring Framework reference documentation for more information on produced observations].
    -
    +Auto-configuration enables the instrumentation of all available `JmsTemplate` beans and `@JmsListener` annotated methods.
    +This will produce `"jms.message.publish"` and `"jms.message.process"` metrics respectively.
    +See the {spring-framework-docs}/integration/observability.html#observability.jms[Spring Framework reference documentation for more information on produced observations].
     
     
     [[actuator.metrics.supported.spring-mvc]]
    diff --git a/src/checkstyle/import-control.xml b/src/checkstyle/import-control.xml
    index 78d5bbabeab6..b3c993a4543f 100644
    --- a/src/checkstyle/import-control.xml
    +++ b/src/checkstyle/import-control.xml
    @@ -1,6 +1,7 @@
     
     
     
    +	
     	
     	
     	
    
    From 89a0ac3018ab811edfde406e6f605d4f9084ce3f Mon Sep 17 00:00:00 2001
    From: Moritz Halbritter 
    Date: Fri, 1 Dec 2023 14:47:24 +0100
    Subject: [PATCH 0839/1215] Reword documentation
    
    ---
     .../spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc    | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc
    index 50af4384f6a8..e4e9189b2462 100644
    --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc
    +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/tracing.adoc
    @@ -70,7 +70,7 @@ Press the "Show" button to see the details of that trace.
     [[actuator.micrometer-tracing.logging]]
     === Logging Correlation IDs
     Correlation IDs provide a helpful way to link lines in your log files to spans/traces.
    -When you are using Micrometer Tracing, Spring Boot will include correlation IDs in your logs.
    +If you are using Micrometer Tracing, Spring Boot will include correlation IDs in your logs by default.
     
     The default correlation ID is built from `traceId` and `spanId` https://logback.qos.ch/manual/mdc.html[MDC] values.
     For example, if Micrometer Tracing has added an MDC `traceId` of `803B448A0489F84084905D3093480352` and an MDC `spanId` of `3425F23BB2432450` the log output will include the correlation ID `[803B448A0489F84084905D3093480352-3425F23BB2432450]`.
    
    From 829bec7602496bb14a4b7f0936f2201e6909e2db Mon Sep 17 00:00:00 2001
    From: Arthur Gavlyukovskiy 
    Date: Fri, 1 Dec 2023 23:30:55 +0100
    Subject: [PATCH 0840/1215] Update documentation about jetty http2 dependency
    
    See gh-38632
    ---
     .../spring-boot-docs/src/docs/asciidoc/howto/webserver.adoc     | 2 +-
     .../boot/web/embedded/jetty/SslServerCustomizer.java            | 2 +-
     2 files changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/webserver.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/webserver.adoc
    index 4424f4bc9323..a8502afc7f19 100644
    --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/webserver.adoc
    +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/webserver.adoc
    @@ -221,7 +221,7 @@ More on this in the {tomcat-docs}/apr.html[official Tomcat documentation].
     
     [[howto.webserver.configure-http2.jetty]]
     ==== HTTP/2 With Jetty
    -For HTTP/2 support, Jetty requires the additional `org.eclipse.jetty.http2:http2-server` dependency.
    +For HTTP/2 support, Jetty requires the additional `org.eclipse.jetty.http2:jetty-http2-server` dependency.
     To use `h2c` no other dependencies are required.
     To use `h2`, you also need to choose one of the following dependencies, depending on your deployment:
     
    diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java
    index 329b6a73f2fb..f215f9d4419a 100644
    --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java
    +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/SslServerCustomizer.java
    @@ -96,7 +96,7 @@ private ServerConnector createServerConnector(Server server, SslContextFactory.S
     		Assert.state(isJettyAlpnPresent(),
     				() -> "An 'org.eclipse.jetty:jetty-alpn-*-server' dependency is required for HTTP/2 support.");
     		Assert.state(isJettyHttp2Present(),
    -				() -> "The 'org.eclipse.jetty.http2:http2-server' dependency is required for HTTP/2 support.");
    +				() -> "The 'org.eclipse.jetty.http2:jetty-http2-server' dependency is required for HTTP/2 support.");
     		return createHttp2ServerConnector(config, sslContextFactory, server);
     	}
     
    
    From 02347abefb6aab7f386b7cc629ff48e2a3437b5c Mon Sep 17 00:00:00 2001
    From: Moritz Halbritter 
    Date: Tue, 5 Dec 2023 10:58:04 +0100
    Subject: [PATCH 0841/1215] Disable propagation of traces if tracing is
     disabled
    
    Closes gh-38641
    ---
     .../tracing/BraveAutoConfiguration.java       | 125 +-----------
     .../BravePropagationConfigurations.java       | 178 ++++++++++++++++++
     .../tracing/CompositePropagationFactory.java  |   9 +
     .../OpenTelemetryAutoConfiguration.java       |  47 +----
     ...penTelemetryPropagationConfigurations.java | 105 +++++++++++
     .../tracing/BraveAutoConfigurationTests.java  |  10 +
     .../OpenTelemetryAutoConfigurationTests.java  |   9 +
     7 files changed, 320 insertions(+), 163 deletions(-)
     create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BravePropagationConfigurations.java
     create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryPropagationConfigurations.java
    
    diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfiguration.java
    index 47d65fe69cbc..9e1c8a6bd413 100644
    --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfiguration.java
    +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfiguration.java
    @@ -24,22 +24,10 @@
     import brave.Tracing;
     import brave.Tracing.Builder;
     import brave.TracingCustomizer;
    -import brave.baggage.BaggageField;
    -import brave.baggage.BaggagePropagation;
    -import brave.baggage.BaggagePropagation.FactoryBuilder;
    -import brave.baggage.BaggagePropagationConfig;
    -import brave.baggage.BaggagePropagationCustomizer;
    -import brave.baggage.CorrelationScopeConfig.SingleCorrelationField;
    -import brave.baggage.CorrelationScopeCustomizer;
    -import brave.baggage.CorrelationScopeDecorator;
    -import brave.context.slf4j.MDCScopeDecorator;
     import brave.handler.SpanHandler;
     import brave.propagation.CurrentTraceContext;
    -import brave.propagation.CurrentTraceContext.ScopeDecorator;
     import brave.propagation.CurrentTraceContextCustomizer;
    -import brave.propagation.Propagation;
     import brave.propagation.Propagation.Factory;
    -import brave.propagation.Propagation.KeyFactory;
     import brave.propagation.ThreadLocalCurrentTraceContext;
     import brave.sampler.Sampler;
     import io.micrometer.tracing.brave.bridge.BraveBaggageManager;
    @@ -53,17 +41,15 @@
     import io.micrometer.tracing.exporter.SpanReporter;
     
     import org.springframework.beans.factory.ObjectProvider;
    -import org.springframework.boot.actuate.autoconfigure.tracing.TracingProperties.Baggage.Correlation;
     import org.springframework.boot.actuate.autoconfigure.tracing.TracingProperties.Propagation.PropagationType;
     import org.springframework.boot.autoconfigure.AutoConfiguration;
     import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
     import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
     import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
     import org.springframework.boot.context.properties.EnableConfigurationProperties;
     import org.springframework.boot.context.properties.IncompatibleConfigurationException;
     import org.springframework.context.annotation.Bean;
    -import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.Import;
     import org.springframework.core.Ordered;
     import org.springframework.core.annotation.Order;
     import org.springframework.core.env.Environment;
    @@ -79,9 +65,12 @@
     @AutoConfiguration(before = MicrometerTracingAutoConfiguration.class)
     @ConditionalOnClass({ Tracer.class, BraveTracer.class })
     @EnableConfigurationProperties(TracingProperties.class)
    +@Import({ BravePropagationConfigurations.PropagationWithoutBaggage.class,
    +		BravePropagationConfigurations.PropagationWithBaggage.class,
    +		BravePropagationConfigurations.NoPropagation.class })
     public class BraveAutoConfiguration {
     
    -	private static final BraveBaggageManager BRAVE_BAGGAGE_MANAGER = new BraveBaggageManager();
    +	static final BraveBaggageManager BRAVE_BAGGAGE_MANAGER = new BraveBaggageManager();
     
     	/**
     	 * Default value for application name if {@code spring.application.name} is not set.
    @@ -182,108 +171,4 @@ BraveSpanCustomizer braveSpanCustomizer(SpanCustomizer spanCustomizer) {
     		return new BraveSpanCustomizer(spanCustomizer);
     	}
     
    -	@Configuration(proxyBeanMethods = false)
    -	@ConditionalOnProperty(value = "management.tracing.baggage.enabled", havingValue = "false")
    -	static class BraveNoBaggageConfiguration {
    -
    -		@Bean
    -		@ConditionalOnMissingBean
    -		Factory propagationFactory(TracingProperties properties) {
    -			return CompositePropagationFactory.create(properties.getPropagation());
    -		}
    -
    -	}
    -
    -	@Configuration(proxyBeanMethods = false)
    -	@ConditionalOnProperty(value = "management.tracing.baggage.enabled", matchIfMissing = true)
    -	static class BraveBaggageConfiguration {
    -
    -		private final TracingProperties tracingProperties;
    -
    -		BraveBaggageConfiguration(TracingProperties tracingProperties) {
    -			this.tracingProperties = tracingProperties;
    -		}
    -
    -		@Bean
    -		@ConditionalOnMissingBean
    -		BaggagePropagation.FactoryBuilder propagationFactoryBuilder(
    -				ObjectProvider baggagePropagationCustomizers) {
    -			// There's a chicken-and-egg problem here: to create a builder, we need a
    -			// factory. But the CompositePropagationFactory needs data from the builder.
    -			// We create a throw-away builder with a throw-away factory, and then copy the
    -			// config to the real builder.
    -			FactoryBuilder throwAwayBuilder = BaggagePropagation.newFactoryBuilder(createThrowAwayFactory());
    -			baggagePropagationCustomizers.orderedStream()
    -				.forEach((customizer) -> customizer.customize(throwAwayBuilder));
    -			CompositePropagationFactory propagationFactory = CompositePropagationFactory.create(
    -					this.tracingProperties.getPropagation(), BRAVE_BAGGAGE_MANAGER,
    -					LocalBaggageFields.extractFrom(throwAwayBuilder));
    -			FactoryBuilder builder = BaggagePropagation.newFactoryBuilder(propagationFactory);
    -			throwAwayBuilder.configs().forEach(builder::add);
    -			return builder;
    -		}
    -
    -		@SuppressWarnings("deprecation")
    -		private Factory createThrowAwayFactory() {
    -			return new Factory() {
    -
    -				@Override
    -				public  Propagation create(KeyFactory keyFactory) {
    -					return null;
    -				}
    -
    -			};
    -		}
    -
    -		@Bean
    -		@Order(0)
    -		BaggagePropagationCustomizer remoteFieldsBaggagePropagationCustomizer() {
    -			return (builder) -> {
    -				List remoteFields = this.tracingProperties.getBaggage().getRemoteFields();
    -				for (String fieldName : remoteFields) {
    -					builder.add(BaggagePropagationConfig.SingleBaggageField.remote(BaggageField.create(fieldName)));
    -				}
    -			};
    -		}
    -
    -		@Bean
    -		@ConditionalOnMissingBean
    -		Factory propagationFactory(BaggagePropagation.FactoryBuilder factoryBuilder) {
    -			return factoryBuilder.build();
    -		}
    -
    -		@Bean
    -		@ConditionalOnMissingBean
    -		CorrelationScopeDecorator.Builder mdcCorrelationScopeDecoratorBuilder(
    -				ObjectProvider correlationScopeCustomizers) {
    -			CorrelationScopeDecorator.Builder builder = MDCScopeDecorator.newBuilder();
    -			correlationScopeCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
    -			return builder;
    -		}
    -
    -		@Bean
    -		@Order(0)
    -		@ConditionalOnProperty(prefix = "management.tracing.baggage.correlation", name = "enabled",
    -				matchIfMissing = true)
    -		CorrelationScopeCustomizer correlationFieldsCorrelationScopeCustomizer() {
    -			return (builder) -> {
    -				Correlation correlationProperties = this.tracingProperties.getBaggage().getCorrelation();
    -				for (String field : correlationProperties.getFields()) {
    -					BaggageField baggageField = BaggageField.create(field);
    -					SingleCorrelationField correlationField = SingleCorrelationField.newBuilder(baggageField)
    -						.flushOnUpdate()
    -						.build();
    -					builder.add(correlationField);
    -				}
    -			};
    -		}
    -
    -		@Bean
    -		@ConditionalOnMissingBean(CorrelationScopeDecorator.class)
    -		ScopeDecorator correlationScopeDecorator(CorrelationScopeDecorator.Builder builder) {
    -			return builder.build();
    -		}
    -
    -	}
    -
     }
    diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BravePropagationConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BravePropagationConfigurations.java
    new file mode 100644
    index 000000000000..17bc93021945
    --- /dev/null
    +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BravePropagationConfigurations.java
    @@ -0,0 +1,178 @@
    +/*
    + * Copyright 2012-2023 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.actuate.autoconfigure.tracing;
    +
    +import java.util.List;
    +
    +import brave.baggage.BaggageField;
    +import brave.baggage.BaggagePropagation;
    +import brave.baggage.BaggagePropagation.FactoryBuilder;
    +import brave.baggage.BaggagePropagationConfig;
    +import brave.baggage.BaggagePropagationCustomizer;
    +import brave.baggage.CorrelationScopeConfig.SingleCorrelationField;
    +import brave.baggage.CorrelationScopeCustomizer;
    +import brave.baggage.CorrelationScopeDecorator;
    +import brave.context.slf4j.MDCScopeDecorator;
    +import brave.propagation.CurrentTraceContext.ScopeDecorator;
    +import brave.propagation.Propagation;
    +import brave.propagation.Propagation.Factory;
    +import brave.propagation.Propagation.KeyFactory;
    +
    +import org.springframework.beans.factory.ObjectProvider;
    +import org.springframework.boot.actuate.autoconfigure.tracing.TracingProperties.Baggage.Correlation;
    +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    +import org.springframework.boot.context.properties.EnableConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +import org.springframework.core.annotation.Order;
    +
    +/**
    + * Brave propagation configurations. They are imported by {@link BraveAutoConfiguration}.
    + *
    + * @author Moritz Halbritter
    + */
    +class BravePropagationConfigurations {
    +
    +	/**
    +	 * Propagates traces but no baggage.
    +	 */
    +	@Configuration(proxyBeanMethods = false)
    +	@ConditionalOnProperty(value = "management.tracing.baggage.enabled", havingValue = "false")
    +	static class PropagationWithoutBaggage {
    +
    +		@Bean
    +		@ConditionalOnMissingBean(Factory.class)
    +		@ConditionalOnEnabledTracing
    +		CompositePropagationFactory propagationFactory(TracingProperties properties) {
    +			return CompositePropagationFactory.create(properties.getPropagation());
    +		}
    +
    +	}
    +
    +	/**
    +	 * Propagates traces and baggage.
    +	 */
    +	@Configuration(proxyBeanMethods = false)
    +	@ConditionalOnProperty(value = "management.tracing.baggage.enabled", matchIfMissing = true)
    +	@EnableConfigurationProperties(TracingProperties.class)
    +	static class PropagationWithBaggage {
    +
    +		private final TracingProperties tracingProperties;
    +
    +		PropagationWithBaggage(TracingProperties tracingProperties) {
    +			this.tracingProperties = tracingProperties;
    +		}
    +
    +		@Bean
    +		@ConditionalOnMissingBean
    +		BaggagePropagation.FactoryBuilder propagationFactoryBuilder(
    +				ObjectProvider baggagePropagationCustomizers) {
    +			// There's a chicken-and-egg problem here: to create a builder, we need a
    +			// factory. But the CompositePropagationFactory needs data from the builder.
    +			// We create a throw-away builder with a throw-away factory, and then copy the
    +			// config to the real builder.
    +			FactoryBuilder throwAwayBuilder = BaggagePropagation.newFactoryBuilder(createThrowAwayFactory());
    +			baggagePropagationCustomizers.orderedStream()
    +				.forEach((customizer) -> customizer.customize(throwAwayBuilder));
    +			CompositePropagationFactory propagationFactory = CompositePropagationFactory.create(
    +					this.tracingProperties.getPropagation(), BraveAutoConfiguration.BRAVE_BAGGAGE_MANAGER,
    +					LocalBaggageFields.extractFrom(throwAwayBuilder));
    +			FactoryBuilder builder = BaggagePropagation.newFactoryBuilder(propagationFactory);
    +			throwAwayBuilder.configs().forEach(builder::add);
    +			return builder;
    +		}
    +
    +		@SuppressWarnings("deprecation")
    +		private Factory createThrowAwayFactory() {
    +			return new Factory() {
    +
    +				@Override
    +				public  Propagation create(KeyFactory keyFactory) {
    +					return null;
    +				}
    +
    +			};
    +		}
    +
    +		@Bean
    +		@Order(0)
    +		BaggagePropagationCustomizer remoteFieldsBaggagePropagationCustomizer() {
    +			return (builder) -> {
    +				List remoteFields = this.tracingProperties.getBaggage().getRemoteFields();
    +				for (String fieldName : remoteFields) {
    +					builder.add(BaggagePropagationConfig.SingleBaggageField.remote(BaggageField.create(fieldName)));
    +				}
    +			};
    +		}
    +
    +		@Bean
    +		@ConditionalOnMissingBean
    +		@ConditionalOnEnabledTracing
    +		Factory propagationFactory(BaggagePropagation.FactoryBuilder factoryBuilder) {
    +			return factoryBuilder.build();
    +		}
    +
    +		@Bean
    +		@ConditionalOnMissingBean
    +		CorrelationScopeDecorator.Builder mdcCorrelationScopeDecoratorBuilder(
    +				ObjectProvider correlationScopeCustomizers) {
    +			CorrelationScopeDecorator.Builder builder = MDCScopeDecorator.newBuilder();
    +			correlationScopeCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
    +			return builder;
    +		}
    +
    +		@Bean
    +		@Order(0)
    +		@ConditionalOnProperty(prefix = "management.tracing.baggage.correlation", name = "enabled",
    +				matchIfMissing = true)
    +		CorrelationScopeCustomizer correlationFieldsCorrelationScopeCustomizer() {
    +			return (builder) -> {
    +				Correlation correlationProperties = this.tracingProperties.getBaggage().getCorrelation();
    +				for (String field : correlationProperties.getFields()) {
    +					BaggageField baggageField = BaggageField.create(field);
    +					SingleCorrelationField correlationField = SingleCorrelationField.newBuilder(baggageField)
    +						.flushOnUpdate()
    +						.build();
    +					builder.add(correlationField);
    +				}
    +			};
    +		}
    +
    +		@Bean
    +		@ConditionalOnMissingBean(CorrelationScopeDecorator.class)
    +		ScopeDecorator correlationScopeDecorator(CorrelationScopeDecorator.Builder builder) {
    +			return builder.build();
    +		}
    +
    +	}
    +
    +	/**
    +	 * Propagates neither traces nor baggage.
    +	 */
    +	@Configuration(proxyBeanMethods = false)
    +	static class NoPropagation {
    +
    +		@Bean
    +		@ConditionalOnMissingBean(Factory.class)
    +		CompositePropagationFactory noopPropagationFactory() {
    +			return CompositePropagationFactory.noop();
    +		}
    +
    +	}
    +
    +}
    diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/CompositePropagationFactory.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/CompositePropagationFactory.java
    index 4e3b09b1e915..14ec2aea1e66 100644
    --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/CompositePropagationFactory.java
    +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/CompositePropagationFactory.java
    @@ -17,6 +17,7 @@
     package org.springframework.boot.actuate.autoconfigure.tracing;
     
     import java.util.Collection;
    +import java.util.Collections;
     import java.util.List;
     import java.util.function.Predicate;
     import java.util.stream.Stream;
    @@ -84,6 +85,14 @@ public TraceContext decorate(TraceContext context) {
     			.orElse(context);
     	}
     
    +	/**
    +	 * Creates a new {@link CompositePropagationFactory} which doesn't do any propagation.
    +	 * @return the {@link CompositePropagationFactory}
    +	 */
    +	static CompositePropagationFactory noop() {
    +		return new CompositePropagationFactory(Collections.emptyList(), Collections.emptyList());
    +	}
    +
     	/**
     	 * Creates a new {@link CompositePropagationFactory}.
     	 * @param properties the propagation properties
    diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java
    index 92e75afc0761..9673ec0f25fe 100644
    --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java
    +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java
    @@ -32,9 +32,7 @@
     import io.micrometer.tracing.otel.bridge.OtelSpanCustomizer;
     import io.micrometer.tracing.otel.bridge.OtelTracer;
     import io.micrometer.tracing.otel.bridge.OtelTracer.EventPublisher;
    -import io.micrometer.tracing.otel.bridge.Slf4JBaggageEventListener;
     import io.micrometer.tracing.otel.bridge.Slf4JEventListener;
    -import io.micrometer.tracing.otel.propagation.BaggageTextMapPropagator;
     import io.opentelemetry.api.OpenTelemetry;
     import io.opentelemetry.api.metrics.MeterProvider;
     import io.opentelemetry.api.trace.Tracer;
    @@ -56,10 +54,9 @@
     import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
     import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
     import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
     import org.springframework.boot.context.properties.EnableConfigurationProperties;
     import org.springframework.context.annotation.Bean;
    -import org.springframework.context.annotation.Configuration;
    +import org.springframework.context.annotation.Import;
     
     /**
      * {@link EnableAutoConfiguration Auto-configuration} for OpenTelemetry tracing.
    @@ -72,6 +69,9 @@
     @AutoConfiguration(value = "openTelemetryTracingAutoConfiguration", before = MicrometerTracingAutoConfiguration.class)
     @ConditionalOnClass({ OtelTracer.class, SdkTracerProvider.class, OpenTelemetry.class })
     @EnableConfigurationProperties(TracingProperties.class)
    +@Import({ OpenTelemetryPropagationConfigurations.PropagationWithoutBaggage.class,
    +		OpenTelemetryPropagationConfigurations.PropagationWithBaggage.class,
    +		OpenTelemetryPropagationConfigurations.NoPropagation.class })
     public class OpenTelemetryAutoConfiguration {
     
     	private final TracingProperties tracingProperties;
    @@ -172,45 +172,6 @@ OtelSpanCustomizer otelSpanCustomizer() {
     		return new OtelSpanCustomizer();
     	}
     
    -	@Configuration(proxyBeanMethods = false)
    -	@ConditionalOnProperty(prefix = "management.tracing.baggage", name = "enabled", matchIfMissing = true)
    -	static class BaggageConfiguration {
    -
    -		private final TracingProperties tracingProperties;
    -
    -		BaggageConfiguration(TracingProperties tracingProperties) {
    -			this.tracingProperties = tracingProperties;
    -		}
    -
    -		@Bean
    -		TextMapPropagator textMapPropagatorWithBaggage(OtelCurrentTraceContext otelCurrentTraceContext) {
    -			List remoteFields = this.tracingProperties.getBaggage().getRemoteFields();
    -			BaggageTextMapPropagator baggagePropagator = new BaggageTextMapPropagator(remoteFields,
    -					new OtelBaggageManager(otelCurrentTraceContext, remoteFields, Collections.emptyList()));
    -			return CompositeTextMapPropagator.create(this.tracingProperties.getPropagation(), baggagePropagator);
    -		}
    -
    -		@Bean
    -		@ConditionalOnMissingBean
    -		@ConditionalOnProperty(prefix = "management.tracing.baggage.correlation", name = "enabled",
    -				matchIfMissing = true)
    -		Slf4JBaggageEventListener otelSlf4JBaggageEventListener() {
    -			return new Slf4JBaggageEventListener(this.tracingProperties.getBaggage().getCorrelation().getFields());
    -		}
    -
    -	}
    -
    -	@Configuration(proxyBeanMethods = false)
    -	@ConditionalOnProperty(prefix = "management.tracing.baggage", name = "enabled", havingValue = "false")
    -	static class NoBaggageConfiguration {
    -
    -		@Bean
    -		TextMapPropagator textMapPropagator(TracingProperties properties) {
    -			return CompositeTextMapPropagator.create(properties.getPropagation(), null);
    -		}
    -
    -	}
    -
     	static class OTelEventPublisher implements EventPublisher {
     
     		private final List listeners;
    diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryPropagationConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryPropagationConfigurations.java
    new file mode 100644
    index 000000000000..4b9fcad16613
    --- /dev/null
    +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryPropagationConfigurations.java
    @@ -0,0 +1,105 @@
    +/*
    + * Copyright 2012-2023 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.actuate.autoconfigure.tracing;
    +
    +import java.util.Collections;
    +import java.util.List;
    +
    +import io.micrometer.tracing.otel.bridge.OtelBaggageManager;
    +import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext;
    +import io.micrometer.tracing.otel.bridge.Slf4JBaggageEventListener;
    +import io.micrometer.tracing.otel.propagation.BaggageTextMapPropagator;
    +import io.opentelemetry.context.propagation.TextMapPropagator;
    +
    +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    +import org.springframework.boot.context.properties.EnableConfigurationProperties;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +
    +/**
    + * OpenTelemetry propagation configurations. They are imported by
    + * {@link OpenTelemetryAutoConfiguration}.
    + *
    + * @author Moritz Halbritter
    + */
    +class OpenTelemetryPropagationConfigurations {
    +
    +	/**
    +	 * Propagates traces but no baggage.
    +	 */
    +	@Configuration(proxyBeanMethods = false)
    +	@ConditionalOnProperty(prefix = "management.tracing.baggage", name = "enabled", havingValue = "false")
    +	@EnableConfigurationProperties(TracingProperties.class)
    +	static class PropagationWithoutBaggage {
    +
    +		@Bean
    +		@ConditionalOnEnabledTracing
    +		TextMapPropagator textMapPropagator(TracingProperties properties) {
    +			return CompositeTextMapPropagator.create(properties.getPropagation(), null);
    +		}
    +
    +	}
    +
    +	/**
    +	 * Propagates traces and baggage.
    +	 */
    +	@Configuration(proxyBeanMethods = false)
    +	@ConditionalOnProperty(prefix = "management.tracing.baggage", name = "enabled", matchIfMissing = true)
    +	@EnableConfigurationProperties(TracingProperties.class)
    +	static class PropagationWithBaggage {
    +
    +		private final TracingProperties tracingProperties;
    +
    +		PropagationWithBaggage(TracingProperties tracingProperties) {
    +			this.tracingProperties = tracingProperties;
    +		}
    +
    +		@Bean
    +		@ConditionalOnEnabledTracing
    +		TextMapPropagator textMapPropagatorWithBaggage(OtelCurrentTraceContext otelCurrentTraceContext) {
    +			List remoteFields = this.tracingProperties.getBaggage().getRemoteFields();
    +			BaggageTextMapPropagator baggagePropagator = new BaggageTextMapPropagator(remoteFields,
    +					new OtelBaggageManager(otelCurrentTraceContext, remoteFields, Collections.emptyList()));
    +			return CompositeTextMapPropagator.create(this.tracingProperties.getPropagation(), baggagePropagator);
    +		}
    +
    +		@Bean
    +		@ConditionalOnMissingBean
    +		@ConditionalOnProperty(prefix = "management.tracing.baggage.correlation", name = "enabled",
    +				matchIfMissing = true)
    +		Slf4JBaggageEventListener otelSlf4JBaggageEventListener() {
    +			return new Slf4JBaggageEventListener(this.tracingProperties.getBaggage().getCorrelation().getFields());
    +		}
    +
    +	}
    +
    +	/**
    +	 * Propagates neither traces nor baggage.
    +	 */
    +	@Configuration(proxyBeanMethods = false)
    +	static class NoPropagation {
    +
    +		@Bean
    +		@ConditionalOnMissingBean
    +		TextMapPropagator noopTextMapPropagator() {
    +			return TextMapPropagator.noop();
    +		}
    +
    +	}
    +
    +}
    diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java
    index 1637f2901289..246a6a184c10 100644
    --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java
    +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java
    @@ -344,6 +344,16 @@ void compositeSpanHandlerUsesFilterPredicateAndReportersInOrder() {
     		});
     	}
     
    +	@Test
    +	void shouldDisablePropagationIfTracingIsDisabled() {
    +		this.contextRunner.withPropertyValues("management.tracing.enabled=false").run((context) -> {
    +			assertThat(context).hasSingleBean(Factory.class);
    +			Factory factory = context.getBean(Factory.class);
    +			Propagation propagation = factory.get();
    +			assertThat(propagation.keys()).isEmpty();
    +		});
    +	}
    +
     	private void injectToMap(Map map, String key, String value) {
     		map.put(key, value);
     	}
    diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java
    index 64bb18571381..0d7d8b171bed 100644
    --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java
    +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java
    @@ -290,6 +290,15 @@ void defaultSpanProcessorShouldUseMeterProviderIfAvailable() {
     		});
     	}
     
    +	@Test
    +	void shouldDisablePropagationIfTracingIsDisabled() {
    +		this.contextRunner.withPropertyValues("management.tracing.enabled=false").run((context) -> {
    +			assertThat(context).hasSingleBean(TextMapPropagator.class);
    +			TextMapPropagator propagator = context.getBean(TextMapPropagator.class);
    +			assertThat(propagator.fields()).isEmpty();
    +		});
    +	}
    +
     	private List getInjectors(TextMapPropagator propagator) {
     		assertThat(propagator).as("propagator").isNotNull();
     		if (propagator instanceof CompositeTextMapPropagator compositePropagator) {
    
    From 8e3f9cbc1a32c96b1b35869f698cb8abda52c533 Mon Sep 17 00:00:00 2001
    From: Andy Wilkinson 
    Date: Tue, 28 Nov 2023 17:47:40 +0000
    Subject: [PATCH 0842/1215] Upgrade to Hibernate 6.4.0.Final
    
    Closes gh-38523
    ---
     spring-boot-project/spring-boot-dependencies/build.gradle | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle
    index 1768b5b755a0..661c6a1618c3 100644
    --- a/spring-boot-project/spring-boot-dependencies/build.gradle
    +++ b/spring-boot-project/spring-boot-dependencies/build.gradle
    @@ -377,7 +377,7 @@ bom {
     			]
     		}
     	}
    -	library("Hibernate", "6.3.1.Final") {
    +	library("Hibernate", "6.4.0.Final") {
     		group("org.hibernate.orm") {
     			modules = [
     				"hibernate-agroal",
    
    From 49990afd78926da3a2c8d00c3a1869d1cd2be8fb Mon Sep 17 00:00:00 2001
    From: Andy Wilkinson 
    Date: Tue, 5 Dec 2023 20:49:34 +0000
    Subject: [PATCH 0843/1215] Polish
    
    See gh-38592
    ---
     .../boot/loader/nio/file/NestedFileSystemTests.java         | 6 +++---
     1 file changed, 3 insertions(+), 3 deletions(-)
    
    diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemTests.java
    index aa56b0b6e1e1..8eb79c743f43 100644
    --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemTests.java
    +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemTests.java
    @@ -127,15 +127,15 @@ void getPathWhenClosedThrowsException() throws Exception {
     	}
     
     	@Test
    -	void getPathWhenFirstIsNullThrowsException() {
    +	void getPathWhenFirstIsNull() {
     		Path path = this.fileSystem.getPath(null);
    -		assertThat(path.toString()).endsWith("/test.jar");
    +		assertThat(path.toString()).endsWith(File.separator + "test.jar");
     	}
     
     	@Test
     	void getPathWhenFirstIsBlank() {
     		Path path = this.fileSystem.getPath("");
    -		assertThat(path.toString()).endsWith("/test.jar");
    +		assertThat(path.toString()).endsWith(File.separator + "test.jar");
     	}
     
     	@Test
    
    From 4fb68d4e234c0aa29d4ed453f2401387dd913fea Mon Sep 17 00:00:00 2001
    From: Phillip Webb 
    Date: Tue, 5 Dec 2023 13:57:13 -0800
    Subject: [PATCH 0844/1215] Start building against Spring Framework 6.1.2
     snapshots
    
    See gh-38666
    ---
     gradle.properties | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/gradle.properties b/gradle.properties
    index 1dfc138f24b1..09bed8d5c337 100644
    --- a/gradle.properties
    +++ b/gradle.properties
    @@ -12,7 +12,7 @@ junitJupiterVersion=5.10.1
     kotlinVersion=1.9.20
     mavenVersion=3.9.4
     nativeBuildToolsVersion=0.9.28
    -springFrameworkVersion=6.1.1
    +springFrameworkVersion=6.1.2-SNAPSHOT
     tomcatVersion=10.1.16
     
     kotlin.stdlib.default.dependency=false
    
    From 6b58051aad95f6484b5a1db8479aa69ca7ab63b9 Mon Sep 17 00:00:00 2001
    From: Phillip Webb 
    Date: Tue, 5 Dec 2023 14:06:46 -0800
    Subject: [PATCH 0845/1215] Polish Binder code
    
    ---
     .../boot/context/properties/bind/Binder.java  | 35 ++++++++-----------
     .../properties/bind/DataObjectBinder.java     |  4 +--
     2 files changed, 17 insertions(+), 22 deletions(-)
    
    diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java
    index c0924484651b..b88e03c90620 100644
    --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java
    +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java
    @@ -25,8 +25,10 @@
     import java.util.HashSet;
     import java.util.List;
     import java.util.Map;
    +import java.util.Objects;
     import java.util.Set;
     import java.util.function.Consumer;
    +import java.util.function.Function;
     import java.util.function.Supplier;
     
     import org.springframework.beans.PropertyEditorRegistry;
    @@ -360,7 +362,8 @@ private  T handleBindResult(ConfigurationPropertyName name, Bindable targe
     			result = context.getConverter().convert(result, target);
     		}
     		if (result == null && create) {
    -			result = create(target, context);
    +			result = fromDataObjectBinders(target.getBindMethod(),
    +					(dataObjectBinder) -> dataObjectBinder.create(target, context));
     			result = handler.onCreate(name, target, context, result);
     			result = context.getConverter().convert(result, target);
     			Assert.state(result != null, () -> "Unable to create instance for " + target.getType());
    @@ -369,16 +372,6 @@ private  T handleBindResult(ConfigurationPropertyName name, Bindable targe
     		return context.getConverter().convert(result, target);
     	}
     
    -	private Object create(Bindable target, Context context) {
    -		for (DataObjectBinder dataObjectBinder : this.dataObjectBinders.get(target.getBindMethod())) {
    -			Object instance = dataObjectBinder.create(target, context);
    -			if (instance != null) {
    -				return instance;
    -			}
    -		}
    -		return null;
    -	}
    -
     	private  T handleBindError(ConfigurationPropertyName name, Bindable target, BindHandler handler,
     			Context context, Exception error) {
     		try {
    @@ -477,15 +470,17 @@ private Object bindDataObject(ConfigurationPropertyName name, Bindable target
     		}
     		DataObjectPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(name.append(propertyName),
     				propertyTarget, handler, context, false, false);
    -		return context.withDataObject(type, () -> {
    -			for (DataObjectBinder dataObjectBinder : this.dataObjectBinders.get(bindMethod)) {
    -				Object instance = dataObjectBinder.bind(name, target, context, propertyBinder);
    -				if (instance != null) {
    -					return instance;
    -				}
    -			}
    -			return null;
    -		});
    +		return context.withDataObject(type, () -> fromDataObjectBinders(bindMethod,
    +				(dataObjectBinder) -> dataObjectBinder.bind(name, target, context, propertyBinder)));
    +	}
    +
    +	private Object fromDataObjectBinders(BindMethod bindMethod, Function operation) {
    +		return this.dataObjectBinders.get(bindMethod)
    +			.stream()
    +			.map(operation)
    +			.filter(Objects::nonNull)
    +			.findFirst()
    +			.orElse(null);
     	}
     
     	private boolean isUnbindableBean(ConfigurationPropertyName name, Bindable target, Context context) {
    diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java
    index 4bf63dd665d7..6fdd230c6d8d 100644
    --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java
    +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java
    @@ -33,11 +33,11 @@ interface DataObjectBinder {
     	/**
     	 * Return a bound instance or {@code null} if the {@link DataObjectBinder} does not
     	 * support the specified {@link Bindable}.
    +	 * @param  the source type
     	 * @param name the name being bound
     	 * @param target the bindable to bind
     	 * @param context the bind context
     	 * @param propertyBinder property binder
    -	 * @param  the source type
     	 * @return a bound instance or {@code null}
     	 */
     	 T bind(ConfigurationPropertyName name, Bindable target, Context context,
    @@ -46,9 +46,9 @@  T bind(ConfigurationPropertyName name, Bindable target, Context context,
     	/**
     	 * Return a newly created instance or {@code null} if the {@link DataObjectBinder}
     	 * does not support the specified {@link Bindable}.
    +	 * @param  the source type
     	 * @param target the bindable to create
     	 * @param context the bind context
    -	 * @param  the source type
     	 * @return the created instance
     	 */
     	 T create(Bindable target, Context context);
    
    From f6090227313fd8077b00851acd31c1fc6e860974 Mon Sep 17 00:00:00 2001
    From: Phillip Webb 
    Date: Tue, 5 Dec 2023 14:08:08 -0800
    Subject: [PATCH 0846/1215] Add suppressed missing parameters exception from
     ValueObjectBinder
    
    Update `DataObjectBinder` interface and `ValueObjectBinder`
    implementation so that suppressed exceptions are added whenever
    parameter names cannot be discovered.
    
    See gh-38603
    ---
     .../boot/context/properties/bind/Binder.java  |  8 +-
     .../properties/bind/DataObjectBinder.java     | 11 +++
     .../properties/bind/ValueObjectBinder.java    | 84 +++++++++++++++----
     .../bind/ValueObjectBinderTests.java          | 14 +++-
     4 files changed, 101 insertions(+), 16 deletions(-)
    
    diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java
    index b88e03c90620..4d8fbf0133d2 100644
    --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java
    +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java
    @@ -366,7 +366,13 @@ private  T handleBindResult(ConfigurationPropertyName name, Bindable targe
     					(dataObjectBinder) -> dataObjectBinder.create(target, context));
     			result = handler.onCreate(name, target, context, result);
     			result = context.getConverter().convert(result, target);
    -			Assert.state(result != null, () -> "Unable to create instance for " + target.getType());
    +			if (result == null) {
    +				IllegalStateException ex = new IllegalStateException(
    +						"Unable to create instance for " + target.getType());
    +				this.dataObjectBinders.get(target.getBindMethod())
    +					.forEach((dataObjectBinder) -> dataObjectBinder.onUnableToCreateInstance(target, context, ex));
    +				throw ex;
    +			}
     		}
     		handler.onFinish(name, target, context, result);
     		return context.getConverter().convert(result, target);
    diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java
    index 6fdd230c6d8d..f57b1c4c4467 100644
    --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java
    +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java
    @@ -53,4 +53,15 @@  T bind(ConfigurationPropertyName name, Bindable target, Context context,
     	 */
     	 T create(Bindable target, Context context);
     
    +	/**
    +	 * Callback that can be used to add additional suppressed exceptions when an instance
    +	 * cannot be created.
    +	 * @param  the source type
    +	 * @param target the bindable that was being created
    +	 * @param context the bind context
    +	 * @param exception the exception about to be thrown
    +	 */
    +	default  void onUnableToCreateInstance(Bindable target, Binder.Context context, RuntimeException exception) {
    +	}
    +
     }
    diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java
    index 5f51e91f3410..aecac3e612db 100644
    --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java
    +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java
    @@ -19,6 +19,7 @@
     import java.lang.annotation.Annotation;
     import java.lang.reflect.Array;
     import java.lang.reflect.Constructor;
    +import java.lang.reflect.Method;
     import java.lang.reflect.Modifier;
     import java.lang.reflect.Parameter;
     import java.util.ArrayList;
    @@ -27,6 +28,7 @@
     import java.util.List;
     import java.util.Map;
     import java.util.Optional;
    +import java.util.function.Consumer;
     
     import kotlin.reflect.KFunction;
     import kotlin.reflect.KParameter;
    @@ -35,6 +37,7 @@
     import org.apache.commons.logging.LogFactory;
     
     import org.springframework.beans.BeanUtils;
    +import org.springframework.boot.context.properties.bind.Binder.Context;
     import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
     import org.springframework.core.CollectionFactory;
     import org.springframework.core.DefaultParameterNameDiscoverer;
    @@ -69,7 +72,7 @@ class ValueObjectBinder implements DataObjectBinder {
     	@Override
     	public  T bind(ConfigurationPropertyName name, Bindable target, Binder.Context context,
     			DataObjectPropertyBinder propertyBinder) {
    -		ValueObject valueObject = ValueObject.get(target, this.constructorProvider, context);
    +		ValueObject valueObject = ValueObject.get(target, this.constructorProvider, context, Discoverer.LENIENT);
     		if (valueObject == null) {
     			return null;
     		}
    @@ -90,7 +93,7 @@ public  T bind(ConfigurationPropertyName name, Bindable target, Binder.Con
     
     	@Override
     	public  T create(Bindable target, Binder.Context context) {
    -		ValueObject valueObject = ValueObject.get(target, this.constructorProvider, context);
    +		ValueObject valueObject = ValueObject.get(target, this.constructorProvider, context, Discoverer.LENIENT);
     		if (valueObject == null) {
     			return null;
     		}
    @@ -102,6 +105,16 @@ public  T create(Bindable target, Binder.Context context) {
     		return valueObject.instantiate(args);
     	}
     
    +	@Override
    +	public  void onUnableToCreateInstance(Bindable target, Context context, RuntimeException exception) {
    +		try {
    +			ValueObject.get(target, this.constructorProvider, context, Discoverer.STRICT);
    +		}
    +		catch (Exception ex) {
    +			exception.addSuppressed(ex);
    +		}
    +	}
    +
     	private  T getDefaultValue(Binder.Context context, ConstructorParameter parameter) {
     		ResolvableType type = parameter.getType();
     		Annotation[] annotations = parameter.getAnnotations();
    @@ -187,7 +200,7 @@ T instantiate(List args) {
     
     		@SuppressWarnings("unchecked")
     		static  ValueObject get(Bindable bindable, BindConstructorProvider constructorProvider,
    -				Binder.Context context) {
    +				Binder.Context context, ParameterNameDiscoverer parameterNameDiscoverer) {
     			Class type = (Class) bindable.getType().resolve();
     			if (type == null || type.isEnum() || Modifier.isAbstract(type.getModifiers())) {
     				return null;
    @@ -198,9 +211,10 @@ static  ValueObject get(Bindable bindable, BindConstructorProvider cons
     				return null;
     			}
     			if (KotlinDetector.isKotlinType(type)) {
    -				return KotlinValueObject.get((Constructor) bindConstructor, bindable.getType());
    +				return KotlinValueObject.get((Constructor) bindConstructor, bindable.getType(),
    +						parameterNameDiscoverer);
     			}
    -			return DefaultValueObject.get(bindConstructor, bindable.getType());
    +			return DefaultValueObject.get(bindConstructor, bindable.getType(), parameterNameDiscoverer);
     		}
     
     	}
    @@ -246,12 +260,13 @@ List getConstructorParameters() {
     			return this.constructorParameters;
     		}
     
    -		static  ValueObject get(Constructor bindConstructor, ResolvableType type) {
    +		static  ValueObject get(Constructor bindConstructor, ResolvableType type,
    +				ParameterNameDiscoverer parameterNameDiscoverer) {
     			KFunction kotlinConstructor = ReflectJvmMapping.getKotlinFunction(bindConstructor);
     			if (kotlinConstructor != null) {
     				return new KotlinValueObject<>(bindConstructor, kotlinConstructor, type);
     			}
    -			return DefaultValueObject.get(bindConstructor, type);
    +			return DefaultValueObject.get(bindConstructor, type, parameterNameDiscoverer);
     		}
     
     	}
    @@ -262,8 +277,6 @@ static  ValueObject get(Constructor bindConstructor, ResolvableType typ
     	 */
     	private static final class DefaultValueObject extends ValueObject {
     
    -		private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
    -
     		private final List constructorParameters;
     
     		private DefaultValueObject(Constructor constructor, List constructorParameters) {
    @@ -277,12 +290,10 @@ List getConstructorParameters() {
     		}
     
     		@SuppressWarnings("unchecked")
    -		static  ValueObject get(Constructor bindConstructor, ResolvableType type) {
    -			String[] names = PARAMETER_NAME_DISCOVERER.getParameterNames(bindConstructor);
    +		static  ValueObject get(Constructor bindConstructor, ResolvableType type,
    +				ParameterNameDiscoverer parameterNameDiscoverer) {
    +			String[] names = parameterNameDiscoverer.getParameterNames(bindConstructor);
     			if (names == null) {
    -				logger.debug(LogMessage.format(
    -						"Unable to use value object binding with %s as parameter names cannot be discovered",
    -						bindConstructor));
     				return null;
     			}
     			List constructorParameters = parseConstructorParameters(bindConstructor, type, names);
    @@ -339,4 +350,49 @@ ResolvableType getType() {
     
     	}
     
    +	/**
    +	 * {@link ParameterNameDiscoverer} used for value data object binding.
    +	 */
    +	static final class Discoverer implements ParameterNameDiscoverer {
    +
    +		private static final ParameterNameDiscoverer DEFAULT_DELEGATE = new DefaultParameterNameDiscoverer();
    +
    +		private static final ParameterNameDiscoverer LENIENT = new Discoverer(DEFAULT_DELEGATE, (message) -> {
    +		});
    +
    +		private static final ParameterNameDiscoverer STRICT = new Discoverer(DEFAULT_DELEGATE, (message) -> {
    +			throw new IllegalStateException(message.toString());
    +		});
    +
    +		private final ParameterNameDiscoverer delegate;
    +
    +		private final Consumer noParameterNamesHandler;
    +
    +		private Discoverer(ParameterNameDiscoverer delegate, Consumer noParameterNamesHandler) {
    +			this.delegate = delegate;
    +			this.noParameterNamesHandler = noParameterNamesHandler;
    +		}
    +
    +		@Override
    +		public String[] getParameterNames(Method method) {
    +			throw new UnsupportedOperationException();
    +		}
    +
    +		@Override
    +		public String[] getParameterNames(Constructor constructor) {
    +			String[] names = this.delegate.getParameterNames(constructor);
    +			if (names != null) {
    +				return names;
    +			}
    +			LogMessage message = LogMessage.format(
    +					"Unable to use value object binding with constructor [%s] as parameter names cannot be discovered. "
    +							+ "Ensure that the compiler uses the '-parameters' flag",
    +					constructor);
    +			this.noParameterNamesHandler.accept(message);
    +			logger.debug(message);
    +			return null;
    +		}
    +
    +	}
    +
     }
    diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java
    index a47f30aa0fcb..ac95a9d1d617 100644
    --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java
    +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ValueObjectBinderTests.java
    @@ -27,6 +27,7 @@
     import java.util.Optional;
     
     import com.jayway.jsonpath.JsonPath;
    +import com.jayway.jsonpath.internal.CharacterIndex;
     import org.junit.jupiter.api.Test;
     
     import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
    @@ -394,7 +395,7 @@ public record RecordProperties(
     	}
     
     	@Test // gh-38201
    -	void bindWithNonExtractableParameterNamesAndNonIterablePropertySource() throws Exception {
    +	void bindWhenNonExtractableParameterNamesOnPropertyAndNonIterablePropertySource() throws Exception {
     		verifyJsonPathParametersCannotBeResolved();
     		MockConfigurationPropertySource source = new MockConfigurationPropertySource();
     		source.put("test.value", "test");
    @@ -404,6 +405,17 @@ void bindWithNonExtractableParameterNamesAndNonIterablePropertySource() throws E
     		assertThat(bound.getValue()).isEqualTo("test");
     	}
     
    +	@Test
    +	void createWhenNonExtractableParameterNamesOnPropertyAndNonIterablePropertySource() throws Exception {
    +		assertThat(new DefaultParameterNameDiscoverer()
    +			.getParameterNames(CharacterIndex.class.getDeclaredConstructor(CharSequence.class))).isNull();
    +		MockConfigurationPropertySource source = new MockConfigurationPropertySource();
    +		this.sources.add(source.nonIterable());
    +		Bindable target = Bindable.of(CharacterIndex.class).withBindMethod(BindMethod.VALUE_OBJECT);
    +		assertThatExceptionOfType(BindException.class).isThrownBy(() -> this.binder.bindOrCreate("test", target))
    +			.withStackTraceContaining("Ensure that the compiler uses the '-parameters' flag");
    +	}
    +
     	private void verifyJsonPathParametersCannotBeResolved() throws NoSuchFieldException {
     		Class jsonPathClass = NonExtractableParameterName.class.getDeclaredField("jsonPath").getType();
     		Constructor[] constructors = jsonPathClass.getDeclaredConstructors();
    
    From ce7d384d2c0504899c19e829404a011e3b91bcfa Mon Sep 17 00:00:00 2001
    From: Phillip Webb 
    Date: Tue, 5 Dec 2023 14:08:14 -0800
    Subject: [PATCH 0847/1215] Add MissingParametersFailureAnalyzer
    
    Add a new failure analyzer that provides hints whenever parameter
    names cannot be discovered.
    
    Closes gh-38603
    ---
     .../analyzer/BindFailureAnalyzer.java         |  25 ++--
     .../MissingParameterNamesFailureAnalyzer.java | 116 +++++++++++++++++
     .../MissingParametersFailureAnalyzer.java     |  83 ++++++++++++
     .../main/resources/META-INF/spring.factories  |   1 +
     .../analyzer/BindFailureAnalyzerTests.java    |  13 ++
     ...ingParameterNamesFailureAnalyzerTests.java | 123 ++++++++++++++++++
     6 files changed, 353 insertions(+), 8 deletions(-)
     create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/MissingParameterNamesFailureAnalyzer.java
     create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/MissingParametersFailureAnalyzer.java
     create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/MissingParameterNamesFailureAnalyzerTests.java
    
    diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzer.java
    index 2ee3fa5ae6b8..e5b5efe43619 100644
    --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzer.java
    +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzer.java
    @@ -49,15 +49,20 @@ protected FailureAnalysis analyze(Throwable rootFailure, BindException cause) {
     				|| rootCause instanceof UnboundConfigurationPropertiesException) {
     			return null;
     		}
    -		return analyzeGenericBindException(cause);
    +		return analyzeGenericBindException(rootFailure, cause);
     	}
     
    -	private FailureAnalysis analyzeGenericBindException(BindException cause) {
    +	private FailureAnalysis analyzeGenericBindException(Throwable rootFailure, BindException cause) {
    +		FailureAnalysis missingParametersAnalysis = MissingParameterNamesFailureAnalyzer
    +			.analyzeForMissingParameters(rootFailure);
     		StringBuilder description = new StringBuilder(String.format("%s:%n", cause.getMessage()));
     		ConfigurationProperty property = cause.getProperty();
     		buildDescription(description, property);
     		description.append(String.format("%n    Reason: %s", getMessage(cause)));
    -		return getFailureAnalysis(description, cause);
    +		if (missingParametersAnalysis != null) {
    +			MissingParameterNamesFailureAnalyzer.appendPossibility(description);
    +		}
    +		return getFailureAnalysis(description.toString(), cause, missingParametersAnalysis);
     	}
     
     	private void buildDescription(StringBuilder description, ConfigurationProperty property) {
    @@ -98,14 +103,18 @@ private String getExceptionTypeAndMessage(Throwable ex) {
     		return ex.getClass().getName() + (StringUtils.hasText(message) ? ": " + message : "");
     	}
     
    -	private FailureAnalysis getFailureAnalysis(Object description, BindException cause) {
    -		StringBuilder message = new StringBuilder("Update your application's configuration");
    +	private FailureAnalysis getFailureAnalysis(String description, BindException cause,
    +			FailureAnalysis missingParametersAnalysis) {
    +		StringBuilder action = new StringBuilder("Update your application's configuration");
     		Collection validValues = findValidValues(cause);
     		if (!validValues.isEmpty()) {
    -			message.append(String.format(". The following values are valid:%n"));
    -			validValues.forEach((value) -> message.append(String.format("%n    %s", value)));
    +			action.append(String.format(". The following values are valid:%n"));
    +			validValues.forEach((value) -> action.append(String.format("%n    %s", value)));
    +		}
    +		if (missingParametersAnalysis != null) {
    +			action.append(String.format("%n%n%s", missingParametersAnalysis.getAction()));
     		}
    -		return new FailureAnalysis(description.toString(), message.toString(), cause);
    +		return new FailureAnalysis(description, action.toString(), cause);
     	}
     
     	private Collection findValidValues(BindException ex) {
    diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/MissingParameterNamesFailureAnalyzer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/MissingParameterNamesFailureAnalyzer.java
    new file mode 100644
    index 000000000000..442a2cabbb7a
    --- /dev/null
    +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/MissingParameterNamesFailureAnalyzer.java
    @@ -0,0 +1,116 @@
    +/*
    + * Copyright 2012-2023 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.diagnostics.analyzer;
    +
    +import java.util.HashSet;
    +import java.util.Set;
    +
    +import org.springframework.boot.diagnostics.FailureAnalysis;
    +import org.springframework.boot.diagnostics.FailureAnalyzer;
    +import org.springframework.core.Ordered;
    +import org.springframework.core.annotation.Order;
    +import org.springframework.util.StringUtils;
    +
    +/**
    + * {@link FailureAnalyzer} for exceptions caused by missing parameter names. This analyzer
    + * is ordered last, if other analyzers wish to also report parameter actions they can use
    + * the {@link #analyzeForMissingParameters(Throwable)} static method.
    + *
    + * @author Phillip Webb
    + */
    +@Order(Ordered.LOWEST_PRECEDENCE)
    +class MissingParameterNamesFailureAnalyzer implements FailureAnalyzer {
    +
    +	private static final String USE_PARAMETERS_MESSAGE = "Ensure that the compiler uses the '-parameters' flag";
    +
    +	static final String POSSIBILITY = "This may be due to missing parameter name information";
    +
    +	static String ACTION = """
    +			Ensure that your compiler is configured to use the '-parameters' flag.
    +			You may need to update both your build tool settings as well as your IDE.
    +			(See https://github.com/spring-projects/spring-framework/wiki/Upgrading-to-Spring-Framework-6.x#parameter-name-retention)
    +							""";
    +
    +	@Override
    +	public FailureAnalysis analyze(Throwable failure) {
    +		return analyzeForMissingParameters(failure);
    +	}
    +
    +	/**
    +	 * Analyze the given failure for missing parameter name exceptions.
    +	 * @param failure the failure to analyze
    +	 * @return a failure analysis or {@code null}
    +	 */
    +	static FailureAnalysis analyzeForMissingParameters(Throwable failure) {
    +		return analyzeForMissingParameters(failure, failure, new HashSet<>());
    +	}
    +
    +	private static FailureAnalysis analyzeForMissingParameters(Throwable rootFailure, Throwable cause,
    +			Set seen) {
    +		if (cause != null && seen.add(cause)) {
    +			if (isSpringParametersException(cause)) {
    +				return getAnalysis(rootFailure, cause);
    +			}
    +			FailureAnalysis analysis = analyzeForMissingParameters(rootFailure, cause.getCause(), seen);
    +			if (analysis != null) {
    +				return analysis;
    +			}
    +			for (Throwable suppressed : cause.getSuppressed()) {
    +				analysis = analyzeForMissingParameters(rootFailure, suppressed, seen);
    +				if (analysis != null) {
    +					return analysis;
    +				}
    +			}
    +		}
    +		return null;
    +	}
    +
    +	private static boolean isSpringParametersException(Throwable failure) {
    +		String message = failure.getMessage();
    +		return message != null && message.contains(USE_PARAMETERS_MESSAGE) && isSpringException(failure);
    +	}
    +
    +	private static boolean isSpringException(Throwable failure) {
    +		StackTraceElement[] elements = failure.getStackTrace();
    +		return elements.length > 0 && isSpringClass(elements[0].getClassName());
    +	}
    +
    +	private static boolean isSpringClass(String className) {
    +		return className != null && className.startsWith("org.springframework.");
    +	}
    +
    +	private static FailureAnalysis getAnalysis(Throwable rootFailure, Throwable cause) {
    +		StringBuilder description = new StringBuilder(String.format("%s:%n", cause.getMessage()));
    +		if (rootFailure != cause) {
    +			description.append(String.format("%n    Resulting Failure: %s", getExceptionTypeAndMessage(rootFailure)));
    +		}
    +		return new FailureAnalysis(description.toString(), ACTION, rootFailure);
    +	}
    +
    +	private static String getExceptionTypeAndMessage(Throwable ex) {
    +		String message = ex.getMessage();
    +		return ex.getClass().getName() + (StringUtils.hasText(message) ? ": " + message : "");
    +	}
    +
    +	public static void appendPossibility(StringBuilder description) {
    +		if (!description.toString().endsWith(System.lineSeparator())) {
    +			description.append("%n".formatted());
    +		}
    +		description.append("%n%s".formatted(POSSIBILITY));
    +	}
    +
    +}
    diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/MissingParametersFailureAnalyzer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/MissingParametersFailureAnalyzer.java
    new file mode 100644
    index 000000000000..350e30aafa2a
    --- /dev/null
    +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/MissingParametersFailureAnalyzer.java
    @@ -0,0 +1,83 @@
    +/*
    + * Copyright 2012-2023 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.diagnostics.analyzer;
    +
    +import java.util.HashSet;
    +import java.util.Set;
    +
    +import org.springframework.boot.diagnostics.FailureAnalysis;
    +import org.springframework.boot.diagnostics.FailureAnalyzer;
    +import org.springframework.core.Ordered;
    +import org.springframework.core.annotation.Order;
    +
    +/**
    + * @author Phillip Webb
    + */
    +@Order(Ordered.HIGHEST_PRECEDENCE)
    +class MissingParametersFailureAnalyzer implements FailureAnalyzer {
    +
    +	private static final String USE_PARAMETERS_MESSAGE = "Ensure that the compiler uses the '-parameters' flag";
    +
    +	@Override
    +	public FailureAnalysis analyze(Throwable failure) {
    +		return analyze(failure, failure, new HashSet<>());
    +	}
    +
    +	private FailureAnalysis analyze(Throwable rootFailure, Throwable cause, Set seen) {
    +		if (cause == null || !seen.add(cause)) {
    +			return null;
    +		}
    +		if (isSpringParametersException(cause)) {
    +			return getAnalysis(rootFailure, cause);
    +		}
    +		FailureAnalysis analysis = analyze(rootFailure, cause.getCause(), seen);
    +		if (analysis != null) {
    +			return analysis;
    +		}
    +		for (Throwable suppressed : cause.getSuppressed()) {
    +			analysis = analyze(rootFailure, suppressed, seen);
    +			if (analysis != null) {
    +				return analysis;
    +			}
    +		}
    +		return null;
    +	}
    +
    +	private boolean isSpringParametersException(Throwable failure) {
    +		String message = failure.getMessage();
    +		return message != null && message.contains(USE_PARAMETERS_MESSAGE) && isSpringException(failure);
    +	}
    +
    +	private boolean isSpringException(Throwable failure) {
    +		StackTraceElement[] elements = failure.getStackTrace();
    +		return elements.length > 0 && isSpringClass(elements[0].getClassName());
    +	}
    +
    +	private boolean isSpringClass(String className) {
    +		return className != null && className.startsWith("org.springframework.");
    +	}
    +
    +	private FailureAnalysis getAnalysis(Throwable rootFailure, Throwable cause) {
    +		String description = "";
    +		if (rootFailure != cause) {
    +
    +		}
    +		String action = "";
    +		return new FailureAnalysis(description, action, rootFailure);
    +	}
    +
    +}
    diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories
    index e6775b7491f1..f6cea1c9e686 100644
    --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories
    +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories
    @@ -70,6 +70,7 @@ org.springframework.boot.diagnostics.analyzer.BeanNotOfRequiredTypeFailureAnalyz
     org.springframework.boot.diagnostics.analyzer.BindFailureAnalyzer,\
     org.springframework.boot.diagnostics.analyzer.BindValidationFailureAnalyzer,\
     org.springframework.boot.diagnostics.analyzer.UnboundConfigurationPropertyFailureAnalyzer,\
    +org.springframework.boot.diagnostics.analyzer.MissingParameterNamesFailureAnalyzer,\
     org.springframework.boot.diagnostics.analyzer.MutuallyExclusiveConfigurationPropertiesFailureAnalyzer,\
     org.springframework.boot.diagnostics.analyzer.NoSuchMethodFailureAnalyzer,\
     org.springframework.boot.diagnostics.analyzer.NoUniqueBeanDefinitionFailureAnalyzer,\
    diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzerTests.java
    index 5d5f09d03230..5d03732ff9e0 100644
    --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzerTests.java
    +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzerTests.java
    @@ -99,6 +99,19 @@ void bindExceptionDueToMapConversionFailure() {
     						+ "org.springframework.boot.logging.LogLevel>]"));
     	}
     
    +	@Test
    +	void bindExceptionWithSupressedMissingParametersException() {
    +		BeanCreationException failure = createFailure(GenericFailureConfiguration.class, "test.foo.value=alpha");
    +		failure.addSuppressed(new IllegalStateException(
    +				"Missing parameter names. Ensure that the compiler uses the '-parameters' flag"));
    +		FailureAnalysis analysis = new BindFailureAnalyzer().analyze(failure);
    +		assertThat(analysis.getDescription())
    +			.contains(failure("test.foo.value", "alpha", "\"test.foo.value\" from property source \"test\"",
    +					"failed to convert java.lang.String to int"))
    +			.contains(MissingParameterNamesFailureAnalyzer.POSSIBILITY);
    +		assertThat(analysis.getAction()).contains(MissingParameterNamesFailureAnalyzer.ACTION);
    +	}
    +
     	private static String failure(String property, String value, String origin, String reason) {
     		return String.format("Property: %s%n    Value: \"%s\"%n    Origin: %s%n    Reason: %s", property, value, origin,
     				reason);
    diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/MissingParameterNamesFailureAnalyzerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/MissingParameterNamesFailureAnalyzerTests.java
    new file mode 100644
    index 000000000000..9251d201d8d4
    --- /dev/null
    +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/MissingParameterNamesFailureAnalyzerTests.java
    @@ -0,0 +1,123 @@
    +/*
    + * Copyright 2012-2023 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.diagnostics.analyzer;
    +
    +import java.lang.reflect.Method;
    +
    +import org.junit.jupiter.api.Test;
    +
    +import org.springframework.boot.diagnostics.FailureAnalysis;
    +import org.springframework.core.MethodParameter;
    +import org.springframework.web.context.request.NativeWebRequest;
    +import org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver;
    +
    +import static org.assertj.core.api.Assertions.assertThat;
    +
    +/**
    + * Tests for {@link MissingParameterNamesFailureAnalyzer}.
    + *
    + * @author Phillip Webb
    + */
    +class MissingParameterNamesFailureAnalyzerTests {
    +
    +	@Test
    +	void analyzeWhenMissingParametersExceptionReturnsFailure() throws Exception {
    +		MissingParameterNamesFailureAnalyzer analyzer = new MissingParameterNamesFailureAnalyzer();
    +		FailureAnalysis analysis = analyzer.analyze(getSpringFrameworkMissingParameterException());
    +		assertThat(analysis.getDescription())
    +			.isEqualTo(String.format("Name for argument of type [java.lang.String] not specified, and parameter name "
    +					+ "information not available via reflection. Ensure that the compiler uses the '-parameters' flag.:%n"));
    +		assertThat(analysis.getAction()).isEqualTo(MissingParameterNamesFailureAnalyzer.ACTION);
    +	}
    +
    +	@Test
    +	void analyzeForMissingParametersWhenMissingParametersExceptionReturnsFailure() throws Exception {
    +		FailureAnalysis analysis = MissingParameterNamesFailureAnalyzer
    +			.analyzeForMissingParameters(getSpringFrameworkMissingParameterException());
    +		assertThat(analysis.getDescription())
    +			.isEqualTo(String.format("Name for argument of type [java.lang.String] not specified, and parameter name "
    +					+ "information not available via reflection. Ensure that the compiler uses the '-parameters' flag.:%n"));
    +		assertThat(analysis.getAction()).isEqualTo(MissingParameterNamesFailureAnalyzer.ACTION);
    +	}
    +
    +	@Test
    +	void analyzeForMissingParametersWhenInCauseReturnsFailure() throws Exception {
    +		RuntimeException exception = new RuntimeException("Badness", getSpringFrameworkMissingParameterException());
    +		FailureAnalysis analysis = MissingParameterNamesFailureAnalyzer.analyzeForMissingParameters(exception);
    +		assertThat(analysis.getDescription())
    +			.isEqualTo(String.format("Name for argument of type [java.lang.String] not specified, and parameter name "
    +					+ "information not available via reflection. Ensure that the compiler uses the '-parameters' flag.:%n%n"
    +					+ "    Resulting Failure: java.lang.RuntimeException: Badness"));
    +		assertThat(analysis.getAction()).isEqualTo(MissingParameterNamesFailureAnalyzer.ACTION);
    +	}
    +
    +	@Test
    +	void analyzeForMissingParametersWhenInSuppressedReturnsFailure() throws Exception {
    +		RuntimeException exception = new RuntimeException("Badness");
    +		exception.addSuppressed(getSpringFrameworkMissingParameterException());
    +		FailureAnalysis analysis = MissingParameterNamesFailureAnalyzer.analyzeForMissingParameters(exception);
    +		assertThat(analysis.getDescription())
    +			.isEqualTo(String.format("Name for argument of type [java.lang.String] not specified, and parameter name "
    +					+ "information not available via reflection. Ensure that the compiler uses the '-parameters' flag.:%n%n"
    +					+ "    Resulting Failure: java.lang.RuntimeException: Badness"));
    +		assertThat(analysis.getAction()).isEqualTo(MissingParameterNamesFailureAnalyzer.ACTION);
    +	}
    +
    +	@Test
    +	void analyzeForMissingParametersWhenNotPresentReturnsNull() {
    +		RuntimeException exception = new RuntimeException("Badness");
    +		FailureAnalysis analysis = MissingParameterNamesFailureAnalyzer.analyzeForMissingParameters(exception);
    +		assertThat(analysis).isNull();
    +	}
    +
    +	private RuntimeException getSpringFrameworkMissingParameterException() throws Exception {
    +		MockResolver resolver = new MockResolver();
    +		Method method = getClass().getDeclaredMethod("example", String.class);
    +		MethodParameter parameter = new MethodParameter(method, 0);
    +		try {
    +			resolver.resolveArgument(parameter, null, null, null);
    +		}
    +		catch (RuntimeException ex) {
    +			return ex;
    +		}
    +		throw new AssertionError("Did not throw");
    +	}
    +
    +	void example(String name) {
    +	}
    +
    +	static class MockResolver extends AbstractNamedValueMethodArgumentResolver {
    +
    +		@Override
    +		public boolean supportsParameter(MethodParameter parameter) {
    +			return true;
    +		}
    +
    +		@Override
    +		protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
    +			return new NamedValueInfo("", false, null);
    +		}
    +
    +		@Override
    +		protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request)
    +				throws Exception {
    +			return null;
    +		}
    +
    +	}
    +
    +}
    
    From ffdd405fb1823acc0948c22945df0a4641c94bea Mon Sep 17 00:00:00 2001
    From: Phillip Webb 
    Date: Tue, 5 Dec 2023 14:28:28 -0800
    Subject: [PATCH 0848/1215] Update NoUniqueBeanDefinitionFailureAnalyzer with
     parameter hints
    
    Add addition description and action text to help point to the
    fact that the `NoUniqueBeanDefinitionException` can be thrown
    due to a missing `-parameters` compiler setting.
    
    Closes gh-38652
    ---
     .../NoUniqueBeanDefinitionFailureAnalyzer.java      | 13 +++++++------
     .../NoUniqueBeanDefinitionFailureAnalyzerTests.java |  8 ++++++++
     2 files changed, 15 insertions(+), 6 deletions(-)
    
    diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/NoUniqueBeanDefinitionFailureAnalyzer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/NoUniqueBeanDefinitionFailureAnalyzer.java
    index 77c37fb0ad20..dee8f8eb9fd9 100644
    --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/NoUniqueBeanDefinitionFailureAnalyzer.java
    +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/NoUniqueBeanDefinitionFailureAnalyzer.java
    @@ -56,11 +56,12 @@ protected FailureAnalysis analyze(Throwable rootFailure, NoUniqueBeanDefinitionE
     		for (String beanName : beanNames) {
     			buildMessage(message, beanName);
     		}
    -		return new FailureAnalysis(message.toString(),
    -				"Consider marking one of the beans as @Primary, updating the consumer to"
    -						+ " accept multiple beans, or using @Qualifier to identify the"
    -						+ " bean that should be consumed",
    -				cause);
    +		MissingParameterNamesFailureAnalyzer.appendPossibility(message);
    +		StringBuilder action = new StringBuilder(
    +				"Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, "
    +						+ "or using @Qualifier to identify the bean that should be consumed");
    +		action.append("%n%n%s".formatted(MissingParameterNamesFailureAnalyzer.ACTION));
    +		return new FailureAnalysis(message.toString(), action.toString(), cause);
     	}
     
     	private void buildMessage(StringBuilder message, String beanName) {
    @@ -69,7 +70,7 @@ private void buildMessage(StringBuilder message, String beanName) {
     			message.append(getDefinitionDescription(beanName, definition));
     		}
     		catch (NoSuchBeanDefinitionException ex) {
    -			message.append(String.format("\t- %s: a programmatically registered singleton", beanName));
    +			message.append(String.format("\t- %s: a programmatically registered singleton%n", beanName));
     		}
     	}
     
    diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/NoUniqueBeanDefinitionFailureAnalyzerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/NoUniqueBeanDefinitionFailureAnalyzerTests.java
    index 35943e7ccf64..e8600faabe2a 100644
    --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/NoUniqueBeanDefinitionFailureAnalyzerTests.java
    +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/NoUniqueBeanDefinitionFailureAnalyzerTests.java
    @@ -93,6 +93,14 @@ void failureAnalysisForObjectProviderConstructorConsumer() {
     		assertFoundBeans(failureAnalysis);
     	}
     
    +	@Test
    +	void failureAnalysisIncludesPossiblyMissingParameterNames() {
    +		FailureAnalysis failureAnalysis = analyzeFailure(createFailure(MethodConsumer.class));
    +		assertThat(failureAnalysis.getDescription()).contains(MissingParameterNamesFailureAnalyzer.POSSIBILITY);
    +		assertThat(failureAnalysis.getAction()).contains(MissingParameterNamesFailureAnalyzer.ACTION);
    +		assertFoundBeans(failureAnalysis);
    +	}
    +
     	private BeanCreationException createFailure(Class consumer) {
     		this.context.registerBean("beanOne", TestBean.class);
     		this.context.register(DuplicateBeansProducer.class, consumer);
    
    From b5de38787c6abd4fc6522aa006b3d460ae23ffc5 Mon Sep 17 00:00:00 2001
    From: Phillip Webb 
    Date: Tue, 5 Dec 2023 14:57:33 -0800
    Subject: [PATCH 0849/1215] Restore `Session.Cookie` class for binary
     back-compatibility
    
    Fixes gh-38589
    ---
     .../boot/web/servlet/server/Session.java      | 10 +++++-
     .../boot/web/servlet/server/SessionTests.java | 36 +++++++++++++++++++
     2 files changed, 45 insertions(+), 1 deletion(-)
     create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/SessionTests.java
    
    diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/Session.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/Session.java
    index 815f84abda2c..d561342bfacf 100644
    --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/Session.java
    +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/Session.java
    @@ -23,7 +23,6 @@
     
     import org.springframework.boot.context.properties.NestedConfigurationProperty;
     import org.springframework.boot.convert.DurationUnit;
    -import org.springframework.boot.web.server.Cookie;
     
     /**
      * Session properties.
    @@ -103,6 +102,15 @@ SessionStoreDirectory getSessionStoreDirectory() {
     		return this.sessionStoreDirectory;
     	}
     
    +	/**
    +	 * Session cookie properties. This class is provided only for back-compatibility
    +	 * reasons, consider using {@link org.springframework.boot.web.server.Cookie} whever
    +	 * possible.
    +	 */
    +	public static class Cookie extends org.springframework.boot.web.server.Cookie {
    +
    +	}
    +
     	/**
     	 * Available session tracking modes (mirrors
     	 * {@link jakarta.servlet.SessionTrackingMode}.
    diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/SessionTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/SessionTests.java
    new file mode 100644
    index 000000000000..75b8b9d44260
    --- /dev/null
    +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/SessionTests.java
    @@ -0,0 +1,36 @@
    +/*
    + * Copyright 2012-2023 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.web.servlet.server;
    +
    +import org.junit.jupiter.api.Test;
    +
    +import static org.assertj.core.api.Assertions.assertThat;
    +
    +/**
    + * Tests for {@link Session}.
    + *
    + * @author Phillip Webb
    + */
    +class SessionTests {
    +
    +	@Test // gh-38589
    +	void getCookieIsBinaryBackCompatible() throws Exception {
    +		Class returnType = Session.class.getDeclaredMethod("getCookie").getReturnType();
    +		assertThat(returnType.getName()).isEqualTo("org.springframework.boot.web.servlet.server.Session$Cookie");
    +	}
    +
    +}
    
    From ad5b844e1f9a11b45302b5011ee99c56b9edd5e6 Mon Sep 17 00:00:00 2001
    From: Moritz Halbritter 
    Date: Wed, 6 Dec 2023 11:22:54 +0100
    Subject: [PATCH 0850/1215] Fix checkstyle issues
    
    MissingParametersFailureAnalyzer looks like it has been commited by
    accident.
    ---
     .../MissingParameterNamesFailureAnalyzer.java |  2 +-
     .../MissingParametersFailureAnalyzer.java     | 83 -------------------
     2 files changed, 1 insertion(+), 84 deletions(-)
     delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/MissingParametersFailureAnalyzer.java
    
    diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/MissingParameterNamesFailureAnalyzer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/MissingParameterNamesFailureAnalyzer.java
    index 442a2cabbb7a..68a7aca5b628 100644
    --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/MissingParameterNamesFailureAnalyzer.java
    +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/MissingParameterNamesFailureAnalyzer.java
    @@ -106,7 +106,7 @@ private static String getExceptionTypeAndMessage(Throwable ex) {
     		return ex.getClass().getName() + (StringUtils.hasText(message) ? ": " + message : "");
     	}
     
    -	public static void appendPossibility(StringBuilder description) {
    +	static void appendPossibility(StringBuilder description) {
     		if (!description.toString().endsWith(System.lineSeparator())) {
     			description.append("%n".formatted());
     		}
    diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/MissingParametersFailureAnalyzer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/MissingParametersFailureAnalyzer.java
    deleted file mode 100644
    index 350e30aafa2a..000000000000
    --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/MissingParametersFailureAnalyzer.java
    +++ /dev/null
    @@ -1,83 +0,0 @@
    -/*
    - * Copyright 2012-2023 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.diagnostics.analyzer;
    -
    -import java.util.HashSet;
    -import java.util.Set;
    -
    -import org.springframework.boot.diagnostics.FailureAnalysis;
    -import org.springframework.boot.diagnostics.FailureAnalyzer;
    -import org.springframework.core.Ordered;
    -import org.springframework.core.annotation.Order;
    -
    -/**
    - * @author Phillip Webb
    - */
    -@Order(Ordered.HIGHEST_PRECEDENCE)
    -class MissingParametersFailureAnalyzer implements FailureAnalyzer {
    -
    -	private static final String USE_PARAMETERS_MESSAGE = "Ensure that the compiler uses the '-parameters' flag";
    -
    -	@Override
    -	public FailureAnalysis analyze(Throwable failure) {
    -		return analyze(failure, failure, new HashSet<>());
    -	}
    -
    -	private FailureAnalysis analyze(Throwable rootFailure, Throwable cause, Set seen) {
    -		if (cause == null || !seen.add(cause)) {
    -			return null;
    -		}
    -		if (isSpringParametersException(cause)) {
    -			return getAnalysis(rootFailure, cause);
    -		}
    -		FailureAnalysis analysis = analyze(rootFailure, cause.getCause(), seen);
    -		if (analysis != null) {
    -			return analysis;
    -		}
    -		for (Throwable suppressed : cause.getSuppressed()) {
    -			analysis = analyze(rootFailure, suppressed, seen);
    -			if (analysis != null) {
    -				return analysis;
    -			}
    -		}
    -		return null;
    -	}
    -
    -	private boolean isSpringParametersException(Throwable failure) {
    -		String message = failure.getMessage();
    -		return message != null && message.contains(USE_PARAMETERS_MESSAGE) && isSpringException(failure);
    -	}
    -
    -	private boolean isSpringException(Throwable failure) {
    -		StackTraceElement[] elements = failure.getStackTrace();
    -		return elements.length > 0 && isSpringClass(elements[0].getClassName());
    -	}
    -
    -	private boolean isSpringClass(String className) {
    -		return className != null && className.startsWith("org.springframework.");
    -	}
    -
    -	private FailureAnalysis getAnalysis(Throwable rootFailure, Throwable cause) {
    -		String description = "";
    -		if (rootFailure != cause) {
    -
    -		}
    -		String action = "";
    -		return new FailureAnalysis(description, action, rootFailure);
    -	}
    -
    -}
    
    From 6dff3c5978158f5ddae67b3b92200a90b3b188b7 Mon Sep 17 00:00:00 2001
    From: Andy Wilkinson 
    Date: Wed, 6 Dec 2023 14:18:40 +0000
    Subject: [PATCH 0851/1215] Adapt to change in Framework's disconnected client
     detection
    
    See gh-38666
    ---
     .../AbstractErrorWebExceptionHandler.java     | 25 ++-----------------
     .../DefaultErrorWebExceptionHandlerTests.java | 10 --------
     2 files changed, 2 insertions(+), 33 deletions(-)
    
    diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java
    index 8b7daafd5c9c..8afe4a624fdd 100644
    --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java
    +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java
    @@ -18,10 +18,8 @@
     
     import java.util.Collections;
     import java.util.Date;
    -import java.util.HashSet;
     import java.util.List;
     import java.util.Map;
    -import java.util.Set;
     
     import org.apache.commons.logging.Log;
     import reactor.core.publisher.Mono;
    @@ -33,7 +31,6 @@
     import org.springframework.boot.web.reactive.error.ErrorAttributes;
     import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
     import org.springframework.context.ApplicationContext;
    -import org.springframework.core.NestedExceptionUtils;
     import org.springframework.core.io.Resource;
     import org.springframework.core.log.LogMessage;
     import org.springframework.http.HttpLogging;
    @@ -49,6 +46,7 @@
     import org.springframework.web.reactive.function.server.ServerResponse;
     import org.springframework.web.reactive.result.view.ViewResolver;
     import org.springframework.web.server.ServerWebExchange;
    +import org.springframework.web.util.DisconnectedClientHelper;
     import org.springframework.web.util.HtmlUtils;
     
     /**
    @@ -61,19 +59,6 @@
      */
     public abstract class AbstractErrorWebExceptionHandler implements ErrorWebExceptionHandler, InitializingBean {
     
    -	/**
    -	 * Currently duplicated from Spring Web's DisconnectedClientHelper.
    -	 */
    -	private static final Set DISCONNECTED_CLIENT_EXCEPTIONS;
    -
    -	static {
    -		Set exceptions = new HashSet<>();
    -		exceptions.add("ClientAbortException");
    -		exceptions.add("EOFException");
    -		exceptions.add("EofException");
    -		DISCONNECTED_CLIENT_EXCEPTIONS = Collections.unmodifiableSet(exceptions);
    -	}
    -
     	private static final Log logger = HttpLogging.forLogName(AbstractErrorWebExceptionHandler.class);
     
     	private final ApplicationContext applicationContext;
    @@ -305,13 +290,7 @@ public Mono handle(ServerWebExchange exchange, Throwable throwable) {
     	}
     
     	private boolean isDisconnectedClientError(Throwable ex) {
    -		return DISCONNECTED_CLIENT_EXCEPTIONS.contains(ex.getClass().getSimpleName())
    -				|| isDisconnectedClientErrorMessage(NestedExceptionUtils.getMostSpecificCause(ex).getMessage());
    -	}
    -
    -	private boolean isDisconnectedClientErrorMessage(String message) {
    -		message = (message != null) ? message.toLowerCase() : "";
    -		return (message.contains("broken pipe") || message.contains("connection reset by peer"));
    +		return DisconnectedClientHelper.isClientDisconnectedException(ex);
     	}
     
     	/**
    diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerTests.java
    index a704411e835f..8662c71bedb4 100644
    --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerTests.java
    +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerTests.java
    @@ -33,12 +33,10 @@
     import org.springframework.http.codec.ServerCodecConfigurer;
     import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
     import org.springframework.mock.web.server.MockServerWebExchange;
    -import org.springframework.test.util.ReflectionTestUtils;
     import org.springframework.web.reactive.function.server.ServerRequest;
     import org.springframework.web.reactive.result.view.View;
     import org.springframework.web.reactive.result.view.ViewResolver;
     import org.springframework.web.server.ServerWebExchange;
    -import org.springframework.web.util.DisconnectedClientHelper;
     
     import static org.assertj.core.api.Assertions.assertThat;
     import static org.mockito.ArgumentMatchers.any;
    @@ -54,14 +52,6 @@
      */
     class DefaultErrorWebExceptionHandlerTests {
     
    -	@Test
    -	void disconnectedClientExceptionsMatchesFramework() {
    -		Object errorHandlers = ReflectionTestUtils.getField(AbstractErrorWebExceptionHandler.class,
    -				"DISCONNECTED_CLIENT_EXCEPTIONS");
    -		Object webHandlers = ReflectionTestUtils.getField(DisconnectedClientHelper.class, "EXCEPTION_TYPE_NAMES");
    -		assertThat(errorHandlers).isNotNull().isEqualTo(webHandlers);
    -	}
    -
     	@Test
     	void nonStandardErrorStatusCodeShouldNotFail() {
     		ErrorAttributes errorAttributes = mock(ErrorAttributes.class);
    
    From 7fb0f52d7fcf9508aa01e60c27b310d144cdb743 Mon Sep 17 00:00:00 2001
    From: Andy Wilkinson 
    Date: Wed, 6 Dec 2023 20:20:59 +0000
    Subject: [PATCH 0852/1215] Start building against Micrometer 1.12.1 snapshots
    
    See gh-38693
    ---
     spring-boot-project/spring-boot-dependencies/build.gradle | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle
    index 661c6a1618c3..ca2daf157c2d 100644
    --- a/spring-boot-project/spring-boot-dependencies/build.gradle
    +++ b/spring-boot-project/spring-boot-dependencies/build.gradle
    @@ -995,7 +995,7 @@ bom {
     			]
     		}
     	}
    -	library("Micrometer", "1.12.0") {
    +	library("Micrometer", "1.12.1-SNAPSHOT") {
     		considerSnapshots()
     		group("io.micrometer") {
     			modules = [
    
    From 2a839788cce4a8a0497ce9ae127c2349ba5f2ec1 Mon Sep 17 00:00:00 2001
    From: Andy Wilkinson 
    Date: Wed, 6 Dec 2023 20:21:04 +0000
    Subject: [PATCH 0853/1215] Start building against Micrometer Tracing 1.2.1
     snapshots
    
    See gh-38694
    ---
     spring-boot-project/spring-boot-dependencies/build.gradle | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle
    index ca2daf157c2d..902f6bcfd1f7 100644
    --- a/spring-boot-project/spring-boot-dependencies/build.gradle
    +++ b/spring-boot-project/spring-boot-dependencies/build.gradle
    @@ -1008,7 +1008,7 @@ bom {
     			]
     		}
     	}
    -	library("Micrometer Tracing", "1.2.0") {
    +	library("Micrometer Tracing", "1.2.1-SNAPSHOT") {
     		considerSnapshots()
     		calendarName = "Tracing"
     		group("io.micrometer") {
    
    From 4fc2082972740c36d29711aea25f541edbe21f5b Mon Sep 17 00:00:00 2001
    From: Andy Wilkinson 
    Date: Wed, 6 Dec 2023 20:21:09 +0000
    Subject: [PATCH 0854/1215] Start building against Reactor Bom 2023.0.1
     snapshots
    
    See gh-38695
    ---
     spring-boot-project/spring-boot-dependencies/build.gradle | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle
    index 902f6bcfd1f7..23cb03af4fe5 100644
    --- a/spring-boot-project/spring-boot-dependencies/build.gradle
    +++ b/spring-boot-project/spring-boot-dependencies/build.gradle
    @@ -1323,7 +1323,7 @@ bom {
     			]
     		}
     	}
    -	library("Reactor Bom", "2023.0.0") {
    +	library("Reactor Bom", "2023.0.1-SNAPSHOT") {
     		considerSnapshots()
     		calendarName = "Reactor"
     		group("io.projectreactor") {
    
    From 5e4073999708fd138f77b4d1673d60c71623a503 Mon Sep 17 00:00:00 2001
    From: Andy Wilkinson 
    Date: Wed, 6 Dec 2023 20:21:14 +0000
    Subject: [PATCH 0855/1215] Start building against Spring Authorization Server
     1.2.1 snapshots
    
    See gh-38696
    ---
     spring-boot-project/spring-boot-dependencies/build.gradle | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle
    index 23cb03af4fe5..e90f31a64203 100644
    --- a/spring-boot-project/spring-boot-dependencies/build.gradle
    +++ b/spring-boot-project/spring-boot-dependencies/build.gradle
    @@ -1499,7 +1499,7 @@ bom {
     			]
     		}
     	}
    -	library("Spring Authorization Server", "1.2.0") {
    +	library("Spring Authorization Server", "1.2.1-SNAPSHOT") {
     		considerSnapshots()
     		group("org.springframework.security") {
     			modules = [
    
    From 174813c34131fe55b3750a142ea5fb93d81b9b69 Mon Sep 17 00:00:00 2001
    From: Andy Wilkinson 
    Date: Wed, 6 Dec 2023 20:21:19 +0000
    Subject: [PATCH 0856/1215] Start building against Spring Data Bom 2023.1.1
     snapshots
    
    See gh-38697
    ---
     spring-boot-project/spring-boot-dependencies/build.gradle | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle
    index e90f31a64203..458262918008 100644
    --- a/spring-boot-project/spring-boot-dependencies/build.gradle
    +++ b/spring-boot-project/spring-boot-dependencies/build.gradle
    @@ -1515,7 +1515,7 @@ bom {
     			]
     		}
     	}
    -	library("Spring Data Bom", "2023.1.0") {
    +	library("Spring Data Bom", "2023.1.1-SNAPSHOT") {
     		considerSnapshots()
     		calendarName = "Spring Data Release"
     		group("org.springframework.data") {
    
    From da4f2a4679067b487595cfb5ff8e288124e39e02 Mon Sep 17 00:00:00 2001
    From: Andy Wilkinson 
    Date: Wed, 6 Dec 2023 20:21:24 +0000
    Subject: [PATCH 0857/1215] Start building against Spring Integration 6.2.1
     snapshots
    
    See gh-38698
    ---
     spring-boot-project/spring-boot-dependencies/build.gradle | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle
    index 458262918008..57b21c4acfff 100644
    --- a/spring-boot-project/spring-boot-dependencies/build.gradle
    +++ b/spring-boot-project/spring-boot-dependencies/build.gradle
    @@ -1550,7 +1550,7 @@ bom {
     			]
     		}
     	}
    -	library("Spring Integration", "6.2.0") {
    +	library("Spring Integration", "6.2.1-SNAPSHOT") {
     		considerSnapshots()
     		group("org.springframework.integration") {
     			imports = [
    
    From 91efe9396b7e98000cf5871c2d080747eaef04ab Mon Sep 17 00:00:00 2001
    From: Andy Wilkinson 
    Date: Wed, 6 Dec 2023 20:21:29 +0000
    Subject: [PATCH 0858/1215] Start building against Spring LDAP 3.2.1 snapshots
    
    See gh-38699
    ---
     spring-boot-project/spring-boot-dependencies/build.gradle | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle
    index 57b21c4acfff..be82e12da9f1 100644
    --- a/spring-boot-project/spring-boot-dependencies/build.gradle
    +++ b/spring-boot-project/spring-boot-dependencies/build.gradle
    @@ -1567,7 +1567,7 @@ bom {
     			]
     		}
     	}
    -	library("Spring LDAP", "3.2.0") {
    +	library("Spring LDAP", "3.2.1-SNAPSHOT") {
     		considerSnapshots()
     		group("org.springframework.ldap") {
     			modules = [
    
    From fc1a5033e829d903fd2e37989918b1ea202508e4 Mon Sep 17 00:00:00 2001
    From: Andy Wilkinson 
    Date: Wed, 6 Dec 2023 20:21:33 +0000
    Subject: [PATCH 0859/1215] Start building against Spring Security 6.2.1
     snapshots
    
    See gh-38700
    ---
     spring-boot-project/spring-boot-dependencies/build.gradle | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle
    index be82e12da9f1..52926c952c8a 100644
    --- a/spring-boot-project/spring-boot-dependencies/build.gradle
    +++ b/spring-boot-project/spring-boot-dependencies/build.gradle
    @@ -1604,7 +1604,7 @@ bom {
     			]
     		}
     	}
    -	library("Spring Security", "6.2.0") {
    +	library("Spring Security", "6.2.1-SNAPSHOT") {
     		considerSnapshots()
     		group("org.springframework.security") {
     			imports = [
    
    From 847daf484c9833ab0af25ccf79ce8203a8006361 Mon Sep 17 00:00:00 2001
    From: Phillip Webb 
    Date: Wed, 6 Dec 2023 15:57:44 -0800
    Subject: [PATCH 0860/1215] Fix JarUrlTests
    
    Fix `JarUrlTests` to use the jarFile rather than temp.
    ---
     .../boot/loader/net/protocol/jar/JarUrlTests.java  | 14 +++++++-------
     1 file changed, 7 insertions(+), 7 deletions(-)
    
    diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlTests.java
    index 082550058e60..e35db7f04f0e 100644
    --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlTests.java
    +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlTests.java
    @@ -44,43 +44,43 @@ class JarUrlTests {
     	@BeforeEach
     	void setup() throws MalformedURLException {
     		this.jarFile = new File(this.temp, "my.jar");
    -		this.jarFileUrlPath = this.temp.toURI().toURL().toString().substring("file:".length());
    +		this.jarFileUrlPath = this.jarFile.toURI().toURL().toString().substring("file:".length()).replace("!", "%21");
     	}
     
     	@Test
     	void createWithFileReturnsUrl() {
    -		URL url = JarUrl.create(this.temp);
    +		URL url = JarUrl.create(this.jarFile);
     		assertThat(url).hasToString("jar:file:%s!/".formatted(this.jarFileUrlPath));
     	}
     
     	@Test
     	void createWithFileAndEntryReturnsUrl() {
     		JarEntry entry = new JarEntry("lib.jar");
    -		URL url = JarUrl.create(this.temp, entry);
    +		URL url = JarUrl.create(this.jarFile, entry);
     		assertThat(url).hasToString("jar:nested:%s/!lib.jar!/".formatted(this.jarFileUrlPath));
     	}
     
     	@Test
     	void createWithFileAndNullEntryReturnsUrl() {
    -		URL url = JarUrl.create(this.temp, (JarEntry) null);
    +		URL url = JarUrl.create(this.jarFile, (JarEntry) null);
     		assertThat(url).hasToString("jar:file:%s!/".formatted(this.jarFileUrlPath));
     	}
     
     	@Test
     	void createWithFileAndNameReturnsUrl() {
    -		URL url = JarUrl.create(this.temp, "lib.jar");
    +		URL url = JarUrl.create(this.jarFile, "lib.jar");
     		assertThat(url).hasToString("jar:nested:%s/!lib.jar!/".formatted(this.jarFileUrlPath));
     	}
     
     	@Test
     	void createWithFileAndNullNameReturnsUrl() {
    -		URL url = JarUrl.create(this.temp, (String) null);
    +		URL url = JarUrl.create(this.jarFile, (String) null);
     		assertThat(url).hasToString("jar:file:%s!/".formatted(this.jarFileUrlPath));
     	}
     
     	@Test
     	void createWithFileNameAndPathReturnsUrl() {
    -		URL url = JarUrl.create(this.temp, "lib.jar", "com/example/My.class");
    +		URL url = JarUrl.create(this.jarFile, "lib.jar", "com/example/My.class");
     		assertThat(url).hasToString("jar:nested:%s/!lib.jar!/com/example/My.class".formatted(this.jarFileUrlPath));
     	}
     
    
    From 359a6cb5bb99d8b11fbe3a461a2a2ef678aed04e Mon Sep 17 00:00:00 2001
    From: Phillip Webb 
    Date: Wed, 6 Dec 2023 15:57:50 -0800
    Subject: [PATCH 0861/1215] Use encoded version of path for jar URLs
    
    Update `JarUrl` so that the encoded version of the path is used.
    This allows jars to placed in directories with `#` or `!` in the
    name.
    
    Fixes gh-38660
    ---
     .../boot/loader/net/protocol/jar/JarUrl.java       |  2 +-
     .../net/protocol/jar/JarUrlConnectionTests.java    | 14 ++++++++++++++
     .../boot/loader/net/protocol/jar/JarUrlTests.java  | 12 ++++++++++++
     3 files changed, 27 insertions(+), 1 deletion(-)
    
    diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrl.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrl.java
    index 1e40ced32f1f..ca2230d36d7b 100644
    --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrl.java
    +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrl.java
    @@ -79,7 +79,7 @@ public static URL create(File file, String nestedEntryName, String path) {
     	}
     
     	private static String getJarReference(File file, String nestedEntryName) {
    -		String jarFilePath = file.toURI().getPath();
    +		String jarFilePath = file.toURI().getRawPath().replace("!", "%21");
     		return (nestedEntryName != null) ? "nested:" + jarFilePath + "/!" + nestedEntryName : "file:" + jarFilePath;
     	}
     
    diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java
    index c9553c731379..83f522553332 100644
    --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java
    +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnectionTests.java
    @@ -512,6 +512,20 @@ void getLastModifiedHeaderReturnsFileModifiedTime() throws IOException {
     		}
     	}
     
    +	@Test
    +	void getJarFileWhenInFolderWithEncodedCharsReturnsJarFile() throws Exception {
    +		this.temp = new File(this.temp, "te#st");
    +		this.temp.mkdirs();
    +		this.file = new File(this.temp, "test.jar");
    +		this.url = JarUrl.create(this.file, "nested.jar");
    +		assertThat(this.url.toString()).contains("te%23st");
    +		TestJar.create(this.file);
    +		JarUrlConnection connection = JarUrlConnection.open(this.url);
    +		JarFile jarFile = connection.getJarFile();
    +		assertThat(jarFile).isNotNull();
    +		assertThat(jarFile.getEntry("3.dat")).isNotNull();
    +	}
    +
     	private long withoutNanos(long epochMilli) {
     		return Instant.ofEpochMilli(epochMilli).with(ChronoField.NANO_OF_SECOND, 0).toEpochMilli();
     	}
    diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlTests.java
    index e35db7f04f0e..7ca3fb683995 100644
    --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlTests.java
    +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/jar/JarUrlTests.java
    @@ -25,6 +25,8 @@
     import org.junit.jupiter.api.Test;
     import org.junit.jupiter.api.io.TempDir;
     
    +import org.springframework.boot.loader.net.util.UrlDecoder;
    +
     import static org.assertj.core.api.Assertions.assertThat;
     
     /**
    @@ -84,4 +86,14 @@ void createWithFileNameAndPathReturnsUrl() {
     		assertThat(url).hasToString("jar:nested:%s/!lib.jar!/com/example/My.class".formatted(this.jarFileUrlPath));
     	}
     
    +	@Test
    +	void createWithReservedCharsInName() throws Exception {
    +		String badFolderName = "foo#bar!/baz/!oof";
    +		this.temp = new File(this.temp, badFolderName);
    +		setup();
    +		URL url = JarUrl.create(this.jarFile, "lib.jar", "com/example/My.class");
    +		assertThat(url).hasToString("jar:nested:%s/!lib.jar!/com/example/My.class".formatted(this.jarFileUrlPath));
    +		assertThat(UrlDecoder.decode(url.toString())).contains(badFolderName);
    +	}
    +
     }
    
    From e6970243eeddc13fd54bb8afd5c9c1e8bec1ad13 Mon Sep 17 00:00:00 2001
    From: Andy Wilkinson 
    Date: Thu, 7 Dec 2023 11:06:37 +0000
    Subject: [PATCH 0862/1215] Retry read on ClosedByInterruptException
    
    In gh-38154, we started handling ClosedByInterruptException. The
    FileChannel was repaired by recreating it and then the exception was
    rethrown. This allowed other threads to use the channel that had been
    read by an interrupted thread while allowing that interruption to
    continue.
    
    This approach has proven to be insufficient as there are scenarios
    where the read needs to succeed on the interrupted thread. This
    commit updates the handling of ClosedByInterruptException so that
    this is the case. The FileChannel is recreated as before but the
    thread's interrupted flag is now cleared before retrying the read.
    The flag is then reinstated so that any subsequent actions that
    should fail due to the interruption will do so.
    
    We could clear and reinstate the interrupted flag before the first
    read, rather than catching ClosedByInterruptException. This approach
    was rejected as it will have an impact on the performance of the
    happy path where the thread hasn't been interrupted.
    
    Fixes gh-38611
    ---
     .../boot/loader/zip/FileChannelDataBlock.java | 31 +++++++++++++------
     .../loader/zip/FileChannelDataBlockTests.java | 21 +++----------
     2 files changed, 27 insertions(+), 25 deletions(-)
    
    diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/FileChannelDataBlock.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/FileChannelDataBlock.java
    index 0346a87d4d0e..788841ea308b 100644
    --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/FileChannelDataBlock.java
    +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/FileChannelDataBlock.java
    @@ -179,15 +179,7 @@ static class ManagedFileChannel {
     		int read(ByteBuffer dst, long position) throws IOException {
     			synchronized (this.lock) {
     				if (position < this.bufferPosition || position >= this.bufferPosition + this.bufferSize) {
    -					this.buffer.clear();
    -					try {
    -						this.bufferSize = this.fileChannel.read(this.buffer, position);
    -					}
    -					catch (ClosedByInterruptException ex) {
    -						repairFileChannel();
    -						throw ex;
    -					}
    -					this.bufferPosition = position;
    +					fillBuffer(position);
     				}
     				if (this.bufferSize <= 0) {
     					return this.bufferSize;
    @@ -200,6 +192,27 @@ int read(ByteBuffer dst, long position) throws IOException {
     			}
     		}
     
    +		private void fillBuffer(long position) throws IOException {
    +			for (int i = 0; i < 10; i++) {
    +				boolean interrupted = (i != 0) ? Thread.interrupted() : false;
    +				try {
    +					this.buffer.clear();
    +					this.bufferSize = this.fileChannel.read(this.buffer, position);
    +					this.bufferPosition = position;
    +					return;
    +				}
    +				catch (ClosedByInterruptException ex) {
    +					repairFileChannel();
    +				}
    +				finally {
    +					if (interrupted) {
    +						Thread.currentThread().interrupt();
    +					}
    +				}
    +			}
    +			throw new ClosedByInterruptException();
    +		}
    +
     		private void repairFileChannel() throws IOException {
     			if (tracker != null) {
     				tracker.closedFileChannel(this.path, this.fileChannel);
    diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockTests.java
    index df015ff68d55..b5abefc6aada 100644
    --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockTests.java
    +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockTests.java
    @@ -19,11 +19,9 @@
     import java.io.File;
     import java.io.IOException;
     import java.nio.ByteBuffer;
    -import java.nio.channels.ClosedByInterruptException;
     import java.nio.channels.FileChannel;
     import java.nio.file.Files;
     import java.nio.file.Path;
    -import java.util.concurrent.atomic.AtomicReference;
     
     import org.junit.jupiter.api.AfterEach;
     import org.junit.jupiter.api.BeforeEach;
    @@ -77,25 +75,16 @@ void readReadsFile() throws IOException {
     	}
     
     	@Test
    -	void readReadsFileWhenAnotherThreadHasBeenInterrupted() throws IOException, InterruptedException {
    +	void readReadsFileWhenThreadHasBeenInterrupted() throws IOException {
     		try (FileChannelDataBlock block = createAndOpenBlock()) {
     			ByteBuffer buffer = ByteBuffer.allocate(CONTENT.length);
    -			AtomicReference failure = new AtomicReference<>();
    -			Thread thread = new Thread(() -> {
    -				Thread.currentThread().interrupt();
    -				try {
    -					block.read(ByteBuffer.allocate(CONTENT.length), 0);
    -				}
    -				catch (IOException ex) {
    -					failure.set(ex);
    -				}
    -			});
    -			thread.start();
    -			thread.join();
    -			assertThat(failure.get()).isInstanceOf(ClosedByInterruptException.class);
    +			Thread.currentThread().interrupt();
     			assertThat(block.read(buffer, 0)).isEqualTo(6);
     			assertThat(buffer.array()).containsExactly(CONTENT);
     		}
    +		finally {
    +			Thread.interrupted();
    +		}
     	}
     
     	@Test
    
    From beba1f176a93f80c179e7d0970df93ce910f8a9f Mon Sep 17 00:00:00 2001
    From: Andy Wilkinson 
    Date: Mon, 11 Dec 2023 10:45:15 +0000
    Subject: [PATCH 0863/1215] Do not enable WebFlux security unless other
     configuration is active
    
    Following the changes in gh-37504, the reactive resource server
    auto-configuration could enable WebFlux security in situations where
    it was otherwise in active. This could then result in an application
    failing to start as no authentication manager is available.
    
    This commit updates the configurations that enable WebFlux security
    so that they fully back off unless their related configurations are
    active. Previously, only the configuration of the
    SecurityWebFilterChain would back off. This has been expanded to
    cover `@EnableWebFluxSecurity` as well. This has required splitting
    the configuration classes up so that the condition evaluation order
    can be controlled more precisely. We need to ensure that the JWT
    decoder bean or the opaque token introspector bean has been defined
    before evaluation of the conditions for `@EnableWebFluxSecurity`.
    Without this control, the import through `@EnableWebFluxSecurity` in
    one location where the conditions do not matchcan prevent a
    successful import in another where they do.
    
    Fixes gh-38713
    ---
     ...OAuth2ResourceServerAutoConfiguration.java |  6 +++--
     ...tiveOAuth2ResourceServerConfiguration.java | 26 ++++++++++++++-----
     ...eOAuth2ResourceServerJwkConfiguration.java |  2 +-
     ...esourceServerOpaqueTokenConfiguration.java |  2 +-
     ...2ResourceServerAutoConfigurationTests.java | 11 +++++++-
     5 files changed, 35 insertions(+), 12 deletions(-)
    
    diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java
    index 78f28f94b813..92c02ca9d43b 100644
    --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java
    +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2012-2022 the original author or authors.
    + * Copyright 2012-2023 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.
    @@ -40,7 +40,9 @@
     @ConditionalOnClass({ EnableWebFluxSecurity.class })
     @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
     @Import({ ReactiveOAuth2ResourceServerConfiguration.JwtConfiguration.class,
    -		ReactiveOAuth2ResourceServerConfiguration.OpaqueTokenConfiguration.class })
    +		ReactiveOAuth2ResourceServerConfiguration.OpaqueTokenConfiguration.class,
    +		ReactiveOAuth2ResourceServerConfiguration.JwtWebSecurityConfiguration.class,
    +		ReactiveOAuth2ResourceServerConfiguration.OpaqueTokenWebSecurityConfiguration.class })
     public class ReactiveOAuth2ResourceServerAutoConfiguration {
     
     }
    diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerConfiguration.java
    index d4f5388f041d..6cedf7e711ca 100644
    --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerConfiguration.java
    +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerConfiguration.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2012-2022 the original author or authors.
    + * Copyright 2012-2023 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.
    @@ -24,8 +24,8 @@
     import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
     
     /**
    - * Configuration classes for OAuth2 Resource Server These should be {@code @Import} in a
    - * regular auto-configuration class to guarantee their order of execution.
    + * Configuration classes for OAuth2 Resource Server. These should be {@code @Import}ed in
    + * a regular auto-configuration class to guarantee their order of execution.
      *
      * @author Madhura Bhave
      */
    @@ -33,18 +33,30 @@ class ReactiveOAuth2ResourceServerConfiguration {
     
     	@Configuration(proxyBeanMethods = false)
     	@ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveJwtDecoder.class })
    -	@Import({ ReactiveOAuth2ResourceServerJwkConfiguration.JwtConfiguration.class,
    -			ReactiveOAuth2ResourceServerJwkConfiguration.WebSecurityConfiguration.class })
    +	@Import(ReactiveOAuth2ResourceServerJwkConfiguration.JwtConfiguration.class)
     	static class JwtConfiguration {
     
     	}
     
    +	@Configuration(proxyBeanMethods = false)
    +	@ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveJwtDecoder.class })
    +	@Import(ReactiveOAuth2ResourceServerJwkConfiguration.WebSecurityConfiguration.class)
    +	static class JwtWebSecurityConfiguration {
    +
    +	}
    +
     	@Configuration(proxyBeanMethods = false)
     	@ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveOpaqueTokenIntrospector.class })
    -	@Import({ ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.OpaqueTokenIntrospectionClientConfiguration.class,
    -			ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.WebSecurityConfiguration.class })
    +	@Import(ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.OpaqueTokenIntrospectionClientConfiguration.class)
     	static class OpaqueTokenConfiguration {
     
     	}
     
    +	@Configuration(proxyBeanMethods = false)
    +	@ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveOpaqueTokenIntrospector.class })
    +	@Import(ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.WebSecurityConfiguration.class)
    +	static class OpaqueTokenWebSecurityConfiguration {
    +
    +	}
    +
     }
    diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java
    index 31cb13aa60cc..0c10d879b977 100644
    --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java
    +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java
    @@ -164,11 +164,11 @@ SupplierReactiveJwtDecoder jwtDecoderByIssuerUri(
     	}
     
     	@Configuration(proxyBeanMethods = false)
    +	@ConditionalOnBean(ReactiveJwtDecoder.class)
     	@ConditionalOnMissingBean(SecurityWebFilterChain.class)
     	static class WebSecurityConfiguration {
     
     		@Bean
    -		@ConditionalOnBean(ReactiveJwtDecoder.class)
     		SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ReactiveJwtDecoder jwtDecoder) {
     			http.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated());
     			http.oauth2ResourceServer((server) -> customDecoder(server, jwtDecoder));
    diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java
    index dbeb778d8764..6612dfe703cd 100644
    --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java
    +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java
    @@ -56,10 +56,10 @@ SpringReactiveOpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2ResourceServ
     
     	@Configuration(proxyBeanMethods = false)
     	@ConditionalOnMissingBean(SecurityWebFilterChain.class)
    +	@ConditionalOnBean(ReactiveOpaqueTokenIntrospector.class)
     	static class WebSecurityConfiguration {
     
     		@Bean
    -		@ConditionalOnBean(ReactiveOpaqueTokenIntrospector.class)
     		SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
     			http.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated());
     			http.oauth2ResourceServer((resourceServer) -> resourceServer.opaqueToken(withDefaults()));
    diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java
    index 9583efcbc450..d60b395c1bb7 100644
    --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java
    +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java
    @@ -43,6 +43,8 @@
     import reactor.core.publisher.Mono;
     
     import org.springframework.boot.autoconfigure.AutoConfigurations;
    +import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener;
    +import org.springframework.boot.logging.LogLevel;
     import org.springframework.boot.test.context.FilteredClassLoader;
     import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext;
     import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
    @@ -73,6 +75,7 @@
     import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector;
     import org.springframework.security.web.server.MatcherSecurityWebFilterChain;
     import org.springframework.security.web.server.SecurityWebFilterChain;
    +import org.springframework.security.web.server.WebFilterChainProxy;
     import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
     import org.springframework.test.util.ReflectionTestUtils;
     import org.springframework.web.server.WebFilter;
    @@ -116,10 +119,16 @@ void cleanup() throws Exception {
     		}
     	}
     
    +	@Test
    +	void autoConfigurationDoesNotEnableWebSecurityWithoutJwtDecoderOrTokenIntrospector() {
    +		this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(WebFilterChainProxy.class));
    +	}
    +
     	@Test
     	void autoConfigurationShouldConfigureResourceServer() {
     		this.contextRunner
     			.withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com")
    +			.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
     			.run((context) -> {
     				assertThat(context).hasSingleBean(NimbusReactiveJwtDecoder.class);
     				assertFilterConfiguredWithJwtAuthenticationManager(context);
    @@ -385,7 +394,7 @@ void autoConfigurationWhenSecurityWebFilterChainConfigPresentShouldNotAddOne() {
     
     	@Test
     	void autoConfigurationWhenIntrospectionUriAvailableShouldConfigureIntrospectionClient() {
    -		this.contextRunner
    +		this.contextRunner.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
     			.withPropertyValues(
     					"spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com",
     					"spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id",
    
    From 0fe7d78732b54de599aa6c974a105653cb058be4 Mon Sep 17 00:00:00 2001
    From: Phillip Webb 
    Date: Mon, 11 Dec 2023 16:41:14 -0800
    Subject: [PATCH 0864/1215] Restore support for custom bind converters in
     collections
    
    Update the `beansConverterService` introduced in commit f4e05c91c74e
    so that it can also handle collection based conversions.
    
    Fixes gh-38734
    ---
     .../properties/ConversionServiceDeducer.java  |  2 +
     .../ConfigurationPropertiesTests.java         | 40 +++++++++++++++++++
     2 files changed, 42 insertions(+)
    
    diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java
    index 775680c794ae..a80535000f6f 100644
    --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java
    +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java
    @@ -29,6 +29,7 @@
     import org.springframework.core.convert.ConversionService;
     import org.springframework.core.convert.converter.Converter;
     import org.springframework.core.convert.converter.GenericConverter;
    +import org.springframework.core.convert.support.DefaultConversionService;
     import org.springframework.format.Formatter;
     import org.springframework.format.FormatterRegistry;
     import org.springframework.format.support.FormattingConversionService;
    @@ -63,6 +64,7 @@ private List getConversionServices(ConfigurableApplicationCon
     		ConverterBeans converterBeans = new ConverterBeans(applicationContext);
     		if (!converterBeans.isEmpty()) {
     			FormattingConversionService beansConverterService = new FormattingConversionService();
    +			DefaultConversionService.addCollectionConverters(beansConverterService);
     			converterBeans.addTo(beansConverterService);
     			conversionServices.add(beansConverterService);
     		}
    diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java
    index 2e2c22377472..ee5b68be1656 100644
    --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java
    +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java
    @@ -679,6 +679,20 @@ void loadWhenBeanFactoryConversionServiceAndConverterBeanCanUseConverterBean() {
     		assertThat(properties.getAlien().name).isEqualTo("rennaT flA");
     	}
     
    +	@Test // gh-38734
    +	void loadWhenBeanFactoryConversionServiceAndConverterBeanCanUseConverterBeanWithCollections() {
    +		DefaultConversionService conversionService = new DefaultConversionService();
    +		conversionService.addConverter(new PersonConverter());
    +		this.context.getBeanFactory().setConversionService(conversionService);
    +		load(new Class[] { AlienConverterConfiguration.class, PersonAndAliensProperties.class },
    +				"test.person=John Smith", "test.aliens=Alf Tanner,Gilbert");
    +		PersonAndAliensProperties properties = this.context.getBean(PersonAndAliensProperties.class);
    +		assertThat(properties.getPerson().firstName).isEqualTo("John");
    +		assertThat(properties.getPerson().lastName).isEqualTo("Smith");
    +		assertThat(properties.getAliens().get(0).name).isEqualTo("rennaT flA");
    +		assertThat(properties.getAliens().get(1).name).isEqualTo("trebliG");
    +	}
    +
     	@Test
     	void loadWhenConfigurationConverterIsNotQualifiedShouldNotConvert() {
     		assertThatExceptionOfType(BeanCreationException.class)
    @@ -2090,6 +2104,32 @@ void setAlien(Alien alien) {
     
     	}
     
    +	@EnableConfigurationProperties
    +	@ConfigurationProperties(prefix = "test")
    +	static class PersonAndAliensProperties {
    +
    +		private Person person;
    +
    +		private List aliens;
    +
    +		Person getPerson() {
    +			return this.person;
    +		}
    +
    +		void setPerson(Person person) {
    +			this.person = person;
    +		}
    +
    +		List getAliens() {
    +			return this.aliens;
    +		}
    +
    +		void setAliens(List aliens) {
    +			this.aliens = aliens;
    +		}
    +
    +	}
    +
     	@EnableConfigurationProperties
     	@ConfigurationProperties(prefix = "sample")
     	static class MapWithNumericKeyProperties {
    
    From ff82b8d1c1db8020062d18bf9058a5d329669725 Mon Sep 17 00:00:00 2001
    From: Moritz Halbritter 
    Date: Mon, 11 Dec 2023 14:09:06 +0100
    Subject: [PATCH 0865/1215] Add auto-configuration for a no-op tracer
    
    This auto-configuration ensures, if Micrometer Tracing is on the
    classpath, that there is always a tracer. It backs off if there is
    already a tracer, for example contributed by the Brave or the Otel
    auto-configurations, which are run before.
    
    See gh-38568
    ---
     .../tracing/BraveAutoConfiguration.java       |  2 +-
     .../tracing/NoopTracerAutoConfiguration.java  | 44 +++++++++++
     .../OpenTelemetryAutoConfiguration.java       |  3 +-
     ...ot.autoconfigure.AutoConfiguration.imports |  1 +
     .../NoopTracerAutoConfigurationTests.java     | 77 +++++++++++++++++++
     5 files changed, 125 insertions(+), 2 deletions(-)
     create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/NoopTracerAutoConfiguration.java
     create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/NoopTracerAutoConfigurationTests.java
    
    diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfiguration.java
    index 9e1c8a6bd413..b1fbb798c6da 100644
    --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfiguration.java
    +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfiguration.java
    @@ -62,7 +62,7 @@
      * @author Jonatan Ivanov
      * @since 3.0.0
      */
    -@AutoConfiguration(before = MicrometerTracingAutoConfiguration.class)
    +@AutoConfiguration(before = { MicrometerTracingAutoConfiguration.class, NoopTracerAutoConfiguration.class })
     @ConditionalOnClass({ Tracer.class, BraveTracer.class })
     @EnableConfigurationProperties(TracingProperties.class)
     @Import({ BravePropagationConfigurations.PropagationWithoutBaggage.class,
    diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/NoopTracerAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/NoopTracerAutoConfiguration.java
    new file mode 100644
    index 000000000000..ee2f65f8025a
    --- /dev/null
    +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/NoopTracerAutoConfiguration.java
    @@ -0,0 +1,44 @@
    +/*
    + * Copyright 2012-2023 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.actuate.autoconfigure.tracing;
    +
    +import io.micrometer.tracing.Tracer;
    +
    +import org.springframework.boot.autoconfigure.AutoConfiguration;
    +import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
    +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
    +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    +import org.springframework.context.annotation.Bean;
    +
    +/**
    + * {@link EnableAutoConfiguration Auto-configuration} for a no-op implementation of
    + * {@link Tracer}.
    + *
    + * @author Moritz Halbritter
    + * @since 3.2.1
    + */
    +@AutoConfiguration(before = MicrometerTracingAutoConfiguration.class)
    +@ConditionalOnClass(Tracer.class)
    +@ConditionalOnMissingBean(Tracer.class)
    +public class NoopTracerAutoConfiguration {
    +
    +	@Bean
    +	Tracer noopTracer() {
    +		return Tracer.NOOP;
    +	}
    +
    +}
    diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java
    index 9673ec0f25fe..6e5de4b51c6c 100644
    --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java
    +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java
    @@ -66,7 +66,8 @@
      * @author Yanming Zhou
      * @since 3.0.0
      */
    -@AutoConfiguration(value = "openTelemetryTracingAutoConfiguration", before = MicrometerTracingAutoConfiguration.class)
    +@AutoConfiguration(value = "openTelemetryTracingAutoConfiguration",
    +		before = { MicrometerTracingAutoConfiguration.class, NoopTracerAutoConfiguration.class })
     @ConditionalOnClass({ OtelTracer.class, SdkTracerProvider.class, OpenTelemetry.class })
     @EnableConfigurationProperties(TracingProperties.class)
     @Import({ OpenTelemetryPropagationConfigurations.PropagationWithoutBaggage.class,
    diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
    index f48e8e19f435..7801946776fc 100644
    --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
    +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
    @@ -104,6 +104,7 @@ org.springframework.boot.actuate.autoconfigure.startup.StartupEndpointAutoConfig
     org.springframework.boot.actuate.autoconfigure.system.DiskSpaceHealthContributorAutoConfiguration
     org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration
     org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration
    +org.springframework.boot.actuate.autoconfigure.tracing.NoopTracerAutoConfiguration
     org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryAutoConfiguration
     org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpAutoConfiguration
     org.springframework.boot.actuate.autoconfigure.tracing.prometheus.PrometheusExemplarsAutoConfiguration
    diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/NoopTracerAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/NoopTracerAutoConfigurationTests.java
    new file mode 100644
    index 000000000000..92af5942924c
    --- /dev/null
    +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/NoopTracerAutoConfigurationTests.java
    @@ -0,0 +1,77 @@
    +/*
    + * Copyright 2012-2023 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.actuate.autoconfigure.tracing;
    +
    +import io.micrometer.tracing.Tracer;
    +import org.junit.jupiter.api.Test;
    +
    +import org.springframework.boot.autoconfigure.AutoConfigurations;
    +import org.springframework.boot.test.context.FilteredClassLoader;
    +import org.springframework.boot.test.context.runner.ApplicationContextRunner;
    +import org.springframework.context.annotation.Bean;
    +import org.springframework.context.annotation.Configuration;
    +
    +import static org.assertj.core.api.Assertions.assertThat;
    +import static org.mockito.Mockito.mock;
    +
    +/**
    + * Tests for {@link NoopTracerAutoConfiguration}.
    + *
    + * @author Moritz Halbritter
    + */
    +class NoopTracerAutoConfigurationTests {
    +
    +	private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
    +		.withConfiguration(AutoConfigurations.of(NoopTracerAutoConfiguration.class));
    +
    +	@Test
    +	void shouldSupplyNoopTracer() {
    +		this.contextRunner.run((context) -> {
    +			assertThat(context).hasSingleBean(Tracer.class);
    +			Tracer tracer = context.getBean(Tracer.class);
    +			assertThat(tracer).isEqualTo(Tracer.NOOP);
    +		});
    +	}
    +
    +	@Test
    +	void shouldBackOffOnCustomTracer() {
    +		this.contextRunner.withUserConfiguration(CustomTracerConfiguration.class).run((context) -> {
    +			assertThat(context).hasSingleBean(Tracer.class);
    +			assertThat(context).hasBean("customTracer");
    +			Tracer tracer = context.getBean(Tracer.class);
    +			assertThat(tracer).isNotEqualTo(Tracer.NOOP);
    +		});
    +	}
    +
    +	@Test
    +	void shouldBackOffIfMicrometerTracingIsMissing() {
    +		this.contextRunner.withClassLoader(new FilteredClassLoader("io.micrometer.tracing"))
    +			.run((context) -> assertThat(context).doesNotHaveBean(Tracer.class));
    +
    +	}
    +
    +	@Configuration(proxyBeanMethods = false)
    +	private static class CustomTracerConfiguration {
    +
    +		@Bean
    +		Tracer customTracer() {
    +			return mock(Tracer.class);
    +		}
    +
    +	}
    +
    +}
    
    From 198dbb4a45554db18eaaadded81574a9a3f8378d Mon Sep 17 00:00:00 2001
    From: Moritz Halbritter 
    Date: Tue, 12 Dec 2023 11:13:50 +0100
    Subject: [PATCH 0866/1215] Auto-configure observatibility beans in sliced
     tests
    
    If @AutoConfigureObservability is applied to a sliced test, it
    auto-configures:
    
    - An in-memory MeterRegistry
    - A no-op Tracer
    - An ObservationRegistry
    
    Closes gh-38568
    ---
     .../src/docs/asciidoc/features/testing.adoc   | 11 ++++
     .../AutoConfigureObservability.java           | 15 +++--
     ...ability.AutoConfigureObservability.imports | 13 +++++
     ...reObservabilitySlicedIntegrationTests.java | 58 +++++++++++++++++++
     4 files changed, 93 insertions(+), 4 deletions(-)
     create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability.imports
     create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/observability/AutoConfigureObservabilitySlicedIntegrationTests.java
    
    diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc
    index d39d902b31bd..24bcf0d1ee44 100644
    --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc
    +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc
    @@ -255,6 +255,11 @@ If such test needs access to an `MBeanServer`, consider marking it dirty as well
     include::code:MyJmxTests[]
     
     
    +[[features.testing.spring-boot-applications.observations]]
    +==== Using Observations
    +If you annotate <> with `@AutoConfigureObservability`, it auto-configures an `ObservationRegistry`.
    +
    +
     
     [[features.testing.spring-boot-applications.metrics]]
     ==== Using Metrics
    @@ -262,6 +267,9 @@ Regardless of your classpath, meter registries, except the in-memory backed, are
     
     If you need to export metrics to a different backend as part of an integration test, annotate it with `@AutoConfigureObservability`.
     
    +If you annotate <> with `@AutoConfigureObservability`, it auto-configures an in-memory `MeterRegistry`.
    +Data exporting in sliced tests is not supported with the `@AutoConfigureObservability` annotation.
    +
     
     
     [[features.testing.spring-boot-applications.tracing]]
    @@ -272,6 +280,9 @@ If you need those components as part of an integration test, annotate the test w
     
     If you have created your own reporting components (e.g. a custom `SpanExporter` or `SpanHandler`) and you don't want them to be active in tests, you can use the `@ConditionalOnEnabledTracing` annotation to disable them.
     
    +If you annotate <> with `@AutoConfigureObservability`, it auto-configures a no-op `Tracer`.
    +Data exporting in sliced tests is not supported with the `@AutoConfigureObservability` annotation.
    +
     
     
     [[features.testing.spring-boot-applications.mocking-beans]]
    diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/observability/AutoConfigureObservability.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/observability/AutoConfigureObservability.java
    index fb16e8d1032e..746d0a747d90 100644
    --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/observability/AutoConfigureObservability.java
    +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/actuate/observability/AutoConfigureObservability.java
    @@ -23,9 +23,15 @@
     import java.lang.annotation.RetentionPolicy;
     import java.lang.annotation.Target;
     
    +import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
    +
     /**
      * Annotation that can be applied to a test class to enable auto-configuration for
      * observability.
    + * 

    + * If this annotation is applied to a sliced test, an in-memory {@code MeterRegistry}, a + * no-op {@code Tracer} and an {@code ObservationRegistry} is added to the application + * context. * * @author Moritz Halbritter * @since 3.0.0 @@ -34,17 +40,18 @@ @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited +@ImportAutoConfiguration public @interface AutoConfigureObservability { /** - * Whether metrics should be enabled in the test. - * @return whether metrics should be enabled in the test + * Whether metrics should be reported to external systems in the test. + * @return whether metrics should be reported to external systems in the test */ boolean metrics() default true; /** - * Whether tracing should be enabled in the test. - * @return whether tracing should be enabled in the test + * Whether traces should be reported to external systems in the test. + * @return whether traces should be reported to external systems in the test */ boolean tracing() default true; diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability.imports b/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability.imports new file mode 100644 index 000000000000..af374669f52f --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability.imports @@ -0,0 +1,13 @@ +# AutoConfigureObservability auto-configuration imports + +# Observation +org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration + +# Metrics +org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration +org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration +org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration + +# Tracing +org.springframework.boot.actuate.autoconfigure.tracing.NoopTracerAutoConfiguration +org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/observability/AutoConfigureObservabilitySlicedIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/observability/AutoConfigureObservabilitySlicedIntegrationTests.java new file mode 100644 index 000000000000..4b73857da05a --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/actuate/observability/AutoConfigureObservabilitySlicedIntegrationTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2023 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.test.autoconfigure.actuate.observability; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.Tracer; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.ApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AutoConfigureObservability} when used on a sliced test. + * + * @author Moritz Halbritter + */ +@WebMvcTest +@AutoConfigureObservability +class AutoConfigureObservabilitySlicedIntegrationTests { + + @Autowired + private ApplicationContext context; + + @Test + void shouldHaveTracer() { + assertThat(this.context.getBean(Tracer.class)).isEqualTo(Tracer.NOOP); + } + + @Test + void shouldHaveMeterRegistry() { + assertThat(this.context.getBean(MeterRegistry.class)).isInstanceOf(SimpleMeterRegistry.class); + } + + @Test + void shouldHaveObservationRegistry() { + assertThat(this.context.getBean(ObservationRegistry.class)).isNotNull(); + } + +} From 612bf95b05cd5288b0e21d57fcb62b1b0d242b3a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 12 Dec 2023 14:50:20 +0000 Subject: [PATCH 0867/1215] Adapt to changes in the locking model for closing an app context See gh-38666 --- .../boot/SpringApplicationShutdownHookTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationShutdownHookTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationShutdownHookTests.java index 5f2d5c1dfc06..bafe2688da89 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationShutdownHookTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationShutdownHookTests.java @@ -112,8 +112,8 @@ void runWhenContextIsBeingClosedInAnotherThreadWaitsUntilContextIsInactive() thr closing.await(); Thread shutdownThread = new Thread(shutdownHook); shutdownThread.start(); - // Shutdown thread should become blocked on monitor held by context thread - Awaitility.await().atMost(Duration.ofSeconds(30)).until(shutdownThread::getState, State.BLOCKED::equals); + // Shutdown thread should start waiting for context to become inactive + Awaitility.await().atMost(Duration.ofSeconds(30)).until(shutdownThread::getState, State.TIMED_WAITING::equals); // Allow context thread to proceed, unblocking shutdown thread proceedWithClose.countDown(); contextThread.join(); From e81d1226fefa99d51acf6d87af59338a055e3ccb Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 12 Dec 2023 16:26:25 +0100 Subject: [PATCH 0868/1215] Prevent integer overflow when checking disk space --- .../java/org/springframework/boot/loader/jar/JarFileTests.java | 2 +- .../org/springframework/boot/loader/zip/ZipContentTests.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java index b37a99183a72..27c30a0a7aa7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jar/JarFileTests.java @@ -582,7 +582,7 @@ void zip64JarThatExceedsZipEntryLimitCanBeRead() throws Exception { @Test void zip64JarThatExceedsZipSizeLimitCanBeRead() throws Exception { - Assumptions.assumeTrue(this.tempDir.getFreeSpace() > 6 * 1024 * 1024 * 1024, "Insufficient disk space"); + Assumptions.assumeTrue(this.tempDir.getFreeSpace() > 6L * 1024 * 1024 * 1024, "Insufficient disk space"); File zip64Jar = new File(this.tempDir, "zip64.jar"); File entry = new File(this.tempDir, "entry.dat"); CRC32 crc32 = new CRC32(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipContentTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipContentTests.java index fe0e8bd54263..5ae7fe82f255 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipContentTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipContentTests.java @@ -283,7 +283,7 @@ void openWhenZip64ThatExceedsZipEntryLimitOpensZip() throws Exception { @Test void openWhenZip64ThatExceedsZipSizeLimitOpensZip() throws Exception { - Assumptions.assumeTrue(this.tempDir.getFreeSpace() > 6 * 1024 * 1024 * 1024, "Insufficient disk space"); + Assumptions.assumeTrue(this.tempDir.getFreeSpace() > 6L * 1024 * 1024 * 1024, "Insufficient disk space"); File zip64File = new File(this.tempDir, "zip64.zip"); File entryFile = new File(this.tempDir, "entry.dat"); CRC32 crc32 = new CRC32(); From c50172d5c7ef282f375a53f8fe5c59de9de58311 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 13 Dec 2023 08:26:11 +0100 Subject: [PATCH 0869/1215] Undeprecate 'management.metrics.tags' Closes gh-38583 --- .../autoconfigure/metrics/MetricsProperties.java | 3 --- .../metrics/PropertiesMeterFilter.java | 1 - .../src/docs/asciidoc/actuator/metrics.adoc | 13 ++++++++++++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java index 2bafe29923f2..6ed7759cd14b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java @@ -25,7 +25,6 @@ import java.util.Map; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.boot.context.properties.NestedConfigurationProperty; /** @@ -79,8 +78,6 @@ public Map getEnable() { return this.enable; } - @Deprecated(since = "3.2.0", forRemoval = true) - @DeprecatedConfigurationProperty(replacement = "management.observations.key-values", since = "3.2.0") public Map getTags() { return this.tags; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilter.java index 6293056f58a4..bf506756478a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/PropertiesMeterFilter.java @@ -50,7 +50,6 @@ public class PropertiesMeterFilter implements MeterFilter { private final MeterFilter mapFilter; - @SuppressWarnings("removal") public PropertiesMeterFilter(MetricsProperties properties) { Assert.notNull(properties, "Properties must not be null"); this.properties = properties; diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc index 40cb14597f43..f31a807d8298 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc @@ -1111,8 +1111,19 @@ These use the global registry that is not Spring-managed. [[actuator.metrics.customizing.common-tags]] ==== Common Tags +Common tags are generally used for dimensional drill-down on the operating environment, such as host, instance, region, stack, and others. +Commons tags are applied to all meters and can be configured, as the following example shows: -You can configure common tags using the <>. +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + management: + metrics: + tags: + region: "us-east-1" + stack: "prod" +---- + +The preceding example adds `region` and `stack` tags to all meters with a value of `us-east-1` and `prod`, respectively. NOTE: The order of common tags is important if you use Graphite. As the order of common tags cannot be guaranteed by using this approach, Graphite users are advised to define a custom `MeterFilter` instead. From 964ccbb000d8cd81accf1cb5bacb40db525ec836 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 13 Dec 2023 11:17:08 +0000 Subject: [PATCH 0870/1215] Revert "Do not enable WebFlux security unless other configuration is active" This reverts commit beba1f176a93f80c179e7d0970df93ce910f8a9f. See gh-38713 --- ...OAuth2ResourceServerAutoConfiguration.java | 6 ++--- ...tiveOAuth2ResourceServerConfiguration.java | 26 +++++-------------- ...eOAuth2ResourceServerJwkConfiguration.java | 2 +- ...esourceServerOpaqueTokenConfiguration.java | 2 +- ...2ResourceServerAutoConfigurationTests.java | 11 +------- 5 files changed, 12 insertions(+), 35 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java index 92c02ca9d43b..78f28f94b813 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2022 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. @@ -40,9 +40,7 @@ @ConditionalOnClass({ EnableWebFluxSecurity.class }) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) @Import({ ReactiveOAuth2ResourceServerConfiguration.JwtConfiguration.class, - ReactiveOAuth2ResourceServerConfiguration.OpaqueTokenConfiguration.class, - ReactiveOAuth2ResourceServerConfiguration.JwtWebSecurityConfiguration.class, - ReactiveOAuth2ResourceServerConfiguration.OpaqueTokenWebSecurityConfiguration.class }) + ReactiveOAuth2ResourceServerConfiguration.OpaqueTokenConfiguration.class }) public class ReactiveOAuth2ResourceServerAutoConfiguration { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerConfiguration.java index 6cedf7e711ca..d4f5388f041d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2022 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. @@ -24,8 +24,8 @@ import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; /** - * Configuration classes for OAuth2 Resource Server. These should be {@code @Import}ed in - * a regular auto-configuration class to guarantee their order of execution. + * Configuration classes for OAuth2 Resource Server These should be {@code @Import} in a + * regular auto-configuration class to guarantee their order of execution. * * @author Madhura Bhave */ @@ -33,30 +33,18 @@ class ReactiveOAuth2ResourceServerConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveJwtDecoder.class }) - @Import(ReactiveOAuth2ResourceServerJwkConfiguration.JwtConfiguration.class) + @Import({ ReactiveOAuth2ResourceServerJwkConfiguration.JwtConfiguration.class, + ReactiveOAuth2ResourceServerJwkConfiguration.WebSecurityConfiguration.class }) static class JwtConfiguration { } - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveJwtDecoder.class }) - @Import(ReactiveOAuth2ResourceServerJwkConfiguration.WebSecurityConfiguration.class) - static class JwtWebSecurityConfiguration { - - } - @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveOpaqueTokenIntrospector.class }) - @Import(ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.OpaqueTokenIntrospectionClientConfiguration.class) + @Import({ ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.OpaqueTokenIntrospectionClientConfiguration.class, + ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.WebSecurityConfiguration.class }) static class OpaqueTokenConfiguration { } - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveOpaqueTokenIntrospector.class }) - @Import(ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.WebSecurityConfiguration.class) - static class OpaqueTokenWebSecurityConfiguration { - - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java index 0c10d879b977..31cb13aa60cc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java @@ -164,11 +164,11 @@ SupplierReactiveJwtDecoder jwtDecoderByIssuerUri( } @Configuration(proxyBeanMethods = false) - @ConditionalOnBean(ReactiveJwtDecoder.class) @ConditionalOnMissingBean(SecurityWebFilterChain.class) static class WebSecurityConfiguration { @Bean + @ConditionalOnBean(ReactiveJwtDecoder.class) SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ReactiveJwtDecoder jwtDecoder) { http.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated()); http.oauth2ResourceServer((server) -> customDecoder(server, jwtDecoder)); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java index 6612dfe703cd..dbeb778d8764 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java @@ -56,10 +56,10 @@ SpringReactiveOpaqueTokenIntrospector opaqueTokenIntrospector(OAuth2ResourceServ @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(SecurityWebFilterChain.class) - @ConditionalOnBean(ReactiveOpaqueTokenIntrospector.class) static class WebSecurityConfiguration { @Bean + @ConditionalOnBean(ReactiveOpaqueTokenIntrospector.class) SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http.authorizeExchange((exchanges) -> exchanges.anyExchange().authenticated()); http.oauth2ResourceServer((resourceServer) -> resourceServer.opaqueToken(withDefaults())); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java index d60b395c1bb7..9583efcbc450 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java @@ -43,8 +43,6 @@ import reactor.core.publisher.Mono; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; -import org.springframework.boot.logging.LogLevel; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; @@ -75,7 +73,6 @@ import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; import org.springframework.security.web.server.MatcherSecurityWebFilterChain; import org.springframework.security.web.server.SecurityWebFilterChain; -import org.springframework.security.web.server.WebFilterChainProxy; import org.springframework.security.web.server.authentication.AuthenticationWebFilter; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.server.WebFilter; @@ -119,16 +116,10 @@ void cleanup() throws Exception { } } - @Test - void autoConfigurationDoesNotEnableWebSecurityWithoutJwtDecoderOrTokenIntrospector() { - this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(WebFilterChainProxy.class)); - } - @Test void autoConfigurationShouldConfigureResourceServer() { this.contextRunner .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com") - .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) .run((context) -> { assertThat(context).hasSingleBean(NimbusReactiveJwtDecoder.class); assertFilterConfiguredWithJwtAuthenticationManager(context); @@ -394,7 +385,7 @@ void autoConfigurationWhenSecurityWebFilterChainConfigPresentShouldNotAddOne() { @Test void autoConfigurationWhenIntrospectionUriAvailableShouldConfigureIntrospectionClient() { - this.contextRunner.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) + this.contextRunner .withPropertyValues( "spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://check-token.com", "spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id", From afad358047c4fa9a0a9281d406e875279528a495 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 13 Dec 2023 12:44:04 +0000 Subject: [PATCH 0871/1215] Align reactive web security more closely with servlet web security There are some notable differences in the behavior of Spring Security's reactive and servlet-based web security. Notably, Servlet-based web security (`@EnableWebSecurity`) works without any authentication manager, rejecting requests as not authorized. By contrast reactive-based web security (`@EnableWebFluxSecurity`) fails to start up when there's no authentication manager, either provided directly as a bean or derived from a ReactiveUserDetailsService. There are also further differences at runtime where empty Monos from all ReactiveAuthenticationManagers results in an internal error and a 500 response whereas a similar situation in the servlet implementation results in a 401. Previously, to accommodate these differences in behavior, Spring Boot's auto-configuration would behave differently. In the Servlet case, web security would be enabled whenever the necessary dependencies were on the classpath. In the reactive case, web security would back off in the absence of an authentication manager to prevent a start up failure. While this difference is rooted in Spring Security, it is undesirable and something that we want to avoid Spring Boot users being exposed to where possible. Unfortunately, the situation is more likely to occur than before as ReactiveUserDetailsServiceAutoConfiguration now backs off more readily (gh-35338). This makes it more likely that the context will contain neither a reactive authetication manager not a reactive user details service. This commit reworks the auto-configurations related to reactive security. ReactiveSecurityAutoConfiguration will now auto-configure an "empty" reactive authentication manager that denies access through Mono.error in the absence of a ReactiveAuthenticationManager, ReactiveUserDetailsService, or SecurityWebFilterChain. The last of these is to allow for the situation where a filter chain has been defined with an authentication manager configured directly on it. This configuration of an authentication manager allows `@EnableWebFluxSecurity` to be auto-configured more readily, removing one of the differences between reactive- and Servlet-based security. Corresponding updates to the auto-configurations for reactive OAuth2 support have also been made. They no longer try to auto-configure `@EnableWebFluxSecurity`, relying instead upon ReactiveSecurityAutoConfiguration, which they are ordered before, to do that instead. Closes gh-38713 --- .../ReactiveOAuth2ClientConfigurations.java | 9 ----- ...eOAuth2ResourceServerJwkConfiguration.java | 9 ----- ...esourceServerOpaqueTokenConfiguration.java | 9 ----- .../ReactiveSecurityAutoConfiguration.java | 40 +++++++------------ ...2ResourceServerAutoConfigurationTests.java | 2 +- ...eactiveSecurityAutoConfigurationTests.java | 21 ++++++---- 6 files changed, 29 insertions(+), 61 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientConfigurations.java index 8d25fbc2e345..8eb3871b9377 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientConfigurations.java @@ -28,7 +28,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; @@ -38,7 +37,6 @@ import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository; import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; import org.springframework.security.web.server.SecurityWebFilterChain; -import org.springframework.security.web.server.WebFilterChainProxy; import static org.springframework.security.config.Customizer.withDefaults; @@ -94,13 +92,6 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { return http.build(); } - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(WebFilterChainProxy.class) - @EnableWebFluxSecurity - static class EnableWebFluxSecurityConfiguration { - - } - } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java index 31cb13aa60cc..5f5cba160eaa 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java @@ -35,7 +35,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity.OAuth2ResourceServerSpec; import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; @@ -50,7 +49,6 @@ import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder; import org.springframework.security.web.server.SecurityWebFilterChain; -import org.springframework.security.web.server.WebFilterChainProxy; import org.springframework.util.CollectionUtils; /** @@ -179,13 +177,6 @@ private void customDecoder(OAuth2ResourceServerSpec server, ReactiveJwtDecoder d server.jwt((jwt) -> jwt.jwtDecoder(decoder)); } - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(WebFilterChainProxy.class) - @EnableWebFluxSecurity - static class EnableWebFluxSecurityConfiguration { - - } - } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java index dbeb778d8764..f4d9614253e8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerOpaqueTokenConfiguration.java @@ -22,12 +22,10 @@ import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; import org.springframework.security.oauth2.server.resource.introspection.SpringReactiveOpaqueTokenIntrospector; import org.springframework.security.web.server.SecurityWebFilterChain; -import org.springframework.security.web.server.WebFilterChainProxy; import static org.springframework.security.config.Customizer.withDefaults; @@ -66,13 +64,6 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { return http.build(); } - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(WebFilterChainProxy.class) - @EnableWebFluxSecurity - static class EnableWebFluxSecurityConfiguration { - - } - } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfiguration.java index 5bcb151be46b..9e07d8f9b98a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfiguration.java @@ -17,21 +17,21 @@ package org.springframework.boot.autoconfigure.security.reactive; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.WebFilterChainProxy; import org.springframework.web.reactive.config.WebFluxConfigurer; @@ -52,33 +52,21 @@ @ConditionalOnClass({ Flux.class, EnableWebFluxSecurity.class, WebFilterChainProxy.class, WebFluxConfigurer.class }) public class ReactiveSecurityAutoConfiguration { - @Configuration(proxyBeanMethods = false) - @ConditionalOnMissingBean(WebFilterChainProxy.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) - @Conditional(EnableWebFluxSecurityCondition.class) - @EnableWebFluxSecurity - static class EnableWebFluxSecurityConfiguration { - - } - - static final class EnableWebFluxSecurityCondition extends AnyNestedCondition { - - EnableWebFluxSecurityCondition() { - super(ConfigurationPhase.REGISTER_BEAN); - } - - @ConditionalOnBean(ReactiveAuthenticationManager.class) - static final class ConditionalOnReactiveAuthenticationManagerBean { - - } - - @ConditionalOnBean(ReactiveUserDetailsService.class) - static final class ConditionalOnReactiveUserDetailsService { + @Configuration(proxyBeanMethods = false) + class SpringBootWebFluxSecurityConfiguration { + @Bean + @ConditionalOnMissingBean({ ReactiveAuthenticationManager.class, ReactiveUserDetailsService.class, + SecurityWebFilterChain.class }) + ReactiveAuthenticationManager denyAllAuthenticationManager() { + return (authentication) -> Mono.error(new UsernameNotFoundException(authentication.getName())); } - @ConditionalOnBean(SecurityWebFilterChain.class) - static final class ConditionalOnSecurityWebFilterChain { + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(WebFilterChainProxy.class) + @EnableWebFluxSecurity + static class EnableWebFluxSecurityConfiguration { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java index 9583efcbc450..e8165ee18916 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java @@ -740,6 +740,7 @@ private Consumer> audClaimValidator() { .isEqualTo("aud"); } + @EnableWebFluxSecurity static class TestConfig { @Bean @@ -781,7 +782,6 @@ ReactiveOpaqueTokenIntrospector decoder() { } - @EnableWebFluxSecurity @Configuration(proxyBeanMethods = false) static class SecurityWebFilterChainConfig { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java index dd5dd07ef32d..006f7b228f1a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveSecurityAutoConfigurationTests.java @@ -20,7 +20,6 @@ import reactor.core.publisher.Flux; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration.EnableWebFluxSecurityConfiguration; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -53,28 +52,36 @@ void backsOffWhenWebFilterChainProxyBeanPresent() { } @Test - void backsOffWhenReactiveAuthenticationManagerNotPresent() { + void autoConfiguresDenyAllReactiveAuthenticationManagerWhenNoAlternativeIsAvailable() { this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ReactiveSecurityAutoConfiguration.class) - .doesNotHaveBean(EnableWebFluxSecurityConfiguration.class)); + .hasBean("denyAllAuthenticationManager")); } @Test void enablesWebFluxSecurityWhenUserDetailsServiceIsPresent() { - this.contextRunner.withUserConfiguration(UserDetailsServiceConfiguration.class) - .run((context) -> assertThat(context).getBean(WebFilterChainProxy.class).isNotNull()); + this.contextRunner.withUserConfiguration(UserDetailsServiceConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(WebFilterChainProxy.class); + assertThat(context).doesNotHaveBean("denyAllAuthenticationManager"); + }); } @Test void enablesWebFluxSecurityWhenReactiveAuthenticationManagerIsPresent() { this.contextRunner .withBean(ReactiveAuthenticationManager.class, () -> mock(ReactiveAuthenticationManager.class)) - .run((context) -> assertThat(context).getBean(WebFilterChainProxy.class).isNotNull()); + .run((context) -> { + assertThat(context).hasSingleBean(WebFilterChainProxy.class); + assertThat(context).doesNotHaveBean("denyAllAuthenticationManager"); + }); } @Test void enablesWebFluxSecurityWhenSecurityWebFilterChainIsPresent() { this.contextRunner.withBean(SecurityWebFilterChain.class, () -> mock(SecurityWebFilterChain.class)) - .run((context) -> assertThat(context).getBean(WebFilterChainProxy.class).isNotNull()); + .run((context) -> { + assertThat(context).hasSingleBean(WebFilterChainProxy.class); + assertThat(context).doesNotHaveBean("denyAllAuthenticationManager"); + }); } @Test From b4a4e91238663ddbd9648a89474e7442bd7faaf6 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 13 Dec 2023 13:23:46 -0800 Subject: [PATCH 0872/1215] Update ZipString to deal with reads that do not return all data Refine the logic in `ZipString.hash` and `ZipString.compare` to deal with the fact a read operation may not return all available bytes. Fixes gh-38751 --- .../boot/loader/zip/ByteArrayDataBlock.java | 10 ++++ .../boot/loader/zip/ZipString.java | 46 +++++++++++-------- .../boot/loader/zip/ZipStringTests.java | 7 ++- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ByteArrayDataBlock.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ByteArrayDataBlock.java index d1a4f7fcf982..3c1d4b41389b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ByteArrayDataBlock.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ByteArrayDataBlock.java @@ -28,12 +28,19 @@ class ByteArrayDataBlock implements CloseableDataBlock { private final byte[] bytes; + private final int maxReadSize; + /** * Create a new {@link ByteArrayDataBlock} backed by the given bytes. * @param bytes the bytes to use */ ByteArrayDataBlock(byte... bytes) { + this(bytes, -1); + } + + ByteArrayDataBlock(byte[] bytes, int maxReadSize) { this.bytes = bytes; + this.maxReadSize = maxReadSize; } @Override @@ -49,6 +56,9 @@ public int read(ByteBuffer dst, long pos) throws IOException { private int read(ByteBuffer dst, int pos) { int remaining = dst.remaining(); int length = Math.min(this.bytes.length - pos, remaining); + if (this.maxReadSize > 0 && length > this.maxReadSize) { + length = this.maxReadSize; + } dst.put(this.bytes, pos, length); return length; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipString.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipString.java index 6ffc4d7d68eb..4533f45a51fc 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipString.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipString.java @@ -108,19 +108,15 @@ static int hash(ByteBuffer buffer, DataBlock dataBlock, long pos, int len, boole byte[] bytes = buffer.array(); int hash = 0; char lastChar = 0; + int codePointSize = 1; while (len > 0) { - int count = readInBuffer(dataBlock, pos, buffer, len); - len -= count; - pos += count; + int count = readInBuffer(dataBlock, pos, buffer, len, codePointSize); for (int byteIndex = 0; byteIndex < count;) { - int codePointSize = getCodePointSize(bytes, byteIndex); + codePointSize = getCodePointSize(bytes, byteIndex); if (!hasEnoughBytes(byteIndex, codePointSize, count)) { - pos--; - len++; break; } int codePoint = getCodePoint(bytes, byteIndex, codePointSize); - byteIndex += codePointSize; if (codePoint <= 0xFFFF) { lastChar = (char) (codePoint & 0xFFFF); hash = 31 * hash + lastChar; @@ -130,6 +126,10 @@ static int hash(ByteBuffer buffer, DataBlock dataBlock, long pos, int len, boole hash = 31 * hash + Character.highSurrogate(codePoint); hash = 31 * hash + Character.lowSurrogate(codePoint); } + byteIndex += codePointSize; + pos += codePointSize; + len -= codePointSize; + codePointSize = 1; } } hash = (addEndSlash && lastChar != '/') ? 31 * hash + '/' : hash; @@ -198,19 +198,15 @@ private static int compare(ByteBuffer buffer, DataBlock dataBlock, long pos, int int maxCharSequenceLength = (!addSlash) ? charSequence.length() : charSequence.length() + 1; int result = 0; byte[] bytes = buffer.array(); + int codePointSize = 1; while (len > 0) { - int count = readInBuffer(dataBlock, pos, buffer, len); - len -= count; - pos += count; + int count = readInBuffer(dataBlock, pos, buffer, len, codePointSize); for (int byteIndex = 0; byteIndex < count;) { - int codePointSize = getCodePointSize(bytes, byteIndex); + codePointSize = getCodePointSize(bytes, byteIndex); if (!hasEnoughBytes(byteIndex, codePointSize, count)) { - pos--; - len++; break; } int codePoint = getCodePoint(bytes, byteIndex, codePointSize); - result += codePointSize; if (codePoint <= 0xFFFF) { char ch = (char) (codePoint & 0xFFFF); if (charSequenceIndex >= maxCharSequenceLength @@ -230,10 +226,14 @@ private static int compare(ByteBuffer buffer, DataBlock dataBlock, long pos, int return -1; } } + byteIndex += codePointSize; + pos += codePointSize; + len -= codePointSize; + result += codePointSize; + codePointSize = 1; if (compareType == CompareType.STARTS_WITH && charSequenceIndex >= charSequence.length()) { return result; } - byteIndex += codePointSize; } } return (charSequenceIndex >= charSequence.length()) ? result : -1; @@ -273,16 +273,22 @@ static String readString(DataBlock data, long pos, long len) { } } - private static int readInBuffer(DataBlock dataBlock, long pos, ByteBuffer buffer, int maxLen) throws IOException { + private static int readInBuffer(DataBlock dataBlock, long pos, ByteBuffer buffer, int maxLen, int minLen) + throws IOException { buffer.clear(); if (buffer.remaining() > maxLen) { buffer.limit(maxLen); } - int count = dataBlock.read(buffer, pos); - if (count <= 0) { - throw new EOFException(); + int result = 0; + while (result < minLen) { + int count = dataBlock.read(buffer, pos); + if (count <= 0) { + throw new EOFException(); + } + result += count; + pos += count; } - return count; + return result; } private static int getCodePointSize(byte[] bytes, int i) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipStringTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipStringTests.java index d421c2514520..0716d65aac97 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipStringTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipStringTests.java @@ -86,7 +86,10 @@ void testHash(HashSourceType sourceType, String source, boolean addEndSlash, int case DATA_BLOCK -> { ByteArrayDataBlock dataBlock = new ByteArrayDataBlock(source.getBytes(StandardCharsets.UTF_8)); assertThat(ZipString.hash(null, dataBlock, 0, (int) dataBlock.size(), addEndSlash)).isEqualTo(expected); - + } + case SINGLE_BYTE_READ_DATA_BLOCK -> { + ByteArrayDataBlock dataBlock = new ByteArrayDataBlock(source.getBytes(StandardCharsets.UTF_8), 1); + assertThat(ZipString.hash(null, dataBlock, 0, (int) dataBlock.size(), addEndSlash)).isEqualTo(expected); } } } @@ -187,7 +190,7 @@ private AbstractIntegerAssert assertStartsWith(String source, CharSequence ch enum HashSourceType { - STRING, CHAR_SEQUENCE, DATA_BLOCK + STRING, CHAR_SEQUENCE, DATA_BLOCK, SINGLE_BYTE_READ_DATA_BLOCK } From 0dedccc1a1dae9bcba6e8ca09f479e92f58cbd73 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 09:41:52 +0000 Subject: [PATCH 0873/1215] Upgrade to Micrometer 1.12.1 Closes gh-38693 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 52926c952c8a..d2842aba0f77 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -995,7 +995,7 @@ bom { ] } } - library("Micrometer", "1.12.1-SNAPSHOT") { + library("Micrometer", "1.12.1") { considerSnapshots() group("io.micrometer") { modules = [ From 96f1a46fefdad18213a9a85774d3997e9198cb45 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 09:41:55 +0000 Subject: [PATCH 0874/1215] Upgrade to Micrometer Tracing 1.2.1 Closes gh-38694 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d2842aba0f77..9bc94fd703dd 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1008,7 +1008,7 @@ bom { ] } } - library("Micrometer Tracing", "1.2.1-SNAPSHOT") { + library("Micrometer Tracing", "1.2.1") { considerSnapshots() calendarName = "Tracing" group("io.micrometer") { From 60ebb32e8ac1f67ad919a97477a75022efe3884c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 09:41:57 +0000 Subject: [PATCH 0875/1215] Upgrade to Reactor Bom 2023.0.1 Closes gh-38695 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 9bc94fd703dd..afb3a09a215b 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1323,7 +1323,7 @@ bom { ] } } - library("Reactor Bom", "2023.0.1-SNAPSHOT") { + library("Reactor Bom", "2023.0.1") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From 8c5b7a87ae1a08d3b5157449c9f1dc0b0a5cc48a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 10:21:48 +0000 Subject: [PATCH 0876/1215] Adapt to latest changes in the locking model for context close See gh-38666 --- .../boot/SpringApplicationShutdownHookTests.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationShutdownHookTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationShutdownHookTests.java index bafe2688da89..0d821496e30d 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationShutdownHookTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/SpringApplicationShutdownHookTests.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; @@ -113,7 +114,7 @@ void runWhenContextIsBeingClosedInAnotherThreadWaitsUntilContextIsInactive() thr Thread shutdownThread = new Thread(shutdownHook); shutdownThread.start(); // Shutdown thread should start waiting for context to become inactive - Awaitility.await().atMost(Duration.ofSeconds(30)).until(shutdownThread::getState, State.TIMED_WAITING::equals); + Awaitility.await().atMost(Duration.ofSeconds(30)).until(shutdownThread::getState, State.WAITING::equals); // Allow context thread to proceed, unblocking shutdown thread proceedWithClose.countDown(); contextThread.join(); @@ -252,7 +253,7 @@ protected void onClose() { } if (this.proceedWithClose != null) { try { - this.proceedWithClose.await(); + this.proceedWithClose.await(1, TimeUnit.MINUTES); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); From e44e0c8f1ed10e0cfcd839fa382ff0483831fff4 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 14 Dec 2023 11:17:12 +0100 Subject: [PATCH 0877/1215] Remove ErrorAttributes.ERROR_ATTRIBUTE This commit removes the now defunkt `ErrorAttributes.ERROR_ATTRIBUTE` that was introduce to register handled errors as metrics. This has been replaced since 3.0 by a direct support in Spring Framework and had no effect whatsoever since that release. This also updates the documentation to point to the Framework mechanism that replaced it. Fixes gh-33731 --- .../src/docs/asciidoc/web/reactive.adoc | 6 +-- .../src/docs/asciidoc/web/servlet.adoc | 6 +-- .../MyExceptionHandlingController.java | 41 ------------------ .../springmvc/errorhandling/MyController.java | 34 --------------- .../MyExceptionHandlingController.kt | 42 ------------------- .../springmvc/errorhandling/MyController.kt | 33 --------------- .../error/DefaultErrorAttributes.java | 1 - .../web/reactive/error/ErrorAttributes.java | 9 +--- .../servlet/error/DefaultErrorAttributes.java | 3 -- .../web/servlet/error/ErrorAttributes.java | 10 +---- .../error/DefaultErrorAttributesTests.java | 3 -- .../error/DefaultErrorAttributesTests.java | 10 ----- 12 files changed, 6 insertions(+), 192 deletions(-) delete mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/reactive/webflux/errorhandling/MyExceptionHandlingController.java delete mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/springmvc/errorhandling/MyController.java delete mode 100644 spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/reactive/webflux/errorhandling/MyExceptionHandlingController.kt delete mode 100644 spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/servlet/springmvc/errorhandling/MyController.kt diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc index 82292dbe64aa..6e5a2d1b60bc 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc @@ -189,10 +189,8 @@ include::code:MyErrorWebExceptionHandler[] For a more complete picture, you can also subclass `DefaultErrorWebExceptionHandler` directly and override specific methods. -In some cases, errors handled at the controller or handler function level are not recorded by the <>. -Applications can ensure that such exceptions are recorded with the request metrics by setting the handled exception as a request attribute: - -include::code:MyExceptionHandlingController[] +In some cases, errors handled at the controller level are not recorded by web observations or the <>. +Applications can ensure that such exceptions are recorded with the observations by {spring-framework-docs}/integration/observability.html#observability.http-server.reactive[setting the handled exception on the observation context]. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc index 19503eab0796..fbc330fd0ce9 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/servlet.adoc @@ -355,10 +355,8 @@ include::code:MyControllerAdvice[] In the preceding example, if `MyException` is thrown by a controller defined in the same package as `SomeController`, a JSON representation of the `MyErrorBody` POJO is used instead of the `ErrorAttributes` representation. -In some cases, errors handled at the controller level are not recorded by the <>. -Applications can ensure that such exceptions are recorded with the request metrics by setting the handled exception as a request attribute: - -include::code:MyController[] +In some cases, errors handled at the controller level are not recorded by web observations or the <>. +Applications can ensure that such exceptions are recorded with the observations by {spring-framework-docs}/integration/observability.html#observability.http-server.servlet[setting the handled exception on the observation context]. diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/reactive/webflux/errorhandling/MyExceptionHandlingController.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/reactive/webflux/errorhandling/MyExceptionHandlingController.java deleted file mode 100644 index b1913940a84e..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/reactive/webflux/errorhandling/MyExceptionHandlingController.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2012-2021 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.docs.web.reactive.webflux.errorhandling; - -import org.springframework.boot.web.reactive.error.ErrorAttributes; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.reactive.result.view.Rendering; -import org.springframework.web.server.ServerWebExchange; - -@Controller -public class MyExceptionHandlingController { - - @GetMapping("/profile") - public Rendering userProfile() { - // ... - throw new IllegalStateException(); - } - - @ExceptionHandler(IllegalStateException.class) - public Rendering handleIllegalState(ServerWebExchange exchange, IllegalStateException exc) { - exchange.getAttributes().putIfAbsent(ErrorAttributes.ERROR_ATTRIBUTE, exc); - return Rendering.view("errorView").modelAttribute("message", exc.getMessage()).build(); - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/springmvc/errorhandling/MyController.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/springmvc/errorhandling/MyController.java deleted file mode 100644 index e93f0ba92b43..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/web/servlet/springmvc/errorhandling/MyController.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2012-2021 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.docs.web.servlet.springmvc.errorhandling; - -import jakarta.servlet.http.HttpServletRequest; - -import org.springframework.boot.web.servlet.error.ErrorAttributes; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.ExceptionHandler; - -@Controller -public class MyController { - - @ExceptionHandler(CustomException.class) - String handleCustomException(HttpServletRequest request, CustomException ex) { - request.setAttribute(ErrorAttributes.ERROR_ATTRIBUTE, ex); - return "errorView"; - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/reactive/webflux/errorhandling/MyExceptionHandlingController.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/reactive/webflux/errorhandling/MyExceptionHandlingController.kt deleted file mode 100644 index 26dfa251d465..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/reactive/webflux/errorhandling/MyExceptionHandlingController.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2012-2022 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.docs.web.reactive.webflux.errorhandling - -import org.springframework.boot.web.reactive.error.ErrorAttributes -import org.springframework.stereotype.Controller -import org.springframework.web.bind.annotation.ExceptionHandler -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.reactive.result.view.Rendering -import org.springframework.web.server.ServerWebExchange - -@Suppress("UNUSED_PARAMETER") -@Controller -class MyExceptionHandlingController { - - @GetMapping("/profile") - fun userProfile(): Rendering { - // ... - throw IllegalStateException() - } - - @ExceptionHandler(IllegalStateException::class) - fun handleIllegalState(exchange: ServerWebExchange, exc: IllegalStateException): Rendering { - exchange.attributes.putIfAbsent(ErrorAttributes.ERROR_ATTRIBUTE, exc) - return Rendering.view("errorView").modelAttribute("message", exc.message ?: "").build() - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/servlet/springmvc/errorhandling/MyController.kt b/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/servlet/springmvc/errorhandling/MyController.kt deleted file mode 100644 index deead1e60da0..000000000000 --- a/spring-boot-project/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/web/servlet/springmvc/errorhandling/MyController.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2012-2022 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.docs.web.servlet.springmvc.errorhandling - -import jakarta.servlet.http.HttpServletRequest -import org.springframework.boot.web.servlet.error.ErrorAttributes -import org.springframework.stereotype.Controller -import org.springframework.web.bind.annotation.ExceptionHandler - -@Controller -class MyController { - - @ExceptionHandler(CustomException::class) - fun handleCustomException(request: HttpServletRequest, ex: CustomException?): String { - request.setAttribute(ErrorAttributes.ERROR_ATTRIBUTE, ex) - return "errorView" - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java index d55366b53c4a..354b46988a8f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java @@ -152,7 +152,6 @@ private void handleException(Map errorAttributes, Throwable erro @Override public Throwable getError(ServerRequest request) { Optional error = request.attribute(ERROR_INTERNAL_ATTRIBUTE); - error.ifPresent((value) -> request.attributes().putIfAbsent(ErrorAttributes.ERROR_ATTRIBUTE, value)); return (Throwable) error .orElseThrow(() -> new IllegalStateException("Missing exception attribute in ServerWebExchange")); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/ErrorAttributes.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/ErrorAttributes.java index 3c9a1d5567ba..aab94b4e31ec 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/ErrorAttributes.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/ErrorAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -34,13 +34,6 @@ */ public interface ErrorAttributes { - /** - * Name of the {@link ServerRequest#attribute(String) request attribute} holding the - * error resolved by the {@code ErrorAttributes} implementation. - * @since 2.5.0 - */ - String ERROR_ATTRIBUTE = ErrorAttributes.class.getName() + ".error"; - /** * Return a {@link Map} of the error attributes. The map can be used as the model of * an error page, or returned as a {@link ServerResponse} body. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java index 21e6bd068fe0..ef02be96709b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java @@ -216,9 +216,6 @@ public Throwable getError(WebRequest webRequest) { if (exception == null) { exception = getAttribute(webRequest, RequestDispatcher.ERROR_EXCEPTION); } - // store the exception in a well-known attribute to make it available to metrics - // instrumentation. - webRequest.setAttribute(ErrorAttributes.ERROR_ATTRIBUTE, exception, WebRequest.SCOPE_REQUEST); return exception; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/ErrorAttributes.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/ErrorAttributes.java index 898363885eac..3f2fc64d1e12 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/ErrorAttributes.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/ErrorAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2023 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. @@ -34,14 +34,6 @@ */ public interface ErrorAttributes { - /** - * Name of the {@link jakarta.servlet.http.HttpServletRequest#getAttribute(String) - * request attribute} holding the error resolved by the {@code ErrorAttributes} - * implementation. - * @since 2.5.0 - */ - String ERROR_ATTRIBUTE = ErrorAttributes.class.getName() + ".error"; - /** * Returns a {@link Map} of the error attributes. The map can be used as the model of * an error page {@link ModelAndView}, or returned as a diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java index 06633045c35b..9b3d63375c5f 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java @@ -160,7 +160,6 @@ void includeException() { Map attributes = this.errorAttributes.getErrorAttributes(serverRequest, ErrorAttributeOptions.of(Include.EXCEPTION, Include.MESSAGE)); assertThat(this.errorAttributes.getError(serverRequest)).isSameAs(error); - assertThat(serverRequest.attribute(ErrorAttributes.ERROR_ATTRIBUTE)).containsSame(error); assertThat(attributes).containsEntry("exception", RuntimeException.class.getName()); assertThat(attributes).containsEntry("message", "Test"); } @@ -178,7 +177,6 @@ void processResponseStatusException() { assertThat(attributes).containsEntry("message", "invalid request"); assertThat(attributes).containsEntry("exception", RuntimeException.class.getName()); assertThat(this.errorAttributes.getError(serverRequest)).isSameAs(error); - assertThat(serverRequest.attribute(ErrorAttributes.ERROR_ATTRIBUTE)).containsSame(error); } @Test @@ -194,7 +192,6 @@ void processResponseStatusExceptionWithNoNestedCause() { assertThat(attributes).containsEntry("message", "could not process request"); assertThat(attributes).containsEntry("exception", ResponseStatusException.class.getName()); assertThat(this.errorAttributes.getError(serverRequest)).isSameAs(error); - assertThat(serverRequest.attribute(ErrorAttributes.ERROR_ATTRIBUTE)).containsSame(error); } @Test diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributesTests.java index 71405232b66e..fe6848d0cb3f 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributesTests.java @@ -88,8 +88,6 @@ void mvcError() { Map attributes = this.errorAttributes.getErrorAttributes(this.webRequest, ErrorAttributeOptions.of(Include.MESSAGE)); assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(ex); - assertThat(this.webRequest.getAttribute(ErrorAttributes.ERROR_ATTRIBUTE, WebRequest.SCOPE_REQUEST)) - .isSameAs(ex); assertThat(modelAndView).isNull(); assertThat(attributes).doesNotContainKey("exception"); assertThat(attributes).containsEntry("message", "Test"); @@ -102,8 +100,6 @@ void servletErrorWithMessage() { Map attributes = this.errorAttributes.getErrorAttributes(this.webRequest, ErrorAttributeOptions.of(Include.MESSAGE)); assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(ex); - assertThat(this.webRequest.getAttribute(ErrorAttributes.ERROR_ATTRIBUTE, WebRequest.SCOPE_REQUEST)) - .isSameAs(ex); assertThat(attributes).doesNotContainKey("exception"); assertThat(attributes).containsEntry("message", "Test"); } @@ -115,8 +111,6 @@ void servletErrorWithoutMessage() { Map attributes = this.errorAttributes.getErrorAttributes(this.webRequest, ErrorAttributeOptions.defaults()); assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(ex); - assertThat(this.webRequest.getAttribute(ErrorAttributes.ERROR_ATTRIBUTE, WebRequest.SCOPE_REQUEST)) - .isSameAs(ex); assertThat(attributes).doesNotContainKey("exception"); assertThat(attributes).doesNotContainKey("message"); } @@ -166,8 +160,6 @@ void unwrapServletException() { Map attributes = this.errorAttributes.getErrorAttributes(this.webRequest, ErrorAttributeOptions.of(Include.MESSAGE)); assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(wrapped); - assertThat(this.webRequest.getAttribute(ErrorAttributes.ERROR_ATTRIBUTE, WebRequest.SCOPE_REQUEST)) - .isSameAs(wrapped); assertThat(attributes).doesNotContainKey("exception"); assertThat(attributes).containsEntry("message", "Test"); } @@ -179,8 +171,6 @@ void getError() { Map attributes = this.errorAttributes.getErrorAttributes(this.webRequest, ErrorAttributeOptions.of(Include.MESSAGE)); assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(error); - assertThat(this.webRequest.getAttribute(ErrorAttributes.ERROR_ATTRIBUTE, WebRequest.SCOPE_REQUEST)) - .isSameAs(error); assertThat(attributes).doesNotContainKey("exception"); assertThat(attributes).containsEntry("message", "Test error"); } From 2e43819e8daa36cfbca7d125a15b4c01cac3264f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 17:07:22 +0000 Subject: [PATCH 0878/1215] Upgrade to AspectJ 1.9.21 Closes gh-38797 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 9f3604c04c89..316bc1b7bada 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -83,7 +83,7 @@ bom { ] } } - library("AspectJ", "1.9.20.1") { + library("AspectJ", "1.9.21") { group("org.aspectj") { modules = [ "aspectjrt", From 06068894a4bd5d4c2a6bb21ddefa44136392a213 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 17:07:27 +0000 Subject: [PATCH 0879/1215] Upgrade to Dropwizard Metrics 4.2.23 Closes gh-38798 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 316bc1b7bada..775734af93fa 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -247,7 +247,7 @@ bom { ] } } - library("Dropwizard Metrics", "4.2.22") { + library("Dropwizard Metrics", "4.2.23") { group("io.dropwizard.metrics") { imports = [ "metrics-bom" From b20ed7c577de04ee0ab898fd5e4937d850f105e2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 17:07:31 +0000 Subject: [PATCH 0880/1215] Upgrade to Groovy 4.0.16 Closes gh-38799 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 775734af93fa..fdcbe392b87d 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -339,7 +339,7 @@ bom { ] } } - library("Groovy", "4.0.15") { + library("Groovy", "4.0.16") { group("org.apache.groovy") { imports = [ "groovy-bom" From f3201880236e8ad30aa6b0c170d7ce705599c68a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 17:07:36 +0000 Subject: [PATCH 0881/1215] Upgrade to HttpClient5 5.2.3 Closes gh-38800 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index fdcbe392b87d..0b1e57e2725a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -438,7 +438,7 @@ bom { ] } } - library("HttpClient5", "5.2.1") { + library("HttpClient5", "5.2.3") { group("org.apache.httpcomponents.client5") { modules = [ "httpclient5", From 8deae8275eef11350f2dbf7365573832fb6a0fcf Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 17:07:41 +0000 Subject: [PATCH 0882/1215] Upgrade to HttpCore5 5.2.4 Closes gh-38801 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 0b1e57e2725a..3d4a38a7a336 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -456,7 +456,7 @@ bom { ] } } - library("HttpCore5", "5.2.3") { + library("HttpCore5", "5.2.4") { group("org.apache.httpcomponents.core5") { modules = [ "httpcore5", From da7cb2ad1b6f287c036862d2a31eb8921a0f06fa Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 17:07:46 +0000 Subject: [PATCH 0883/1215] Upgrade to Janino 3.1.11 Closes gh-38802 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 3d4a38a7a336..d08f76ba954f 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -606,7 +606,7 @@ bom { ] } } - library("Janino", "3.1.10") { + library("Janino", "3.1.11") { group("org.codehaus.janino") { modules = [ "commons-compiler", From d2d303d5aa6de51210406e4e38a800718ca43297 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 17:07:50 +0000 Subject: [PATCH 0884/1215] Upgrade to Jaybird 5.0.3.java11 Closes gh-38803 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d08f76ba954f..0702c55d1dd0 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -636,7 +636,7 @@ bom { ] } } - library("Jaybird", "5.0.2.java11") { + library("Jaybird", "5.0.3.java11") { group("org.firebirdsql.jdbc") { modules = [ "jaybird" From 853aaeb8186c467bcc9e1c2b259480e00dff323e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 17:07:55 +0000 Subject: [PATCH 0885/1215] Upgrade to Jersey 3.1.5 Closes gh-38804 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 0702c55d1dd0..120392422f95 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -664,7 +664,7 @@ bom { ] } } - library("Jersey", "3.1.3") { + library("Jersey", "3.1.5") { group("org.glassfish.jersey") { imports = [ "jersey-bom" From 86599a5062d9f27a7cd513d189537f91a35dbaaa Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 17:08:00 +0000 Subject: [PATCH 0886/1215] Upgrade to Jetty 12.0.4 Closes gh-38805 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 120392422f95..e74615e0af61 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -678,7 +678,7 @@ bom { ] } } - library("Jetty", "12.0.3") { + library("Jetty", "12.0.4") { group("org.eclipse.jetty.ee10") { imports = [ "jetty-ee10-bom" From 3b51bcc9121a2859d0831c2ed568b83ba1f863b7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 17:08:05 +0000 Subject: [PATCH 0887/1215] Upgrade to Kafka 3.6.1 Closes gh-38806 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e74615e0af61..1c1b309cd557 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -753,7 +753,7 @@ bom { ] } } - library("Kafka", "3.6.0") { + library("Kafka", "3.6.1") { group("org.apache.kafka") { modules = [ "connect", From 61fdaec7fa1428d9fdb4377f8b737fb1ea75614c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 17:08:09 +0000 Subject: [PATCH 0888/1215] Upgrade to Kotlin 1.9.21 Closes gh-38807 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 09bed8d5c337..36c2baae5eae 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ commonsCodecVersion=1.16.0 hamcrestVersion=2.2 jacksonVersion=2.15.3 junitJupiterVersion=5.10.1 -kotlinVersion=1.9.20 +kotlinVersion=1.9.21 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.28 springFrameworkVersion=6.1.2-SNAPSHOT From d95e7a5af0d90a2f3a99f3aecb8a1d90827cbb03 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 17:08:14 +0000 Subject: [PATCH 0889/1215] Upgrade to Kotlin Serialization 1.6.2 Closes gh-38808 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 1c1b309cd557..b5774da1e71e 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -814,7 +814,7 @@ bom { ] } } - library("Kotlin Serialization", "1.6.1") { + library("Kotlin Serialization", "1.6.2") { group("org.jetbrains.kotlinx") { imports = [ "kotlinx-serialization-bom" From 4892024b7d7e85babe0fa411b406c11ea7df81f0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 17:08:19 +0000 Subject: [PATCH 0890/1215] Upgrade to Logback 1.4.14 Closes gh-38809 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index b5774da1e71e..2a059ba08a2b 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -846,7 +846,7 @@ bom { ] } } - library("Logback", "1.4.11") { + library("Logback", "1.4.14") { group("ch.qos.logback") { modules = [ "logback-access", From 79455a79f267e274b665e602bc820872220703c9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 17:08:24 +0000 Subject: [PATCH 0891/1215] Upgrade to Maven Javadoc Plugin 3.6.3 Closes gh-38810 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 2a059ba08a2b..9b93cf9764ca 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -953,7 +953,7 @@ bom { ] } } - library("Maven Javadoc Plugin", "3.6.2") { + library("Maven Javadoc Plugin", "3.6.3") { group("org.apache.maven.plugins") { plugins = [ "maven-javadoc-plugin" From 5d3aaf98b8d5551bafc4136aeabca4f12698cf41 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 17:08:28 +0000 Subject: [PATCH 0892/1215] Upgrade to Netty 4.1.102.Final Closes gh-38811 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 9b93cf9764ca..40f6f866cf38 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1077,7 +1077,7 @@ bom { ] } } - library("Netty", "4.1.101.Final") { + library("Netty", "4.1.102.Final") { prohibit { versionRange "[4.1.103.Final]" because "it crashes on macOS (https://github.com/netty/netty/issues/13728)" From 7f064bc4562f6dc8082c1b8fe7bc69d891fab8f2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 17:08:33 +0000 Subject: [PATCH 0893/1215] Upgrade to R2DBC Postgresql 1.0.3.RELEASE Closes gh-38812 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 40f6f866cf38..f1fe16d0fe95 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1282,7 +1282,7 @@ bom { ] } } - library("R2DBC Postgresql", "1.0.2.RELEASE") { + library("R2DBC Postgresql", "1.0.3.RELEASE") { considerSnapshots() group("org.postgresql") { modules = [ From 25b109167c6433620e632045edc821afe22593d4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 17:08:38 +0000 Subject: [PATCH 0894/1215] Upgrade to R2DBC Proxy 1.1.3.RELEASE Closes gh-38813 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f1fe16d0fe95..ee91c194b637 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1290,7 +1290,7 @@ bom { ] } } - library("R2DBC Proxy", "1.1.2.RELEASE") { + library("R2DBC Proxy", "1.1.3.RELEASE") { considerSnapshots() group("io.r2dbc") { modules = [ From 18c7e307cc771b4f2dbf3bc500aa6147409e542b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 17:08:43 +0000 Subject: [PATCH 0895/1215] Upgrade to Spring Framework 6.1.2 Closes gh-38814 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 36c2baae5eae..6aba67a1a83a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ junitJupiterVersion=5.10.1 kotlinVersion=1.9.21 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.28 -springFrameworkVersion=6.1.2-SNAPSHOT +springFrameworkVersion=6.1.2 tomcatVersion=10.1.16 kotlin.stdlib.default.dependency=false From ee19ed7f1741d661203a5069ab0cf82f915bd1f5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 17:08:47 +0000 Subject: [PATCH 0896/1215] Upgrade to Tomcat 10.1.17 Closes gh-38815 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6aba67a1a83a..e30259a2514f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,6 +13,6 @@ kotlinVersion=1.9.21 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.28 springFrameworkVersion=6.1.2 -tomcatVersion=10.1.16 +tomcatVersion=10.1.17 kotlin.stdlib.default.dependency=false From d7f4a8ca6a9782f70af7b4a177274add56e04e27 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 14 Dec 2023 17:08:52 +0000 Subject: [PATCH 0897/1215] Upgrade to UnboundID LDAPSDK 6.0.11 Closes gh-38816 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index ee91c194b637..3acec34c13b3 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1696,7 +1696,7 @@ bom { ] } } - library("UnboundID LDAPSDK", "6.0.10") { + library("UnboundID LDAPSDK", "6.0.11") { group("com.unboundid") { modules = [ "unboundid-ldapsdk" From 1d10e517555998a0f1f5ccf8e236346b1a9a16a0 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 14 Dec 2023 20:33:09 -0800 Subject: [PATCH 0898/1215] Adapt to upstream Spring Security changes --- ...FoundryActuatorAutoConfigurationTests.java | 29 ++++++++++-- .../OAuth2WebSecurityConfigurationTests.java | 45 +++++++++++++------ ...ml2RelyingPartyAutoConfigurationTests.java | 36 +++++++++++---- 3 files changed, 86 insertions(+), 24 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java index dbd0b28dd2c5..067eea979426 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/servlet/CloudFoundryActuatorAutoConfigurationTests.java @@ -18,7 +18,9 @@ import java.util.Arrays; import java.util.Collection; +import java.util.List; +import jakarta.servlet.Filter; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; @@ -43,6 +45,7 @@ import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; +import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpMethod; @@ -55,6 +58,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.client.RestTemplate; import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.filter.CompositeFilter; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -173,9 +177,7 @@ void cloudFoundryPathsIgnoredBySpringSecurity() { this.contextRunner.withBean(TestEndpoint.class, TestEndpoint::new) .withPropertyValues("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id") .run((context) -> { - FilterChainProxy securityFilterChain = (FilterChainProxy) context - .getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN); - SecurityFilterChain chain = securityFilterChain.getFilterChains().get(0); + SecurityFilterChain chain = getSecurityFilterChain(context); assertThat(chain.getFilters()).isEmpty(); MockHttpServletRequest request = new MockHttpServletRequest(); testCloudFoundrySecurity(request, BASE_PATH, chain); @@ -189,6 +191,27 @@ void cloudFoundryPathsIgnoredBySpringSecurity() { }); } + private SecurityFilterChain getSecurityFilterChain(AssertableWebApplicationContext context) { + Filter springSecurityFilterChain = context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN, Filter.class); + FilterChainProxy filterChainProxy = getFilterChainProxy(springSecurityFilterChain); + SecurityFilterChain securityFilterChain = filterChainProxy.getFilterChains().get(0); + return securityFilterChain; + } + + private FilterChainProxy getFilterChainProxy(Filter filter) { + if (filter instanceof FilterChainProxy filterChainProxy) { + return filterChainProxy; + } + if (filter instanceof CompositeFilter) { + List filters = (List) ReflectionTestUtils.getField(filter, "filters"); + return (FilterChainProxy) filters.stream() + .filter(FilterChainProxy.class::isInstance) + .findFirst() + .orElseThrow(); + } + throw new IllegalStateException("No FilterChainProxy found"); + } + private static void testCloudFoundrySecurity(MockHttpServletRequest request, String servletPath, SecurityFilterChain chain) { request.setServletPath(servletPath); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2WebSecurityConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2WebSecurityConfigurationTests.java index 190859e9f7a3..b9cf9f8b6b3f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2WebSecurityConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2WebSecurityConfigurationTests.java @@ -48,6 +48,7 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.ObjectUtils; +import org.springframework.web.filter.CompositeFilter; import static org.assertj.core.api.Assertions.assertThat; @@ -68,7 +69,7 @@ void securityConfigurerConfiguresOAuth2Login() { .run((context) -> { ClientRegistrationRepository expected = context.getBean(ClientRegistrationRepository.class); ClientRegistrationRepository actual = (ClientRegistrationRepository) ReflectionTestUtils.getField( - getFilters(context, OAuth2LoginAuthenticationFilter.class).get(0), + getSecurityFilters(context, OAuth2LoginAuthenticationFilter.class).get(0), "clientRegistrationRepository"); assertThat(isEqual(expected.findByRegistrationId("first"), actual.findByRegistrationId("first"))) .isTrue(); @@ -85,7 +86,7 @@ void securityConfigurerConfiguresAuthorizationCode() { .run((context) -> { ClientRegistrationRepository expected = context.getBean(ClientRegistrationRepository.class); ClientRegistrationRepository actual = (ClientRegistrationRepository) ReflectionTestUtils.getField( - getFilters(context, OAuth2AuthorizationCodeGrantFilter.class).get(0), + getSecurityFilters(context, OAuth2AuthorizationCodeGrantFilter.class).get(0), "clientRegistrationRepository"); assertThat(isEqual(expected.findByRegistrationId("first"), actual.findByRegistrationId("first"))) .isTrue(); @@ -98,8 +99,8 @@ void securityConfigurerConfiguresAuthorizationCode() { void securityConfigurerBacksOffWhenClientRegistrationBeanAbsent() { this.contextRunner.withUserConfiguration(TestConfig.class, OAuth2WebSecurityConfiguration.class) .run((context) -> { - assertThat(getFilters(context, OAuth2LoginAuthenticationFilter.class)).isEmpty(); - assertThat(getFilters(context, OAuth2AuthorizationCodeGrantFilter.class)).isEmpty(); + assertThat(getSecurityFilters(context, OAuth2LoginAuthenticationFilter.class)).isEmpty(); + assertThat(getSecurityFilters(context, OAuth2AuthorizationCodeGrantFilter.class)).isEmpty(); }); } @@ -124,8 +125,8 @@ void securityFilterChainConfigBacksOffWhenOtherSecurityFilterChainBeanPresent() this.contextRunner.withConfiguration(AutoConfigurations.of(WebMvcAutoConfiguration.class)) .withUserConfiguration(TestSecurityFilterChainConfiguration.class, OAuth2WebSecurityConfiguration.class) .run((context) -> { - assertThat(getFilters(context, OAuth2LoginAuthenticationFilter.class)).isEmpty(); - assertThat(getFilters(context, OAuth2AuthorizationCodeGrantFilter.class)).isEmpty(); + assertThat(getSecurityFilters(context, OAuth2LoginAuthenticationFilter.class)).isEmpty(); + assertThat(getSecurityFilters(context, OAuth2AuthorizationCodeGrantFilter.class)).isEmpty(); assertThat(context).getBean(OAuth2AuthorizedClientService.class).isNotNull(); }); } @@ -137,8 +138,8 @@ void securityFilterChainConfigConditionalOnSecurityFilterChainClass() { OAuth2WebSecurityConfiguration.class) .withClassLoader(new FilteredClassLoader(SecurityFilterChain.class)) .run((context) -> { - assertThat(getFilters(context, OAuth2LoginAuthenticationFilter.class)).isEmpty(); - assertThat(getFilters(context, OAuth2AuthorizationCodeGrantFilter.class)).isEmpty(); + assertThat(getSecurityFilters(context, OAuth2LoginAuthenticationFilter.class)).isEmpty(); + assertThat(getSecurityFilters(context, OAuth2AuthorizationCodeGrantFilter.class)).isEmpty(); }); } @@ -164,11 +165,29 @@ void authorizedClientRepositoryBeanIsConditionalOnMissingBean() { }); } - private List getFilters(AssertableWebApplicationContext context, Class filter) { - FilterChainProxy filterChain = (FilterChainProxy) context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN); - List filterChains = filterChain.getFilterChains(); - List filters = filterChains.get(0).getFilters(); - return filters.stream().filter(filter::isInstance).toList(); + private List getSecurityFilters(AssertableWebApplicationContext context, Class filter) { + return getSecurityFilterChain(context).getFilters().stream().filter(filter::isInstance).toList(); + } + + private SecurityFilterChain getSecurityFilterChain(AssertableWebApplicationContext context) { + Filter springSecurityFilterChain = context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN, Filter.class); + FilterChainProxy filterChainProxy = getFilterChainProxy(springSecurityFilterChain); + SecurityFilterChain securityFilterChain = filterChainProxy.getFilterChains().get(0); + return securityFilterChain; + } + + private FilterChainProxy getFilterChainProxy(Filter filter) { + if (filter instanceof FilterChainProxy filterChainProxy) { + return filterChainProxy; + } + if (filter instanceof CompositeFilter) { + List filters = (List) ReflectionTestUtils.getField(filter, "filters"); + return (FilterChainProxy) filters.stream() + .filter(FilterChainProxy.class::isInstance) + .findFirst() + .orElseThrow(); + } + throw new IllegalStateException("No FilterChainProxy found"); } private boolean isEqual(ClientRegistration reg1, ClientRegistration reg2) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyAutoConfigurationTests.java index b3f51121b243..5c8b4ae27da4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyAutoConfigurationTests.java @@ -46,6 +46,8 @@ import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2LogoutRequestFilter; import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.filter.CompositeFilter; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -208,7 +210,7 @@ void relyingPartyRegistrationRepositoryShouldBeConditionalOnMissingBean() { @Test void samlLoginShouldBeConfigured() { this.contextRunner.withPropertyValues(getPropertyValues()) - .run((context) -> assertThat(hasFilter(context, Saml2WebSsoAuthenticationFilter.class)).isTrue()); + .run((context) -> assertThat(hasSecurityFilter(context, Saml2WebSsoAuthenticationFilter.class)).isTrue()); } @Test @@ -216,7 +218,7 @@ void samlLoginShouldBackOffWhenASecurityFilterChainBeanIsPresent() { this.contextRunner.withConfiguration(AutoConfigurations.of(WebMvcAutoConfiguration.class)) .withUserConfiguration(TestSecurityFilterChainConfig.class) .withPropertyValues(getPropertyValues()) - .run((context) -> assertThat(hasFilter(context, Saml2WebSsoAuthenticationFilter.class)).isFalse()); + .run((context) -> assertThat(hasSecurityFilter(context, Saml2WebSsoAuthenticationFilter.class)).isFalse()); } @Test @@ -229,7 +231,7 @@ void samlLoginShouldShouldBeConditionalOnSecurityWebFilterClass() { @Test void samlLogoutShouldBeConfigured() { this.contextRunner.withPropertyValues(getPropertyValues()) - .run((context) -> assertThat(hasFilter(context, Saml2LogoutRequestFilter.class)).isTrue()); + .run((context) -> assertThat(hasSecurityFilter(context, Saml2LogoutRequestFilter.class)).isTrue()); } private String[] getPropertyValuesWithoutSigningCredentials(boolean signRequests) { @@ -323,11 +325,29 @@ private String[] getPropertyValues() { PREFIX + ".foo.acs.binding=redirect" }; } - private boolean hasFilter(AssertableWebApplicationContext context, Class filter) { - FilterChainProxy filterChain = (FilterChainProxy) context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN); - List filterChains = filterChain.getFilterChains(); - List filters = filterChains.get(0).getFilters(); - return filters.stream().anyMatch(filter::isInstance); + private boolean hasSecurityFilter(AssertableWebApplicationContext context, Class filter) { + return getSecurityFilterChain(context).getFilters().stream().anyMatch(filter::isInstance); + } + + private SecurityFilterChain getSecurityFilterChain(AssertableWebApplicationContext context) { + Filter springSecurityFilterChain = context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN, Filter.class); + FilterChainProxy filterChainProxy = getFilterChainProxy(springSecurityFilterChain); + SecurityFilterChain securityFilterChain = filterChainProxy.getFilterChains().get(0); + return securityFilterChain; + } + + private FilterChainProxy getFilterChainProxy(Filter filter) { + if (filter instanceof FilterChainProxy filterChainProxy) { + return filterChainProxy; + } + if (filter instanceof CompositeFilter) { + List filters = (List) ReflectionTestUtils.getField(filter, "filters"); + return (FilterChainProxy) filters.stream() + .filter(FilterChainProxy.class::isInstance) + .findFirst() + .orElseThrow(); + } + throw new IllegalStateException("No FilterChainProxy found"); } private void setupMockResponse(MockWebServer server, Resource resourceBody) throws Exception { From bb37a868b3e5a4db003d503c58c838a5a32784a8 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 14 Dec 2023 20:33:28 -0800 Subject: [PATCH 0899/1215] Temporarily disable failing test See gh-gh-38822 --- .../secure/CustomServletPathUnauthenticatedErrorPageTests.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/CustomServletPathUnauthenticatedErrorPageTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/CustomServletPathUnauthenticatedErrorPageTests.java index ff6577d4eb05..b297a1094995 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/CustomServletPathUnauthenticatedErrorPageTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/CustomServletPathUnauthenticatedErrorPageTests.java @@ -16,6 +16,8 @@ package smoketest.web.secure; +import org.junit.jupiter.api.Disabled; + import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -34,6 +36,7 @@ SampleWebSecureApplication.class }, properties = { "server.error.include-message=always", "spring.security.user.name=username", "spring.security.user.password=password", "spring.mvc.servlet.path=/custom/servlet/path" }) +@Disabled("gh-38822") class CustomServletPathUnauthenticatedErrorPageTests extends AbstractUnauthenticatedErrorPageTests { CustomServletPathUnauthenticatedErrorPageTests() { From 26dc14031e7f05bdcbfea87a6b26222ea5ab759a Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 14 Dec 2023 22:03:07 -0800 Subject: [PATCH 0900/1215] Update `LoadedPemSslStore` to use lazy loading Update `LoadedPemSslStore` so that it loads content lazily. This restores the behavior of Spring Boot 3.1 and allows bundles to be defined with files that don't exist as long as they are never accessed. Fixes gh-38659 --- .../ssl/PropertiesSslBundle.java | 25 ++++------ .../boot/ssl/pem/LoadedPemSslStore.java | 26 +++++++--- .../boot/ssl/pem/PemSslStore.java | 4 +- .../boot/ssl/pem/PemSslStoreBundle.java | 10 +--- .../boot/ssl/pem/LoadedPemSslStoreTests.java | 48 +++++++++++++++++++ 5 files changed, 78 insertions(+), 35 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/LoadedPemSslStoreTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java index a76f5c2fa2b1..c16a5161e843 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ssl/PropertiesSslBundle.java @@ -16,9 +16,6 @@ package org.springframework.boot.autoconfigure.ssl; -import java.io.IOException; -import java.io.UncheckedIOException; - import org.springframework.boot.autoconfigure.ssl.SslBundleProperties.Key; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundleKey; @@ -99,23 +96,17 @@ public SslManagerBundle getManagers() { * @return an {@link SslBundle} instance */ public static SslBundle get(PemSslBundleProperties properties) { - try { - PemSslStore keyStore = getPemSslStore("keystore", properties.getKeystore()); - if (keyStore != null) { - keyStore = keyStore.withAlias(properties.getKey().getAlias()) - .withPassword(properties.getKey().getPassword()); - } - PemSslStore trustStore = getPemSslStore("truststore", properties.getTruststore()); - SslStoreBundle storeBundle = new PemSslStoreBundle(keyStore, trustStore); - return new PropertiesSslBundle(storeBundle, properties); - } - catch (IOException ex) { - throw new UncheckedIOException(ex); + PemSslStore keyStore = getPemSslStore("keystore", properties.getKeystore()); + if (keyStore != null) { + keyStore = keyStore.withAlias(properties.getKey().getAlias()) + .withPassword(properties.getKey().getPassword()); } + PemSslStore trustStore = getPemSslStore("truststore", properties.getTruststore()); + SslStoreBundle storeBundle = new PemSslStoreBundle(keyStore, trustStore); + return new PropertiesSslBundle(storeBundle, properties); } - private static PemSslStore getPemSslStore(String propertyName, PemSslBundleProperties.Store properties) - throws IOException { + private static PemSslStore getPemSslStore(String propertyName, PemSslBundleProperties.Store properties) { PemSslStore pemSslStore = PemSslStore.load(asPemSslStoreDetails(properties)); if (properties.isVerifyKeys()) { CertificateMatcher certificateMatcher = new CertificateMatcher(pemSslStore.privateKey()); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/LoadedPemSslStore.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/LoadedPemSslStore.java index 07f457a1b1f8..5edacd360e61 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/LoadedPemSslStore.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/LoadedPemSslStore.java @@ -17,12 +17,16 @@ package org.springframework.boot.ssl.pem; import java.io.IOException; +import java.io.UncheckedIOException; import java.security.PrivateKey; import java.security.cert.X509Certificate; import java.util.List; +import java.util.function.Supplier; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import org.springframework.util.function.SingletonSupplier; +import org.springframework.util.function.ThrowingSupplier; /** * {@link PemSslStore} loaded from {@link PemSslStoreDetails}. @@ -34,15 +38,23 @@ final class LoadedPemSslStore implements PemSslStore { private final PemSslStoreDetails details; - private final List certificates; + private final Supplier> certificatesSupplier; - private final PrivateKey privateKey; + private final Supplier privateKeySupplier; - LoadedPemSslStore(PemSslStoreDetails details) throws IOException { + LoadedPemSslStore(PemSslStoreDetails details) { Assert.notNull(details, "Details must not be null"); this.details = details; - this.certificates = loadCertificates(details); - this.privateKey = loadPrivateKey(details); + this.certificatesSupplier = supplier(() -> loadCertificates(details)); + this.privateKeySupplier = supplier(() -> loadPrivateKey(details)); + } + + private static Supplier supplier(ThrowingSupplier supplier) { + return SingletonSupplier.of(supplier.throwing(LoadedPemSslStore::asUncheckedIOException)); + } + + private static UncheckedIOException asUncheckedIOException(String message, Exception cause) { + return new UncheckedIOException(message, (IOException) cause); } private static List loadCertificates(PemSslStoreDetails details) throws IOException { @@ -77,12 +89,12 @@ public String password() { @Override public List certificates() { - return this.certificates; + return this.certificatesSupplier.get(); } @Override public PrivateKey privateKey() { - return this.privateKey; + return this.privateKeySupplier.get(); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java index e1ed146f3cdb..d58fc9a71bee 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStore.java @@ -16,7 +16,6 @@ package org.springframework.boot.ssl.pem; -import java.io.IOException; import java.security.KeyStore; import java.security.PrivateKey; import java.security.cert.X509Certificate; @@ -92,9 +91,8 @@ default PemSslStore withPassword(String password) { * {@link PemSslStoreDetails}. * @param details the PEM store details * @return a loaded {@link PemSslStore} or {@code null}. - * @throws IOException on IO error */ - static PemSslStore load(PemSslStoreDetails details) throws IOException { + static PemSslStore load(PemSslStoreDetails details) { if (details == null || details.isEmpty()) { return null; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java index 44c6e0fbff47..5834d2f84fc3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ssl/pem/PemSslStoreBundle.java @@ -17,7 +17,6 @@ package org.springframework.boot.ssl.pem; import java.io.IOException; -import java.io.UncheckedIOException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; @@ -66,13 +65,8 @@ public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails */ @Deprecated(since = "3.2.0", forRemoval = true) public PemSslStoreBundle(PemSslStoreDetails keyStoreDetails, PemSslStoreDetails trustStoreDetails, String alias) { - try { - this.keyStore = createKeyStore("key", PemSslStore.load(keyStoreDetails), alias); - this.trustStore = createKeyStore("trust", PemSslStore.load(trustStoreDetails), alias); - } - catch (IOException ex) { - throw new UncheckedIOException(ex); - } + this.keyStore = createKeyStore("key", PemSslStore.load(keyStoreDetails), alias); + this.trustStore = createKeyStore("trust", PemSslStore.load(trustStoreDetails), alias); } /** diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/LoadedPemSslStoreTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/LoadedPemSslStoreTests.java new file mode 100644 index 000000000000..b7bccce838a5 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/ssl/pem/LoadedPemSslStoreTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2023 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.ssl.pem; + +import java.io.UncheckedIOException; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link LoadedPemSslStore}. + * + * @author Phillip Webb + */ +class LoadedPemSslStoreTests { + + @Test + void certificatesAreLoadedLazily() { + PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:missing-test-cert.pem") + .withPrivateKey("classpath:test-key.pem"); + LoadedPemSslStore store = new LoadedPemSslStore(details); + assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(store::certificates); + } + + @Test + void privateKeyIsLoadedLazily() { + PemSslStoreDetails details = PemSslStoreDetails.forCertificate("classpath:test-cert.pem") + .withPrivateKey("classpath:missing-test-key.pem"); + LoadedPemSslStore store = new LoadedPemSslStore(details); + assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(store::privateKey); + } + +} From 65af35c1ac79c7e75d69ee76a4dcf5ec9f28322a Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 15 Dec 2023 07:42:00 -0800 Subject: [PATCH 0901/1215] Revert "Temporarily disable failing test" This reverts commit bb37a868b3e5a4db003d503c58c838a5a32784a8. Closes gh-38659 --- .../secure/CustomServletPathUnauthenticatedErrorPageTests.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/CustomServletPathUnauthenticatedErrorPageTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/CustomServletPathUnauthenticatedErrorPageTests.java index b297a1094995..ff6577d4eb05 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/CustomServletPathUnauthenticatedErrorPageTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/CustomServletPathUnauthenticatedErrorPageTests.java @@ -16,8 +16,6 @@ package smoketest.web.secure; -import org.junit.jupiter.api.Disabled; - import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -36,7 +34,6 @@ SampleWebSecureApplication.class }, properties = { "server.error.include-message=always", "spring.security.user.name=username", "spring.security.user.password=password", "spring.mvc.servlet.path=/custom/servlet/path" }) -@Disabled("gh-38822") class CustomServletPathUnauthenticatedErrorPageTests extends AbstractUnauthenticatedErrorPageTests { CustomServletPathUnauthenticatedErrorPageTests() { From 42830dc621ff47e5d5e6cc00b2eb5b598b5fd796 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 15 Dec 2023 18:25:18 +0100 Subject: [PATCH 0902/1215] Upgrade to Spring Data Bom 2023.1.1 Closes gh-38697 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 3acec34c13b3..d988dc45d142 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1519,7 +1519,7 @@ bom { ] } } - library("Spring Data Bom", "2023.1.1-SNAPSHOT") { + library("Spring Data Bom", "2023.1.1") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From f2a74c910793dbaa7d6351236e936030d252f302 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 15 Dec 2023 18:26:21 +0100 Subject: [PATCH 0903/1215] Upgrade to Spring Retry 2.0.5 Closes gh-38836 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d988dc45d142..9027f3cb482f 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1600,7 +1600,7 @@ bom { ] } } - library("Spring Retry", "2.0.4") { + library("Spring Retry", "2.0.5") { considerSnapshots() group("org.springframework.retry") { modules = [ From 92a4a1194d7a599cb57b5e5169ee5bbbfce637d8 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 15 Dec 2023 12:08:11 -0800 Subject: [PATCH 0904/1215] Polish --- .../boot/SpringApplication.java | 88 ++++++++++--------- .../boot/StartupInfoLoggerTests.java | 6 +- 2 files changed, 50 insertions(+), 44 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index ede5ab2b6549..c63d2ce0da35 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -1633,6 +1633,9 @@ public ConfigurableApplicationContext getApplicationContext() { } + /** + * {@link SpringApplicationHook} decorator that ensures the hook is only used once. + */ private static final class SingleUseSpringApplicationHook implements SpringApplicationHook { private final AtomicBoolean used = new AtomicBoolean(); @@ -1651,8 +1654,8 @@ public SpringApplicationRunListener getRunListener(SpringApplication springAppli } /** - * <<<<<<< HEAD Starts a non-daemon thread to keep the JVM alive on - * {@link ContextRefreshedEvent}. Stops the thread on {@link ContextClosedEvent}. + * Starts a non-daemon thread to keep the JVM alive on {@link ContextRefreshedEvent}. + * Stops the thread on {@link ContextClosedEvent}. */ private static final class KeepAlive implements ApplicationListener { @@ -1696,15 +1699,18 @@ private void stopKeepAliveThread() { } + /** + * Strategy used to handle startup concerns. + */ abstract static class Startup { private Duration timeTakenToStarted; - abstract long startTime(); + protected abstract long startTime(); - abstract Long processUptime(); + protected abstract Long processUptime(); - abstract String action(); + protected abstract String action(); final Duration started() { long now = System.currentTimeMillis(); @@ -1712,15 +1718,15 @@ final Duration started() { return this.timeTakenToStarted; } + Duration timeTakenToStarted() { + return this.timeTakenToStarted; + } + private Duration ready() { long now = System.currentTimeMillis(); return Duration.ofMillis(now - startTime()); } - Duration timeTakenToStarted() { - return this.timeTakenToStarted; - } - static Startup create() { ClassLoader classLoader = Startup.class.getClassLoader(); return (ClassUtils.isPresent("jdk.crac.management.CRaCMXBean", classLoader) @@ -1730,61 +1736,61 @@ static Startup create() { } - private static class CoordinatedRestoreAtCheckpointStartup extends Startup { + /** + * Standard {@link Startup} implementation. + */ + private static class StandardStartup extends Startup { - private final StandardStartup fallback = new StandardStartup(); + private final Long startTime = System.currentTimeMillis(); @Override - Long processUptime() { - long uptime = CRaCMXBean.getCRaCMXBean().getUptimeSinceRestore(); - return (uptime >= 0) ? uptime : this.fallback.processUptime(); + protected long startTime() { + return this.startTime; } @Override - String action() { - if (restoreTime() >= 0) { - return "Restored"; + protected Long processUptime() { + try { + return ManagementFactory.getRuntimeMXBean().getUptime(); + } + catch (Throwable ex) { + return null; } - return this.fallback.action(); - } - - private long restoreTime() { - return CRaCMXBean.getCRaCMXBean().getRestoreTime(); } @Override - long startTime() { - long restoreTime = restoreTime(); - if (restoreTime >= 0) { - return restoreTime; - } - return this.fallback.startTime(); + protected String action() { + return "Started"; } } - private static class StandardStartup extends Startup { + /** + * Coordinated-Restore-At-Checkpoint {@link Startup} implementation. + */ + private static class CoordinatedRestoreAtCheckpointStartup extends Startup { - private final Long startTime = System.currentTimeMillis(); + private final StandardStartup fallback = new StandardStartup(); @Override - long startTime() { - return this.startTime; + protected Long processUptime() { + long uptime = CRaCMXBean.getCRaCMXBean().getUptimeSinceRestore(); + return (uptime >= 0) ? uptime : this.fallback.processUptime(); } @Override - Long processUptime() { - try { - return ManagementFactory.getRuntimeMXBean().getUptime(); - } - catch (Throwable ex) { - return null; - } + protected String action() { + return (restoreTime() >= 0) ? "Restored" : this.fallback.action(); + } + + private long restoreTime() { + return CRaCMXBean.getCRaCMXBean().getRestoreTime(); } @Override - String action() { - return "Started"; + protected long startTime() { + long restoreTime = restoreTime(); + return (restoreTime >= 0) ? restoreTime : this.fallback.startTime(); } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/StartupInfoLoggerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/StartupInfoLoggerTests.java index a6b1c90ba6de..ae1bbb4ed2ee 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/StartupInfoLoggerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/StartupInfoLoggerTests.java @@ -110,17 +110,17 @@ static class TestStartup extends Startup { } @Override - long startTime() { + protected long startTime() { return this.startTime; } @Override - Long processUptime() { + protected Long processUptime() { return this.uptime; } @Override - String action() { + protected String action() { return this.action; } From 6ae113c18a6151972e8d462f2eba94fb6449f914 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sat, 16 Dec 2023 21:57:16 -0800 Subject: [PATCH 0905/1215] Fix parallel startup of testcontainers Update `TestcontainersLifecycleBeanPostProcessor` so that containers can actually be started in parallel. Prior to this commit, `initializeStartables` would collect beans and in the process trigger the `postProcessAfterInitialization` method on each bean. This would see that `startablesInitialized` was `true` and call `startableBean.start` directly. The result of this was that beans were actually started sequentially and when the `start` method was finally called it had nothing to do. The updated code uses an enum rather than a boolean so that the `postProcessAfterInitialization` method no longer attempts to start beans unless `initializeStartables` has finished. Fixes gh-38831 --- ...tcontainersLifecycleBeanPostProcessor.java | 28 +++++-- ...ainersParallelStartupIntegrationTests.java | 75 +++++++++++++++++++ 2 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestContainersParallelStartupIntegrationTests.java diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java index edafed5c7d01..3ec39b713a5e 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import org.apache.commons.logging.Log; @@ -63,7 +64,7 @@ class TestcontainersLifecycleBeanPostProcessor implements DestructionAwareBeanPo private final TestcontainersStartup startup; - private final AtomicBoolean startablesInitialized = new AtomicBoolean(); + private final AtomicReference startables = new AtomicReference<>(Startables.UNSTARTED); private final AtomicBoolean containersInitialized = new AtomicBoolean(); @@ -79,10 +80,11 @@ public Object postProcessAfterInitialization(Object bean, String beanName) throw initializeContainers(); } if (bean instanceof Startable startableBean) { - if (this.startablesInitialized.compareAndSet(false, true)) { + if (this.startables.compareAndExchange(Startables.UNSTARTED, Startables.STARTING) == Startables.UNSTARTED) { initializeStartables(startableBean, beanName); } - else { + else if (this.startables.get() == Startables.STARTED) { + logger.trace(LogMessage.format("Starting container %s", beanName)); startableBean.start(); } } @@ -90,17 +92,21 @@ public Object postProcessAfterInitialization(Object bean, String beanName) throw } private void initializeStartables(Startable startableBean, String startableBeanName) { + logger.trace(LogMessage.format("Initializing startables")); List beanNames = new ArrayList<>( List.of(this.beanFactory.getBeanNamesForType(Startable.class, false, false))); beanNames.remove(startableBeanName); List beans = getBeans(beanNames); if (beans == null) { - this.startablesInitialized.set(false); + logger.trace(LogMessage.format("Failed to obtain startables %s", beanNames)); + this.startables.set(Startables.UNSTARTED); return; } beanNames.add(startableBeanName); beans.add(startableBean); + logger.trace(LogMessage.format("Starting startables %s", beanNames)); start(beans); + this.startables.set(Startables.STARTED); if (!beanNames.isEmpty()) { logger.debug(LogMessage.format("Initialized and started startable beans '%s'", beanNames)); } @@ -115,8 +121,14 @@ private void start(List beans) { } private void initializeContainers() { + logger.trace("Initializing containers"); List beanNames = List.of(this.beanFactory.getBeanNamesForType(ContainerState.class, false, false)); - if (getBeans(beanNames) == null) { + List beans = getBeans(beanNames); + if (beans != null) { + logger.trace(LogMessage.format("Initialized containers %s", beanNames)); + } + else { + logger.trace(LogMessage.format("Failed to initialize containers %s", beanNames)); this.containersInitialized.set(false); } } @@ -164,4 +176,10 @@ private boolean isReusedContainer(Object bean) { return (bean instanceof GenericContainer container) && container.isShouldBeReused(); } + enum Startables { + + UNSTARTED, STARTING, STARTED + + } + } diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestContainersParallelStartupIntegrationTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestContainersParallelStartupIntegrationTests.java new file mode 100644 index 000000000000..3b6ed3da25b0 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestContainersParallelStartupIntegrationTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-2023 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.testcontainers.lifecycle; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.testcontainers.containers.PostgreSQLContainer; + +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.boot.testcontainers.lifecycle.TestContainersParallelStartupIntegrationTests.ContainerConfig; +import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test for parallel startup. + * + * @author Phillip Webb + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = ContainerConfig.class) +@TestPropertySource(properties = "spring.testcontainers.beans.startup=parallel") +@DirtiesContext +@DisabledIfDockerUnavailable +@ExtendWith(OutputCaptureExtension.class) +public class TestContainersParallelStartupIntegrationTests { + + @Test + void startsInParallel(CapturedOutput out) { + assertThat(out).contains("-lifecycle-0").contains("-lifecycle-1").contains("-lifecycle-2"); + } + + @Configuration(proxyBeanMethods = false) + static class ContainerConfig { + + @Bean + static PostgreSQLContainer container1() { + return new PostgreSQLContainer<>(DockerImageNames.postgresql()); + } + + @Bean + static PostgreSQLContainer container2() { + return new PostgreSQLContainer<>(DockerImageNames.postgresql()); + } + + @Bean + static PostgreSQLContainer container3() { + return new PostgreSQLContainer<>(DockerImageNames.postgresql()); + } + + } + +} From 4c0a19e8c0ef2256e2f7eb00291440f23a1b8852 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Fri, 15 Dec 2023 23:48:11 -0600 Subject: [PATCH 0906/1215] Use authParamString to configure Pulsar authentication Update `PulsarPropertiesMapper` to use JSON encoded parameters rather than a `Map` since the `Map` method is deprecated in Pulsar. This commit simply takes the auth params map and converts them to the expected encoded JSON string of auth parameters. See gh-38839 --- .../pulsar/PulsarPropertiesMapper.java | 23 ++++++++++++++----- .../pulsar/PulsarPropertiesMapperTests.java | 6 +++-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java index 04246d0e91c2..4d5c1827f53f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java @@ -19,6 +19,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Map; +import java.util.TreeMap; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -29,6 +30,7 @@ import org.apache.pulsar.client.api.ProducerBuilder; import org.apache.pulsar.client.api.PulsarClientException.UnsupportedAuthenticationException; import org.apache.pulsar.client.api.ReaderBuilder; +import org.apache.pulsar.common.util.ObjectMapperFactory; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.pulsar.listener.PulsarContainerProperties; @@ -71,13 +73,23 @@ void customizeAdminBuilder(PulsarAdminBuilder adminBuilder, PulsarConnectionDeta private void customizeAuthentication(AuthenticationConsumer authentication, PulsarProperties.Authentication properties) { - if (StringUtils.hasText(properties.getPluginClassName())) { + if (!StringUtils.hasText(properties.getPluginClassName())) { + return; + } + try { + // sort keys for testing and readability + Map params = new TreeMap<>(properties.getParam()); + String authParamString; try { - authentication.accept(properties.getPluginClassName(), properties.getParam()); + authParamString = ObjectMapperFactory.create().writeValueAsString(params); } - catch (UnsupportedAuthenticationException ex) { - throw new IllegalStateException("Unable to configure Pulsar authentication", ex); + catch (Exception ex) { + throw new IllegalStateException("Could not convert auth parameters to encoded string", ex); } + authentication.accept(properties.getPluginClassName(), authParamString); + } + catch (UnsupportedAuthenticationException ex) { + throw new IllegalStateException("Unable to configure Pulsar authentication", ex); } } @@ -158,8 +170,7 @@ private Consumer timeoutProperty(BiConsumer setter) private interface AuthenticationConsumer { - void accept(String authPluginClassName, Map authParams) - throws UnsupportedAuthenticationException; + void accept(String authPluginClassName, String authParamString) throws UnsupportedAuthenticationException; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java index b168d4f71306..458b3abf480b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java @@ -73,12 +73,13 @@ void customizeClientBuilderWhenHasNoAuthentication() { void customizeClientBuilderWhenHasAuthentication() throws UnsupportedAuthenticationException { PulsarProperties properties = new PulsarProperties(); Map params = Map.of("param", "name"); + String authParamString = "{\"param\":\"name\"}"; properties.getClient().getAuthentication().setPluginClassName("myclass"); properties.getClient().getAuthentication().setParam(params); ClientBuilder builder = mock(ClientBuilder.class); new PulsarPropertiesMapper(properties).customizeClientBuilder(builder, new PropertiesPulsarConnectionDetails(properties)); - then(builder).should().authentication("myclass", params); + then(builder).should().authentication("myclass", authParamString); } @Test @@ -112,12 +113,13 @@ void customizeAdminBuilderWhenHasNoAuthentication() { void customizeAdminBuilderWhenHasAuthentication() throws UnsupportedAuthenticationException { PulsarProperties properties = new PulsarProperties(); Map params = Map.of("param", "name"); + String authParamString = "{\"param\":\"name\"}"; properties.getAdmin().getAuthentication().setPluginClassName("myclass"); properties.getAdmin().getAuthentication().setParam(params); PulsarAdminBuilder builder = mock(PulsarAdminBuilder.class); new PulsarPropertiesMapper(properties).customizeAdminBuilder(builder, new PropertiesPulsarConnectionDetails(properties)); - then(builder).should().authentication("myclass", params); + then(builder).should().authentication("myclass", authParamString); } @Test From 2158f4cc43ce07ae6780d56f720870be44255892 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sat, 16 Dec 2023 22:19:35 -0800 Subject: [PATCH 0907/1215] Polish 'Use authParamString to configure Pulsar authentication' See gh-38839 --- .../pulsar/PulsarPropertiesMapper.java | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java index 4d5c1827f53f..ed9411512eb0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java @@ -73,23 +73,24 @@ void customizeAdminBuilder(PulsarAdminBuilder adminBuilder, PulsarConnectionDeta private void customizeAuthentication(AuthenticationConsumer authentication, PulsarProperties.Authentication properties) { - if (!StringUtils.hasText(properties.getPluginClassName())) { - return; - } - try { - // sort keys for testing and readability - Map params = new TreeMap<>(properties.getParam()); - String authParamString; + if (StringUtils.hasText(properties.getPluginClassName())) { try { - authParamString = ObjectMapperFactory.create().writeValueAsString(params); + authentication.accept(properties.getPluginClassName(), + getAuthenticationParamsJson(properties.getParam())); } - catch (Exception ex) { - throw new IllegalStateException("Could not convert auth parameters to encoded string", ex); + catch (UnsupportedAuthenticationException ex) { + throw new IllegalStateException("Unable to configure Pulsar authentication", ex); } - authentication.accept(properties.getPluginClassName(), authParamString); } - catch (UnsupportedAuthenticationException ex) { - throw new IllegalStateException("Unable to configure Pulsar authentication", ex); + } + + private String getAuthenticationParamsJson(Map params) { + Map sortedParams = new TreeMap<>(params); + try { + return ObjectMapperFactory.create().writeValueAsString(sortedParams); + } + catch (Exception ex) { + throw new IllegalStateException("Could not convert auth parameters to encoded string", ex); } } From 88429b6a663782129747b12d8983c42bb446cf45 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sun, 17 Dec 2023 10:53:52 -0800 Subject: [PATCH 0908/1215] Use file urls for unpacked jars Update `JarFileArchive` so that unpacked jars use `file:` URLs rather than `jar:file:`. This aligns with the behavior of Spring Boot 3.1 and allows calls to `class.getSigners()` to work again. Fixes gh-38833 --- .../org/springframework/boot/loader/launch/JarFileArchive.java | 2 +- .../boot/loader/launch/JarFileArchiveTests.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarFileArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarFileArchive.java index 3ccb32009fb3..a38379de908c 100755 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarFileArchive.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarFileArchive.java @@ -113,7 +113,7 @@ private URL getUnpackedNestedJarUrl(JarEntry jarEntry) throws IOException { if (!Files.exists(path) || Files.size(path) != jarEntry.getSize()) { unpack(jarEntry, path); } - return JarUrl.create(path.toFile()); + return path.toUri().toURL(); } private Path getTempUnpackDirectory() { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/JarFileArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/JarFileArchiveTests.java index ac4a521388a7..94fd60609d91 100755 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/JarFileArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/JarFileArchiveTests.java @@ -112,7 +112,8 @@ void getClassPathUrlsWhenHasUnpackCommentUnpacksAndReturnsUrls() throws Exceptio assertThat(urls).hasSize(1); URL url = urls.iterator().next(); assertThat(url).isNotEqualTo(JarUrl.create(this.file, "nested.jar")); - assertThat(url.toString()).startsWith("jar:file:").endsWith("/nested.jar!/"); + // The unpack URL must be a raw file URL (see gh-38833) + assertThat(url.toString()).startsWith("file:").endsWith("/nested.jar").doesNotStartWith("jar:"); } @Test From 561c7f749ba05e65cdabaf3ecd505132edab3081 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sun, 17 Dec 2023 15:22:28 -0800 Subject: [PATCH 0909/1215] Don't start containers imported via @ImportTestcontainers Remove early start of containers imported via `@ImportTestcontainers` so that parallel startup can happen. Fixes gh-38831 --- .../context/ContainerFieldsImporter.java | 4 -- ...hImportTestcontainersIntegrationTests.java | 67 +++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestContainersParallelStartupWithImportTestcontainersIntegrationTests.java diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ContainerFieldsImporter.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ContainerFieldsImporter.java index ed12fd8de10b..95a8fce48ff3 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ContainerFieldsImporter.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ContainerFieldsImporter.java @@ -22,7 +22,6 @@ import java.util.List; import org.testcontainers.containers.Container; -import org.testcontainers.lifecycle.Startable; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.util.Assert; @@ -39,9 +38,6 @@ void registerBeanDefinitions(BeanDefinitionRegistry registry, Class definitio for (Field field : getContainerFields(definitionClass)) { assertValid(field); Container container = getContainer(field); - if (container instanceof Startable startable) { - startable.start(); - } registerBeanDefinition(registry, field, container); } } diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestContainersParallelStartupWithImportTestcontainersIntegrationTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestContainersParallelStartupWithImportTestcontainersIntegrationTests.java new file mode 100644 index 000000000000..7159f470d4f8 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestContainersParallelStartupWithImportTestcontainersIntegrationTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2023 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.testcontainers.lifecycle; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; + +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.boot.testcontainers.context.ImportTestcontainers; +import org.springframework.boot.testcontainers.lifecycle.TestContainersParallelStartupWithImportTestcontainersIntegrationTests.Containers; +import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test for parallel startup. + * + * @author Phillip Webb + */ +@ExtendWith(SpringExtension.class) +@TestPropertySource(properties = "spring.testcontainers.beans.startup=parallel") +@DirtiesContext +@DisabledIfDockerUnavailable +@ExtendWith(OutputCaptureExtension.class) +@ImportTestcontainers(Containers.class) +public class TestContainersParallelStartupWithImportTestcontainersIntegrationTests { + + @Test + void startsInParallel(CapturedOutput out) { + assertThat(out).contains("-lifecycle-0").contains("-lifecycle-1").contains("-lifecycle-2"); + } + + static class Containers { + + @Container + static PostgreSQLContainer container1 = new PostgreSQLContainer<>(DockerImageNames.postgresql()); + + @Container + static PostgreSQLContainer container2 = new PostgreSQLContainer<>(DockerImageNames.postgresql()); + + @Container + static PostgreSQLContainer container3 = new PostgreSQLContainer<>(DockerImageNames.postgresql()); + + } + +} From c4150dff096d23c0c2ce8d500fc5fe789497f46c Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 19 Dec 2023 11:32:19 +0100 Subject: [PATCH 0910/1215] Fix authorization server smoke test Change from spring-projects/spring-authorization-server#1468 See gh-38696 --- ...h2AuthorizationServerApplicationTests.java | 59 ++++++++++--------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java index dbc0bff4e5b0..b0695ea68892 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java @@ -39,7 +39,8 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationServerMetadata; import org.springframework.security.oauth2.server.authorization.oidc.OidcProviderConfiguration; -import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import static org.assertj.core.api.Assertions.assertThat; @@ -103,13 +104,13 @@ void anonymousShouldRedirectToLogin() { void validTokenRequestShouldReturnTokenResponse() { HttpHeaders headers = new HttpHeaders(); headers.setBasicAuth("messaging-client", "secret"); - HttpEntity request = new HttpEntity<>(headers); - String requestUri = UriComponentsBuilder.fromUriString("/token") - .queryParam(OAuth2ParameterNames.CLIENT_ID, "messaging-client") - .queryParam(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) - .queryParam(OAuth2ParameterNames.SCOPE, "message.read+message.write") - .toUriString(); - ResponseEntity> entity = this.restTemplate.exchange(requestUri, HttpMethod.POST, request, + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add(OAuth2ParameterNames.CLIENT_ID, "messaging-client"); + body.add(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()); + body.add(OAuth2ParameterNames.SCOPE, "message.read message.write"); + HttpEntity request = new HttpEntity<>(body, headers); + ResponseEntity> entity = this.restTemplate.exchange("/token", HttpMethod.POST, request, MAP_TYPE_REFERENCE); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); Map tokenResponse = Objects.requireNonNull(entity.getBody()); @@ -123,13 +124,13 @@ void validTokenRequestShouldReturnTokenResponse() { @Test void anonymousTokenRequestShouldReturnUnauthorized() { HttpHeaders headers = new HttpHeaders(); - HttpEntity request = new HttpEntity<>(headers); - String requestUri = UriComponentsBuilder.fromUriString("/token") - .queryParam(OAuth2ParameterNames.CLIENT_ID, "messaging-client") - .queryParam(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) - .queryParam(OAuth2ParameterNames.SCOPE, "message.read+message.write") - .toUriString(); - ResponseEntity> entity = this.restTemplate.exchange(requestUri, HttpMethod.POST, request, + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add(OAuth2ParameterNames.CLIENT_ID, "messaging-client"); + body.add(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()); + body.add(OAuth2ParameterNames.SCOPE, "message.read message.write"); + HttpEntity request = new HttpEntity<>(body, headers); + ResponseEntity> entity = this.restTemplate.exchange("/token", HttpMethod.POST, request, MAP_TYPE_REFERENCE); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); } @@ -137,14 +138,14 @@ void anonymousTokenRequestShouldReturnUnauthorized() { @Test void anonymousTokenRequestWithAcceptHeaderAllShouldReturnUnauthorized() { HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.setAccept(List.of(MediaType.ALL)); - HttpEntity request = new HttpEntity<>(headers); - String requestUri = UriComponentsBuilder.fromUriString("/token") - .queryParam(OAuth2ParameterNames.CLIENT_ID, "messaging-client") - .queryParam(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) - .queryParam(OAuth2ParameterNames.SCOPE, "message.read+message.write") - .toUriString(); - ResponseEntity> entity = this.restTemplate.exchange(requestUri, HttpMethod.POST, request, + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add(OAuth2ParameterNames.CLIENT_ID, "messaging-client"); + body.add(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()); + body.add(OAuth2ParameterNames.SCOPE, "message.read message.write"); + HttpEntity request = new HttpEntity<>(body, headers); + ResponseEntity> entity = this.restTemplate.exchange("/token", HttpMethod.POST, request, MAP_TYPE_REFERENCE); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); } @@ -152,14 +153,14 @@ void anonymousTokenRequestWithAcceptHeaderAllShouldReturnUnauthorized() { @Test void anonymousTokenRequestWithAcceptHeaderTextHtmlShouldRedirectToLogin() { HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.setAccept(List.of(MediaType.TEXT_HTML)); - HttpEntity request = new HttpEntity<>(headers); - String requestUri = UriComponentsBuilder.fromUriString("/token") - .queryParam(OAuth2ParameterNames.CLIENT_ID, "messaging-client") - .queryParam(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()) - .queryParam(OAuth2ParameterNames.SCOPE, "message.read+message.write") - .toUriString(); - ResponseEntity> entity = this.restTemplate.exchange(requestUri, HttpMethod.POST, request, + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add(OAuth2ParameterNames.CLIENT_ID, "messaging-client"); + body.add(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue()); + body.add(OAuth2ParameterNames.SCOPE, "message.read message.write"); + HttpEntity request = new HttpEntity<>(body, headers); + ResponseEntity> entity = this.restTemplate.exchange("/token", HttpMethod.POST, request, MAP_TYPE_REFERENCE); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FOUND); assertThat(entity.getHeaders().getLocation()).isEqualTo(URI.create("http://localhost:" + this.port + "/login")); From 1c210f5c1a27cfa488ac023e71cd97bffb2f7d2f Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 19 Dec 2023 11:34:54 +0100 Subject: [PATCH 0911/1215] Upgrade to Spring AMQP 3.1.1 Closes gh-38860 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 9027f3cb482f..21b98d3d877e 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1495,7 +1495,7 @@ bom { ] } } - library("Spring AMQP", "3.1.0") { + library("Spring AMQP", "3.1.1") { considerSnapshots() group("org.springframework.amqp") { imports = [ From 67458b86620bd6c375df537b3ea764df33e9514a Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 19 Dec 2023 11:35:23 +0100 Subject: [PATCH 0912/1215] Upgrade to Spring LDAP 3.2.1 Closes gh-38699 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 21b98d3d877e..ad5f9381eccb 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1571,7 +1571,7 @@ bom { ] } } - library("Spring LDAP", "3.2.1-SNAPSHOT") { + library("Spring LDAP", "3.2.1") { considerSnapshots() group("org.springframework.ldap") { modules = [ From bdb2cb131a26b6cf191f30a8c3271e39ea6149e1 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 19 Dec 2023 11:35:57 +0100 Subject: [PATCH 0913/1215] Upgrade to Spring Security 6.2.1 Closes gh-38700 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index ad5f9381eccb..345ce69e25ca 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1608,7 +1608,7 @@ bom { ] } } - library("Spring Security", "6.2.1-SNAPSHOT") { + library("Spring Security", "6.2.1") { considerSnapshots() group("org.springframework.security") { imports = [ From 97f08da638f042ce2b5746db00a79d41269bc7bf Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 19 Dec 2023 19:10:41 +0100 Subject: [PATCH 0914/1215] Upgrade to Spring Authorization Server 1.2.1 Closes gh-38696 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 345ce69e25ca..3a7a4c9b5ac0 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1503,7 +1503,7 @@ bom { ] } } - library("Spring Authorization Server", "1.2.1-SNAPSHOT") { + library("Spring Authorization Server", "1.2.1") { considerSnapshots() group("org.springframework.security") { modules = [ From 76c7fe3f8aee999185fa5b6947e371296025e888 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 19 Dec 2023 19:11:15 +0100 Subject: [PATCH 0915/1215] Upgrade to Spring Session 3.2.1 Closes gh-38866 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 3a7a4c9b5ac0..3c1c50f543bd 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1616,7 +1616,7 @@ bom { ] } } - library("Spring Session", "3.2.0") { + library("Spring Session", "3.2.1") { considerSnapshots() prohibit { startsWith(["Apple-", "Bean-", "Corn-", "Dragonfruit-"]) From bf21fa8e76fa5c1626b3bf68978b62ade99430c1 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 19 Dec 2023 21:22:27 +0100 Subject: [PATCH 0916/1215] Upgrade to Spring Integration 6.2.1 Closes gh-38698 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 3c1c50f543bd..134b57e57d8c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1554,7 +1554,7 @@ bom { ] } } - library("Spring Integration", "6.2.1-SNAPSHOT") { + library("Spring Integration", "6.2.1") { considerSnapshots() group("org.springframework.integration") { imports = [ From cf5dc186f89b9a18cc41484925a89107deb7a9f7 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 19 Dec 2023 20:37:53 -0800 Subject: [PATCH 0917/1215] Upgrade to Hibernate 6.4.1.Final Closes gh-38870 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 134b57e57d8c..87e3c6d0cc55 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -377,7 +377,7 @@ bom { ] } } - library("Hibernate", "6.4.0.Final") { + library("Hibernate", "6.4.1.Final") { group("org.hibernate.orm") { modules = [ "hibernate-agroal", From 31bc458a13903b1ac5b1b8894168e85c2473a5f9 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 19 Dec 2023 20:37:58 -0800 Subject: [PATCH 0918/1215] Upgrade to Jetty 12.0.5 Closes gh-38871 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 87e3c6d0cc55..55d190ed1cf5 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -678,7 +678,7 @@ bom { ] } } - library("Jetty", "12.0.4") { + library("Jetty", "12.0.5") { group("org.eclipse.jetty.ee10") { imports = [ "jetty-ee10-bom" From a04a16a783203a7cab6255729cff953107ae29f8 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 19 Dec 2023 20:38:03 -0800 Subject: [PATCH 0919/1215] Upgrade to Netty 4.1.104.Final Closes gh-38872 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 55d190ed1cf5..9a0b62f930f2 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1077,7 +1077,7 @@ bom { ] } } - library("Netty", "4.1.102.Final") { + library("Netty", "4.1.104.Final") { prohibit { versionRange "[4.1.103.Final]" because "it crashes on macOS (https://github.com/netty/netty/issues/13728)" From 21116297f62c26858461e90ddd866cf446610bad Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 19 Dec 2023 20:38:07 -0800 Subject: [PATCH 0920/1215] Upgrade to Pulsar Reactive 0.5.1 Closes gh-38873 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 9a0b62f930f2..7f20cd79e959 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1216,7 +1216,7 @@ bom { ] } } - library("Pulsar Reactive", "0.5.0") { + library("Pulsar Reactive", "0.5.1") { group("org.apache.pulsar") { modules = [ "pulsar-client-reactive-adapter", From 32d69497336cc2024c7c1eba6ec42e8556b6dcd3 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 19 Dec 2023 20:38:11 -0800 Subject: [PATCH 0921/1215] Upgrade to Spring Kafka 3.1.1 Closes gh-38874 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 7f20cd79e959..c53eeed2a0ae 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1562,7 +1562,7 @@ bom { ] } } - library("Spring Kafka", "3.1.0") { + library("Spring Kafka", "3.1.1") { considerSnapshots() group("org.springframework.kafka") { modules = [ From b6d855fa0beddaaefd0001772d11fd3914adef81 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 19 Dec 2023 20:38:15 -0800 Subject: [PATCH 0922/1215] Upgrade to Spring Pulsar 1.0.1 Closes gh-38875 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c53eeed2a0ae..833d36c27aa4 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1582,7 +1582,7 @@ bom { ] } } - library("Spring Pulsar", "1.0.0") { + library("Spring Pulsar", "1.0.1") { group("org.springframework.pulsar") { modules = [ "spring-pulsar", From 67b43baa166901e7b4b4017b26fea10c96dbcd66 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 19 Dec 2023 20:38:20 -0800 Subject: [PATCH 0923/1215] Upgrade to Spring WS 4.0.9 Closes gh-38876 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 833d36c27aa4..83d201427c9a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1628,7 +1628,7 @@ bom { ] } } - library("Spring WS", "4.0.8") { + library("Spring WS", "4.0.9") { considerSnapshots() group("org.springframework.ws") { imports = [ From 1b498dea43e2c9c7cf338e3c0fd88b8c488f8c69 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 19 Dec 2023 20:45:10 -0800 Subject: [PATCH 0924/1215] Drop Netty restriction --- spring-boot-project/spring-boot-dependencies/build.gradle | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 83d201427c9a..04b2c19e5e26 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1078,10 +1078,6 @@ bom { } } library("Netty", "4.1.104.Final") { - prohibit { - versionRange "[4.1.103.Final]" - because "it crashes on macOS (https://github.com/netty/netty/issues/13728)" - } group("io.netty") { imports = [ "netty-bom" From e39d1d14ea844743affda7e4b1d08400ebf729da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ili=C3=A8s=20BELDJILALI?= Date: Sat, 16 Dec 2023 12:47:05 +0100 Subject: [PATCH 0925/1215] Hide application name placeholder when include property is false Update log4j configuration so that an empty value is used when `LOGGED_APPLICATION_NAME` is missing. Prior to this commit when `logging.include-application-name` was `false` the logged output would include the raw `${sys:LOGGED_APPLICATION_NAME}` value. See gh-38847 --- .../boot/logging/log4j2/log4j2-file.xml | 2 +- .../springframework/boot/logging/log4j2/log4j2.xml | 4 ++-- .../boot/logging/log4j2/Log4J2LoggingSystemTests.java | 11 +++++++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml index fb3edde9dfe7..916f1126db61 100644 --- a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml +++ b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml @@ -5,7 +5,7 @@ %5p yyyy-MM-dd'T'HH:mm:ss.SSSXXX %clr{%d{${sys:LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${sys:LOG_LEVEL_PATTERN}} %clr{%pid}{magenta} %clr{---}{faint} %clr{${sys:LOGGED_APPLICATION_NAME:-}[%15.15t]}{faint} %clr{${sys:LOG_CORRELATION_PATTERN:-}}{faint}%clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} - %d{${sys:LOG_DATEFORMAT_PATTERN}} ${sys:LOG_LEVEL_PATTERN} %pid --- ${sys:LOGGED_APPLICATION_NAME}[%t] ${sys:LOG_CORRELATION_PATTERN:-}%-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} + %d{${sys:LOG_DATEFORMAT_PATTERN}} ${sys:LOG_LEVEL_PATTERN} %pid --- ${sys:LOGGED_APPLICATION_NAME:-}[%t] ${sys:LOG_CORRELATION_PATTERN:-}%-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} diff --git a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2.xml b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2.xml index 600f2fa207ed..239f2e35a34c 100644 --- a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2.xml +++ b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2.xml @@ -4,8 +4,8 @@ %xwEx %5p yyyy-MM-dd'T'HH:mm:ss.SSSXXX - %clr{%d{${sys:LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${sys:LOG_LEVEL_PATTERN}} %clr{%pid}{magenta} %clr{---}{faint} %clr{${sys:LOGGED_APPLICATION_NAME}[%15.15t]}{faint} %clr{${sys:LOG_CORRELATION_PATTERN:-}}{faint}%clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} - %d{${sys:LOG_DATEFORMAT_PATTERN}} ${sys:LOG_LEVEL_PATTERN} %pid --- ${sys:LOGGED_APPLICATION_NAME}[%t] ${sys:LOG_CORRELATION_PATTERN:-}%-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} + %clr{%d{${sys:LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${sys:LOG_LEVEL_PATTERN}} %clr{%pid}{magenta} %clr{---}{faint} %clr{${sys:LOGGED_APPLICATION_NAME:-}[%15.15t]}{faint} %clr{${sys:LOG_CORRELATION_PATTERN:-}}{faint}%clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} + %d{${sys:LOG_DATEFORMAT_PATTERN}} ${sys:LOG_LEVEL_PATTERN} %pid --- ${sys:LOGGED_APPLICATION_NAME:-}[%t] ${sys:LOG_CORRELATION_PATTERN:-}%-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java index 308d29d99e7f..75564c7662e7 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java @@ -574,6 +574,17 @@ void applicationNameLoggingWhenHasApplicationName(CapturedOutput output) { assertThat(getLineWithText(output, "Hello world")).contains("[myapp] "); } + @Test + void applicationNamePlaceHolderNotShowingWhenDisabled(CapturedOutput output) { + this.environment.setProperty("spring.application.name", "application-name"); + this.environment.setProperty("logging.include-application-name", "false"); + this.loggingSystem.setStandardConfigLocations(false); + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, null); + this.logger.info("Hello world"); + assertThat(getLineWithText(output, "Hello world")).doesNotContain("${sys:LOGGED_APPLICATION_NAME}"); + } + @Test void applicationNameLoggingWhenDisabled(CapturedOutput output) { this.environment.setProperty("spring.application.name", "myapp"); From b0bc8728311a2308d82c5465ad91a64e1b56d738 Mon Sep 17 00:00:00 2001 From: Sandra Ahlgrimm Date: Thu, 14 Dec 2023 10:46:01 +0100 Subject: [PATCH 0926/1215] Add the LangChain4J to the list of community starters See gh-38776 --- spring-boot-project/spring-boot-starters/README.adoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spring-boot-project/spring-boot-starters/README.adoc b/spring-boot-project/spring-boot-starters/README.adoc index a00d56fb3179..b857a9598a54 100644 --- a/spring-boot-project/spring-boot-starters/README.adoc +++ b/spring-boot-project/spring-boot-starters/README.adoc @@ -154,6 +154,9 @@ do as they were designed before this was clarified. | https://kogito.kie.org/[Kogito] | https://github.com/kiegroup/kogito-runtimes/tree/main/springboot/starters +| https://github.com/langchain4j/langchain4j[LangChain for Java] +| https://github.com/langchain4j/langchain4j/tree/main/langchain4j-spring-boot-starter + | https://www.liquigraph.org/[Liquigraph] | https://github.com/liquigraph/liquigraph From f31ffbf927468c953c845dcc87b258fe9de9a550 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 20 Dec 2023 17:36:06 -0800 Subject: [PATCH 0927/1215] Don't duplicate META-INF entries in nested directory jars Update `ZipContent` so that `META-INF` entries are no longer duplicated in nested jars created from directory entries. This aligns with the behavior of the classic loader and prevents the same META-INF file from being discovered twice. Fixes gh-38862 --- .../springframework/boot/loader/zip/ZipContent.java | 5 +---- .../boot/loader/jar/NestedJarFileTests.java | 5 ++--- .../boot/loader/zip/ZipContentTests.java | 11 ++++------- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java index f63a17b9e859..ca0f3d7a713b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java @@ -637,10 +637,7 @@ private static ZipContent loadNestedDirectory(Source source, ZipContent zip, Ent .load(zip.data, pos); long namePos = pos + ZipCentralDirectoryFileHeaderRecord.FILE_NAME_OFFSET; short nameLen = centralRecord.fileNameLength(); - if (ZipString.startsWith(loader.buffer, zip.data, namePos, nameLen, META_INF) != -1) { - loader.add(centralRecord, pos, false); - } - else if (ZipString.startsWith(loader.buffer, zip.data, namePos, nameLen, directoryName) != -1) { + if (ZipString.startsWith(loader.buffer, zip.data, namePos, nameLen, directoryName) != -1) { loader.add(centralRecord, pos, true); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java index 8050866078df..5c2aed4d5c4e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java @@ -105,9 +105,8 @@ void createWhenNestedJarFileOpensJar() throws IOException { void createWhenNestedJarDirectoryOpensJar() throws IOException { try (NestedJarFile jar = new NestedJarFile(this.file, "d/")) { assertThat(jar.getName()).isEqualTo(this.file.getAbsolutePath() + "!/d/"); - assertThat(jar.size()).isEqualTo(3); - assertThat(jar.stream().map(JarEntry::getName)).containsExactly("META-INF/", "META-INF/MANIFEST.MF", - "9.dat"); + assertThat(jar.size()).isEqualTo(1); + assertThat(jar.stream().map(JarEntry::getName)).containsExactly("9.dat"); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipContentTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipContentTests.java index 5ae7fe82f255..8681c430d6ce 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipContentTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipContentTests.java @@ -210,11 +210,9 @@ void nestedJarFileWhenNameEndsInSlashThrowsException() { @Test void nestedDirectoryReturnsNestedJar() throws IOException { try (ZipContent nested = ZipContent.open(this.file.toPath(), "d/")) { - assertThat(nested.size()).isEqualTo(3); + assertThat(nested.size()).isEqualTo(1); assertThat(nested.getEntry("9.dat")).isNotNull(); - assertThat(nested.getEntry(0).getName()).isEqualTo("META-INF/"); - assertThat(nested.getEntry(1).getName()).isEqualTo("META-INF/MANIFEST.MF"); - assertThat(nested.getEntry(2).getName()).isEqualTo("9.dat"); + assertThat(nested.getEntry(0).getName()).isEqualTo("9.dat"); } } @@ -230,9 +228,8 @@ void getDataWhenNestedDirectoryReturnsVirtualZipDataBlock() throws IOException { File file = new File(this.tempDir, "included.zip"); write(file, nested.openRawZipData()); try (ZipFile loadedZipFile = new ZipFile(file)) { - assertThat(loadedZipFile.size()).isEqualTo(3); - assertThat(loadedZipFile.stream().map(ZipEntry::getName)).containsExactly("META-INF/", - "META-INF/MANIFEST.MF", "9.dat"); + assertThat(loadedZipFile.size()).isEqualTo(1); + assertThat(loadedZipFile.stream().map(ZipEntry::getName)).containsExactly("9.dat"); assertThat(loadedZipFile.getEntry("9.dat")).isNotNull(); try (InputStream in = loadedZipFile.getInputStream(loadedZipFile.getEntry("9.dat"))) { ByteArrayOutputStream out = new ByteArrayOutputStream(); From a42f549612fc8bacb60461f4f06b57e32f0a9e32 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Thu, 21 Dec 2023 12:36:09 +0000 Subject: [PATCH 0928/1215] Next development version (v3.2.2-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e30259a2514f..35f1f0cd978f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.2.1-SNAPSHOT +version=3.2.2-SNAPSHOT org.gradle.caching=true org.gradle.parallel=true From 00cf1a6d13c9410a922c70be7fc89a8706d66689 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 3 Jan 2024 11:37:01 +0000 Subject: [PATCH 0929/1215] Remove unnecessary configuration of idle timeout Fixes gh-38960 --- .../boot/web/embedded/jetty/JettyServletWebServerFactory.java | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java index a50ddc044802..89d0159d52ae 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java @@ -216,7 +216,6 @@ private Server createServer(InetSocketAddress address) { private AbstractConnector createConnector(InetSocketAddress address, Server server) { HttpConfiguration httpConfiguration = new HttpConfiguration(); httpConfiguration.setSendServerVersion(false); - httpConfiguration.setIdleTimeout(30000); List connectionFactories = new ArrayList<>(); connectionFactories.add(new HttpConnectionFactory(httpConfiguration)); if (getHttp2() != null && getHttp2().isEnabled()) { From 433f8a6fd924bf5325bfc36fec91b7ee885afac6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 10:50:34 +0000 Subject: [PATCH 0930/1215] Prepare 3.2.x branch --- .../org/springframework/boot/build/AsciidoctorConventions.java | 2 +- ci/parameters.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/AsciidoctorConventions.java b/buildSrc/src/main/java/org/springframework/boot/build/AsciidoctorConventions.java index 339db1c3f766..175518e7cf15 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/AsciidoctorConventions.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/AsciidoctorConventions.java @@ -134,7 +134,7 @@ private void configureForkOptions(AbstractAsciidoctorTask asciidoctorTask) { private String determineGitHubTag(Project project) { String version = "v" + project.getVersion(); - return (version.endsWith("-SNAPSHOT")) ? "main" : version; + return (version.endsWith("-SNAPSHOT")) ? "3.2.x" : version; } private void configureOptions(AbstractAsciidoctorTask asciidoctorTask) { diff --git a/ci/parameters.yml b/ci/parameters.yml index 0aff59284632..99ba86d913ed 100644 --- a/ci/parameters.yml +++ b/ci/parameters.yml @@ -7,7 +7,7 @@ docker-hub-repository-prefix: "spring-boot" artifactory-snapshot-repository: "libs-snapshot-local" artifactory-staging-repository: "libs-staging-local" artifactory-url: "https://repo.spring.io" -branch: "main" +branch: "3.2.x" milestone: "3.2.x" build-name: "spring-boot" concourse-url: "https://ci.spring.io" From 5ef7db9a28519af200f666987d735cbd931efaa6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 10:56:56 +0000 Subject: [PATCH 0931/1215] Start working on Spring Boot 3.3 --- README.adoc | 2 +- ci/README.adoc | 2 +- ci/parameters.yml | 2 +- ci/pipeline.yml | 4 +- eclipse/spring-boot-project.setup | 4 +- git/hooks/prepare-forward-merge | 2 +- gradle.properties | 2 +- .../spring-boot-dependencies/build.gradle | 2 +- .../jar-layered-custom/jar/src/layers.xml | 2 +- .../war-layered-custom/war/src/layers.xml | 2 +- .../src/main/xsd/layers-3.3.xsd | 100 ++++++++++++++++++ .../dependencies-layer-no-filter.xml | 2 +- .../src/test/resources/layers.xml | 2 +- .../resources/resource-layer-no-filter.xml | 2 +- 14 files changed, 115 insertions(+), 15 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-3.3.xsd diff --git a/README.adoc b/README.adoc index b603722e4e25..a0bc0a3dc0bd 100755 --- a/README.adoc +++ b/README.adoc @@ -1,4 +1,4 @@ -= Spring Boot image:https://ci.spring.io/api/v1/teams/spring-boot/pipelines/spring-boot-3.2.x/jobs/build/badge["Build Status", link="https://ci.spring.io/teams/spring-boot/pipelines/spring-boot-3.2.x?groups=Build"] image:https://badges.gitter.im/Join Chat.svg["Chat",link="https://gitter.im/spring-projects/spring-boot?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"] image:https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Gradle Enterprise", link="https://ge.spring.io/scans?&search.rootProjectNames=Spring%20Boot%20Build&search.rootProjectNames=spring-boot-build"] += Spring Boot image:https://ci.spring.io/api/v1/teams/spring-boot/pipelines/spring-boot-3.3.x/jobs/build/badge["Build Status", link="https://ci.spring.io/teams/spring-boot/pipelines/spring-boot-3.3.x?groups=Build"] image:https://badges.gitter.im/Join Chat.svg["Chat",link="https://gitter.im/spring-projects/spring-boot?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"] image:https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Gradle Enterprise", link="https://ge.spring.io/scans?&search.rootProjectNames=Spring%20Boot%20Build&search.rootProjectNames=spring-boot-build"] :docs: https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference :github: https://github.com/spring-projects/spring-boot diff --git a/ci/README.adoc b/ci/README.adoc index 5ef82f3ab88d..6b4dcd7d2e92 100644 --- a/ci/README.adoc +++ b/ci/README.adoc @@ -11,7 +11,7 @@ The pipeline can be deployed using the following command: [source] ---- -$ fly -t spring-boot set-pipeline -p spring-boot-3.2.x -c ci/pipeline.yml -l ci/parameters.yml +$ fly -t spring-boot set-pipeline -p spring-boot-3.3.x -c ci/pipeline.yml -l ci/parameters.yml ---- NOTE: This assumes that you have credhub integration configured with the appropriate diff --git a/ci/parameters.yml b/ci/parameters.yml index 0aff59284632..90a4160b10a8 100644 --- a/ci/parameters.yml +++ b/ci/parameters.yml @@ -8,7 +8,7 @@ artifactory-snapshot-repository: "libs-snapshot-local" artifactory-staging-repository: "libs-staging-local" artifactory-url: "https://repo.spring.io" branch: "main" -milestone: "3.2.x" +milestone: "3.3.x" build-name: "spring-boot" concourse-url: "https://ci.spring.io" task-timeout: 2h00m diff --git a/ci/pipeline.yml b/ci/pipeline.yml index d661686d422c..46bc9e777450 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -589,7 +589,7 @@ jobs: <<: *sdkman-task-params RELEASE_TYPE: RELEASE BRANCH: ((branch)) - LATEST_GA: true + LATEST_GA: false - name: update-homebrew-tap serial: true plan: @@ -605,7 +605,7 @@ jobs: image: ci-image file: git-repo/ci/tasks/update-homebrew-tap.yml params: - LATEST_GA: true + LATEST_GA: false - put: homebrew-tap-repo params: repository: updated-homebrew-tap-repo diff --git a/eclipse/spring-boot-project.setup b/eclipse/spring-boot-project.setup index 0e060372d33f..2b63bfbc12e9 100644 --- a/eclipse/spring-boot-project.setup +++ b/eclipse/spring-boot-project.setup @@ -11,8 +11,8 @@ xmlns:setup.workingsets="http://www.eclipse.org/oomph/setup/workingsets/1.0" xmlns:workingsets="http://www.eclipse.org/oomph/workingsets/1.0" xsi:schemaLocation="http://www.eclipse.org/oomph/setup/jdt/1.0 http://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/JDT.ecore http://www.eclipse.org/buildship/oomph/1.0 https://raw.githubusercontent.com/eclipse/buildship/master/org.eclipse.buildship.oomph/model/GradleImport-1.0.ecore http://www.eclipse.org/oomph/predicates/1.0 http://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/Predicates.ecore http://www.eclipse.org/oomph/setup/workingsets/1.0 http://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/SetupWorkingSets.ecore http://www.eclipse.org/oomph/workingsets/1.0 http://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/WorkingSets.ecore" - name="spring.boot.3.2.x" - label="Spring Boot 3.2.x"> + name="spring.boot.3.3.x" + label="Spring Boot 3.3.x"> + https://www.springframework.org/schema/layers/layers-3.3.xsd"> **/application*.* diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/layers.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/layers.xml index 41e9157bb728..cd2a48c9b4cd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/layers.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/layers.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/layers/layers-3.3.xsd"> **/application*.* diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-3.3.xsd b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-3.3.xsd new file mode 100644 index 000000000000..20219b9bd8b1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/xsd/layers-3.3.xsd @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml index b6e9af44d621..e5014dce4393 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/boot/layers/layers-3.3.xsd"> diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/layers.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/layers.xml index 81f10e26311c..7f12e4fc63d8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/layers.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/layers.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/boot/layers/layers-3.3.xsd"> META-INF/resources/** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml index ebfb721fb7c2..bb698a6323e5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/boot/layers/layers-3.3.xsd"> From e58960611267ddbf3742352b1c7518f7e74a16cd Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 13:25:26 +0000 Subject: [PATCH 0932/1215] Start building against Micrometer 1.12.2 snapshots See gh-38978 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 04b2c19e5e26..e45a7bd9fbbb 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -995,7 +995,7 @@ bom { ] } } - library("Micrometer", "1.12.1") { + library("Micrometer", "1.12.2-SNAPSHOT") { considerSnapshots() group("io.micrometer") { modules = [ From 44f5a8bfe9e5910e0a55de134b9e427c48e8d8de Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 13:25:31 +0000 Subject: [PATCH 0933/1215] Start building against Micrometer Tracing 1.2.2 snapshots See gh-38979 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e45a7bd9fbbb..541c31acf4c5 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1008,7 +1008,7 @@ bom { ] } } - library("Micrometer Tracing", "1.2.1") { + library("Micrometer Tracing", "1.2.2-SNAPSHOT") { considerSnapshots() calendarName = "Tracing" group("io.micrometer") { From 6b59c010033dcbc0386af1b9caa364b069b3f596 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 13:25:36 +0000 Subject: [PATCH 0934/1215] Start building against Reactor Bom 2023.0.2 snapshots See gh-38980 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 541c31acf4c5..4a14a4107f5c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1323,7 +1323,7 @@ bom { ] } } - library("Reactor Bom", "2023.0.1") { + library("Reactor Bom", "2023.0.2-SNAPSHOT") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From d7fc9a66b4b2f5dc627ac90d90038038fa4ad885 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 13:25:40 +0000 Subject: [PATCH 0935/1215] Start building against Spring Data Bom 2023.1.2 snapshots See gh-38981 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 4a14a4107f5c..192e5b8c7072 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1515,7 +1515,7 @@ bom { ] } } - library("Spring Data Bom", "2023.1.1") { + library("Spring Data Bom", "2023.1.2-SNAPSHOT") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From 4c0f631d93698f94885302bc81a0aa5a7a19cf9f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 13:25:45 +0000 Subject: [PATCH 0936/1215] Start building against Spring Framework 6.1.3 snapshots See gh-38982 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 35f1f0cd978f..cb25a7eb5641 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ junitJupiterVersion=5.10.1 kotlinVersion=1.9.21 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.28 -springFrameworkVersion=6.1.2 +springFrameworkVersion=6.1.3-SNAPSHOT tomcatVersion=10.1.17 kotlin.stdlib.default.dependency=false From 2fb6a2eef4ab36ea41cbfc2f2ee90642c9ef542b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 14:15:35 +0000 Subject: [PATCH 0937/1215] Consider snapshots when upgrading Spring Pulsar Closes gh-38992 --- spring-boot-project/spring-boot-dependencies/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 192e5b8c7072..85430707fe0b 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1579,6 +1579,7 @@ bom { } } library("Spring Pulsar", "1.0.1") { + considerSnapshots() group("org.springframework.pulsar") { modules = [ "spring-pulsar", From d94661f91b1f99bb181a84c70e03a8d7396cc06b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 14:16:53 +0000 Subject: [PATCH 0938/1215] Start building against Spring Pulsar 1.0.2 snapshots See gh-38994 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 85430707fe0b..c51ab9e092b4 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1578,7 +1578,7 @@ bom { ] } } - library("Spring Pulsar", "1.0.1") { + library("Spring Pulsar", "1.0.2-SNAPSHOT") { considerSnapshots() group("org.springframework.pulsar") { modules = [ From 2e7e8cf61ae4a681ae17cce4333099a900f1270e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 14:32:47 +0000 Subject: [PATCH 0939/1215] Start building against Micrometer 1.13.0 snapshots See gh-38984 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 571ef5fa716b..2f4564715282 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -995,7 +995,7 @@ bom { ] } } - library("Micrometer", "1.12.1") { + library("Micrometer", "1.13.0-SNAPSHOT") { considerSnapshots() group("io.micrometer") { modules = [ From f31bbbbeaa0f535b74bcf9aaf037b6799e2cec21 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 14:32:47 +0000 Subject: [PATCH 0940/1215] Start building against Micrometer Tracing 1.3.0 snapshots See gh-38985 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 2f4564715282..645e9eab2fe2 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1008,7 +1008,7 @@ bom { ] } } - library("Micrometer Tracing", "1.2.1") { + library("Micrometer Tracing", "1.3.0-SNAPSHOT") { considerSnapshots() calendarName = "Tracing" group("io.micrometer") { From edbee44ab34b27071b1331dce493a5495f9e1803 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 14:32:48 +0000 Subject: [PATCH 0941/1215] Start building against Reactor Bom 2023.0.2 snapshots See gh-38986 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 645e9eab2fe2..39fdb5a86746 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1323,7 +1323,7 @@ bom { ] } } - library("Reactor Bom", "2023.0.1") { + library("Reactor Bom", "2023.0.2-SNAPSHOT") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From 84b2b3793290acd953f70f175266d957d9910016 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 14:32:49 +0000 Subject: [PATCH 0942/1215] Start building against Spring Authorization Server 1.3.0 snapshots See gh-38987 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 39fdb5a86746..62f1b3280594 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1499,7 +1499,7 @@ bom { ] } } - library("Spring Authorization Server", "1.2.1") { + library("Spring Authorization Server", "1.3.0-SNAPSHOT") { considerSnapshots() group("org.springframework.security") { modules = [ From e0cceed2e60560b428d685b42bced1f8f9119a5f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 14:32:49 +0000 Subject: [PATCH 0943/1215] Start building against Spring Data Bom 2023.1.2 snapshots See gh-38988 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 62f1b3280594..327ecf1440fe 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1515,7 +1515,7 @@ bom { ] } } - library("Spring Data Bom", "2023.1.1") { + library("Spring Data Bom", "2023.1.2-SNAPSHOT") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From 4c4f0392fa74ed7cbdb4c7dfbca2d4c2a9c62cdf Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 14:32:50 +0000 Subject: [PATCH 0944/1215] Start building against Spring Framework 6.1.3 snapshots See gh-38989 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c9aa80ad47df..2f288c7a8715 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ junitJupiterVersion=5.10.1 kotlinVersion=1.9.21 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.28 -springFrameworkVersion=6.1.2 +springFrameworkVersion=6.1.3-SNAPSHOT tomcatVersion=10.1.17 kotlin.stdlib.default.dependency=false From ec8920ccfd99db7fde6a2ffdf61c87c3fcf09764 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 14:32:54 +0000 Subject: [PATCH 0945/1215] Start building against Spring Pulsar 1.0.2 snapshots See gh-38995 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 327ecf1440fe..37b0d94e9ee9 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1578,7 +1578,7 @@ bom { ] } } - library("Spring Pulsar", "1.0.1") { + library("Spring Pulsar", "1.0.2-SNAPSHOT") { considerSnapshots() group("org.springframework.pulsar") { modules = [ From f98f4e20c828c8655a552f9606f1f405b6386439 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 14:32:55 +0000 Subject: [PATCH 0946/1215] Start building against Spring Security 6.3.0 snapshots See gh-38990 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 37b0d94e9ee9..858dec0c50a0 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1605,7 +1605,7 @@ bom { ] } } - library("Spring Security", "6.2.1") { + library("Spring Security", "6.3.0-SNAPSHOT") { considerSnapshots() group("org.springframework.security") { imports = [ From c19c18f998dbf3f41686801dd603b904e10b0c3a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 14:32:56 +0000 Subject: [PATCH 0947/1215] Start building against Spring Session 3.3.0 snapshots See gh-38991 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 858dec0c50a0..69df5fc8cea9 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1613,7 +1613,7 @@ bom { ] } } - library("Spring Session", "3.2.1") { + library("Spring Session", "3.3.0-SNAPSHOT") { considerSnapshots() prohibit { startsWith(["Apple-", "Bean-", "Corn-", "Dragonfruit-"]) From a1c7c0bcccec52e403eda576af12d77c53f528c9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:31:37 +0000 Subject: [PATCH 0948/1215] Upgrade to AssertJ 3.25.1 Closes gh-38997 --- gradle.properties | 2 +- .../mongo/MongoMetricsAutoConfigurationTests.java | 7 ++++--- .../tracing/BraveAutoConfigurationTests.java | 8 ++++---- ...ndpointChildContextConfigurationIntegrationTests.java | 6 ++++-- .../boot/actuate/endpoint/jmx/EndpointMBeanTests.java | 5 +++-- .../couchbase/CouchbaseAutoConfigurationTests.java | 6 +++--- .../graphql/GraphQlAutoConfigurationTests.java | 6 ++++-- .../kafka/KafkaAutoConfigurationIntegrationTests.java | 5 +++-- .../pulsar/PulsarAutoConfigurationTests.java | 7 ++++--- .../OAuth2ResourceServerAutoConfigurationTests.java | 4 ++-- ...actionManagerCustomizationAutoConfigurationTests.java | 9 ++++++--- .../web/reactive/WebFluxAutoConfigurationTests.java | 8 +++++--- .../boot/test/json/AbstractJsonMarshalTesterTests.java | 5 +++-- .../boot/test/json/GsonTesterIntegrationTests.java | 5 +++-- .../boot/test/json/JacksonTesterIntegrationTests.java | 5 +++-- .../boot/context/config/ConfigDataEnvironmentTests.java | 5 +++-- .../boot/context/config/ConfigDataLoadersTests.java | 5 +++-- .../boot/json/AbstractJsonParserTests.java | 7 ++++--- 18 files changed, 62 insertions(+), 43 deletions(-) diff --git a/gradle.properties b/gradle.properties index 2f288c7a8715..7181ac933d9d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ org.gradle.caching=true org.gradle.parallel=true org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 -assertjVersion=3.24.2 +assertjVersion=3.25.1 commonsCodecVersion=1.16.0 hamcrestVersion=2.2 jacksonVersion=2.15.3 diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/mongo/MongoMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/mongo/MongoMetricsAutoConfigurationTests.java index 7c3d870c3baa..5c5757730d15 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/mongo/MongoMetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/mongo/MongoMetricsAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -29,6 +29,7 @@ import io.micrometer.core.instrument.binder.mongodb.MongoConnectionPoolTagsProvider; import io.micrometer.core.instrument.binder.mongodb.MongoMetricsCommandListener; import io.micrometer.core.instrument.binder.mongodb.MongoMetricsConnectionPoolListener; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; @@ -62,7 +63,7 @@ void whenThereIsAMeterRegistryThenMetricsCommandListenerIsAdded() { assertThat(context).hasSingleBean(MongoMetricsCommandListener.class); assertThat(getActualMongoClientSettingsUsedToConstructClient(context)) .extracting(MongoClientSettings::getCommandListeners) - .asList() + .asInstanceOf(InstanceOfAssertFactories.LIST) .containsExactly(context.getBean(MongoMetricsCommandListener.class)); assertThat(getMongoCommandTagsProviderUsedToConstructListener(context)) .isInstanceOf(DefaultMongoCommandTagsProvider.class); @@ -168,7 +169,7 @@ private ContextConsumer assertThatMetricsCommandLi assertThat(context).doesNotHaveBean(MongoMetricsCommandListener.class); assertThat(getActualMongoClientSettingsUsedToConstructClient(context)) .extracting(MongoClientSettings::getCommandListeners) - .asList() + .asInstanceOf(InstanceOfAssertFactories.LIST) .isEmpty(); }; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java index 246a6a184c10..ee476a85b619 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -333,13 +333,13 @@ void compositeSpanHandlerUsesFilterPredicateAndReportersInOrder() { .getBean(CompositeSpanHandlerComponentsConfiguration.class); CompositeSpanHandler composite = context.getBean(CompositeSpanHandler.class); assertThat(composite).extracting("spanFilters") - .asList() + .asInstanceOf(InstanceOfAssertFactories.LIST) .containsExactly(components.filter1, components.filter2); assertThat(composite).extracting("filters") - .asList() + .asInstanceOf(InstanceOfAssertFactories.LIST) .containsExactly(components.predicate2, components.predicate1); assertThat(composite).extracting("reporters") - .asList() + .asInstanceOf(InstanceOfAssertFactories.LIST) .containsExactly(components.reporter1, components.reporter3, components.reporter2); }); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationIntegrationTests.java index 4dfd5c0732e1..ff43ecea4aa0 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -26,6 +26,7 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import reactor.core.publisher.Mono; @@ -143,7 +144,8 @@ void errorEndpointIsUsedWithRestControllerEndpointOnBindingError() { (value) -> assertThat(value).asString().contains("MethodArgumentNotValidException")); assertThat(body).hasEntrySatisfying("message", (value) -> assertThat(value).asString().contains("Validation failed")); - assertThat(body).hasEntrySatisfying("errors", (value) -> assertThat(value).asList().isNotEmpty()); + assertThat(body).hasEntrySatisfying("errors", + (value) -> assertThat(value).asInstanceOf(InstanceOfAssertFactories.LIST).isNotEmpty()); })); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanTests.java index 5fa8a81dd3b4..442bd367c948 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/jmx/EndpointMBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -26,6 +26,7 @@ import javax.management.MBeanInfo; import javax.management.ReflectionException; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -165,7 +166,7 @@ void invokeWhenFluxResultShouldCollectToMonoListAndBlockOnMono() throws MBeanExc new TestJmxOperation((arguments) -> Flux.just("flux", "result"))); EndpointMBean bean = new EndpointMBean(this.responseMapper, null, endpoint); Object result = bean.invoke("testOperation", NO_PARAMS, NO_SIGNATURE); - assertThat(result).asList().containsExactly("flux", "result"); + assertThat(result).asInstanceOf(InstanceOfAssertFactories.LIST).containsExactly("flux", "result"); } @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationTests.java index 3dc81e27767a..c76ea994d6cb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -83,7 +83,7 @@ void shouldUseCustomConnectionDetailsWhenDefined() { .doesNotHaveBean(PropertiesCouchbaseConnectionDetails.class); Cluster cluster = context.getBean(Cluster.class); assertThat(cluster.core()).extracting("connectionString.hosts") - .asList() + .asInstanceOf(InstanceOfAssertFactories.LIST) .extractingResultOf("host") .containsExactly("couchbase.example.com"); }); @@ -109,7 +109,7 @@ void connectionDetailsShouldOverrideProperties() { assertThat(context).hasSingleBean(ClusterEnvironment.class).hasSingleBean(Cluster.class); Cluster cluster = context.getBean(Cluster.class); assertThat(cluster.core()).extracting("connectionString.hosts") - .asList() + .asInstanceOf(InstanceOfAssertFactories.LIST) .extractingResultOf("host") .containsExactly("couchbase.example.com"); }); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java index 4d121314b5ff..dd0f97c7a4a2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -126,7 +126,9 @@ void shouldConfigureDataFetcherExceptionResolvers() { assertThat(graphQL.getQueryStrategy()).extracting("dataFetcherExceptionHandler") .satisfies((exceptionHandler) -> { assertThat(exceptionHandler.getClass().getName()).endsWith("ExceptionResolversExceptionHandler"); - assertThat(exceptionHandler).extracting("resolvers").asList().hasSize(2); + assertThat(exceptionHandler).extracting("resolvers") + .asInstanceOf(InstanceOfAssertFactories.LIST) + .hasSize(2); }); }); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationIntegrationTests.java index 8b0668ffe8dd..02ff53618300 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -28,6 +28,7 @@ import org.apache.kafka.streams.kstream.KStream; import org.apache.kafka.streams.kstream.KTable; import org.apache.kafka.streams.kstream.Materialized; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; @@ -115,7 +116,7 @@ void testEndToEndWithRetryTopics() throws Exception { assertThat(listener).extracting(RetryListener::getKey, RetryListener::getReceived) .containsExactly("foo", "bar"); assertThat(listener).extracting(RetryListener::getTopics) - .asList() + .asInstanceOf(InstanceOfAssertFactories.LIST) .hasSize(5) .containsSequence("testRetryTopic", "testRetryTopic-retry-0", "testRetryTopic-retry-1", "testRetryTopic-retry-2"); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java index 7e56f4129ead..9cc201bbbdab 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -27,6 +27,7 @@ import org.apache.pulsar.client.api.ReaderBuilder; import org.apache.pulsar.client.api.interceptor.ProducerInterceptor; import org.apache.pulsar.common.schema.SchemaType; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledForJreRange; @@ -287,7 +288,7 @@ void whenHasUseDefinedProducerInterceptorInjectsBean() { this.contextRunner.withBean("customProducerInterceptor", ProducerInterceptor.class, () -> interceptor) .run((context) -> assertThat(context).getBean(PulsarTemplate.class) .extracting("interceptors") - .asList() + .asInstanceOf(InstanceOfAssertFactories.LIST) .contains(interceptor)); } @@ -296,7 +297,7 @@ void whenHasUseDefinedProducerInterceptorsInjectsBeansInCorrectOrder() { this.contextRunner.withUserConfiguration(InterceptorTestConfiguration.class) .run((context) -> assertThat(context).getBean(PulsarTemplate.class) .extracting("interceptors") - .asList() + .asInstanceOf(InstanceOfAssertFactories.LIST) .containsExactly(context.getBean("interceptorBar"), context.getBean("interceptorFoo"))); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java index b7aa1a5ec67b..af394c05585d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -424,7 +424,7 @@ void autoConfigurationWhenJwkSetUriAndIntrospectionUriAvailable() { assertThat(context).hasSingleBean(OpaqueTokenIntrospector.class); assertThat(context).hasSingleBean(JwtDecoder.class); assertThat(getBearerTokenFilter(context)).extracting("authenticationManagerResolver.arg$1.providers") - .asList() + .asInstanceOf(InstanceOfAssertFactories.LIST) .hasAtLeastOneElementOfType(JwtAuthenticationProvider.class); }); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfigurationTests.java index 24bf90e0b27b..16c271f64af4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/transaction/TransactionManagerCustomizationAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -18,6 +18,7 @@ import java.util.Collections; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -42,7 +43,7 @@ void autoConfiguresTransactionManagerCustomizers() { this.contextRunner.run((context) -> { TransactionManagerCustomizers customizers = context.getBean(TransactionManagerCustomizers.class); assertThat(customizers).extracting("customizers") - .asList() + .asInstanceOf(InstanceOfAssertFactories.LIST) .hasSize(2) .hasAtLeastOneElementOfType(TransactionProperties.class) .hasAtLeastOneElementOfType(ExecutionListenersTransactionManagerCustomizer.class); @@ -54,7 +55,9 @@ void autoConfiguredTransactionManagerCustomizersBacksOff() { this.contextRunner.withUserConfiguration(CustomTransactionManagerCustomizersConfiguration.class) .run((context) -> { TransactionManagerCustomizers customizers = context.getBean(TransactionManagerCustomizers.class); - assertThat(customizers).extracting("customizers").asList().isEmpty(); + assertThat(customizers).extracting("customizers") + .asInstanceOf(InstanceOfAssertFactories.LIST) + .isEmpty(); }); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java index cf40c58a52c9..2340013df8ea 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -198,7 +198,9 @@ void shouldMapResourcesToCustomPath() { SimpleUrlHandlerMapping hm = context.getBean("resourceHandlerMapping", SimpleUrlHandlerMapping.class); assertThat(hm.getUrlMap().get("/static/**")).isInstanceOf(ResourceWebHandler.class); ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap().get("/static/**"); - assertThat(staticHandler).extracting("locationValues").asList().hasSize(4); + assertThat(staticHandler).extracting("locationValues") + .asInstanceOf(InstanceOfAssertFactories.LIST) + .hasSize(4); }); } @@ -599,7 +601,7 @@ void userConfigurersCanBeOrderedBeforeOrAfterTheAutoConfiguredConfigurer() { .withBean(LowPrecedenceConfigurer.class, LowPrecedenceConfigurer::new) .run((context) -> assertThat(context.getBean(DelegatingWebFluxConfiguration.class)) .extracting("configurers.delegates") - .asList() + .asInstanceOf(InstanceOfAssertFactories.LIST) .extracting((configurer) -> (Class) configurer.getClass()) .containsExactly(HighPrecedenceConfigurer.class, WebFluxConfig.class, LowPrecedenceConfigurer.class)); } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/json/AbstractJsonMarshalTesterTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/json/AbstractJsonMarshalTesterTests.java index 406ff1cc1c98..f680b5f78a33 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/json/AbstractJsonMarshalTesterTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/json/AbstractJsonMarshalTesterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -28,6 +28,7 @@ import java.util.List; import java.util.Map; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -152,7 +153,7 @@ void readReaderShouldReturnObject() throws Exception { void parseListShouldReturnContent() throws Exception { ResolvableType type = ResolvableTypes.get("listOfExampleObject"); AbstractJsonMarshalTester tester = createTester(type); - assertThat(tester.parse(ARRAY_JSON)).asList().containsOnly(OBJECT); + assertThat(tester.parse(ARRAY_JSON)).asInstanceOf(InstanceOfAssertFactories.LIST).containsOnly(OBJECT); } @Test diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/json/GsonTesterIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/json/GsonTesterIntegrationTests.java index ed1aca17d886..b2c02051d59b 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/json/GsonTesterIntegrationTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/json/GsonTesterIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -21,6 +21,7 @@ import java.util.Map; import com.google.gson.Gson; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -61,7 +62,7 @@ void typicalTest() throws Exception { @Test void typicalListTest() throws Exception { String example = "[" + JSON + "]"; - assertThat(this.listJson.parse(example)).asList().hasSize(1); + assertThat(this.listJson.parse(example)).asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(1); assertThat(this.listJson.parse(example).getObject().get(0).getName()).isEqualTo("Spring"); } diff --git a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/json/JacksonTesterIntegrationTests.java b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/json/JacksonTesterIntegrationTests.java index 5a97f9f7c056..7d33b235b7ea 100644 --- a/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/json/JacksonTesterIntegrationTests.java +++ b/spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/json/JacksonTesterIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.springframework.core.io.ByteArrayResource; @@ -63,7 +64,7 @@ void typicalTest() throws Exception { void typicalListTest() throws Exception { JacksonTester.initFields(this, new ObjectMapper()); String example = "[" + JSON + "]"; - assertThat(this.listJson.parse(example)).asList().hasSize(1); + assertThat(this.listJson.parse(example)).asInstanceOf(InstanceOfAssertFactories.LIST).hasSize(1); assertThat(this.listJson.parse(example).getObject().get(0).getName()).isEqualTo("Spring"); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentTests.java index 4b046f0d8307..fdad67f2e08c 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -27,6 +27,7 @@ import java.util.Map; import java.util.function.Supplier; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; @@ -354,7 +355,7 @@ public Enumeration getResources(String name) throws IOException { TestConfigDataEnvironment configDataEnvironment = new TestConfigDataEnvironment(this.logFactory, this.bootstrapContext, this.environment, resourceLoader, this.additionalProfiles, null); assertThat(configDataEnvironment).extracting("loaders.loaders") - .asList() + .asInstanceOf(InstanceOfAssertFactories.LIST) .extracting((item) -> (Class) item.getClass()) .containsOnly(SeparateClassLoaderConfigDataLoader.class); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLoadersTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLoadersTests.java index 50b121fcc0b8..b488be53ea77 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLoadersTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLoadersTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -23,6 +23,7 @@ import java.util.function.Supplier; import org.apache.commons.logging.Log; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -65,7 +66,7 @@ void createWhenLoaderHasDeferredLogFactoryParameterInjectsDeferredLogFactory() { ConfigDataLoaders loaders = new ConfigDataLoaders(this.logFactory, this.bootstrapContext, springFactoriesLoader); assertThat(loaders).extracting("loaders") - .asList() + .asInstanceOf(InstanceOfAssertFactories.LIST) .satisfies(this::containsValidDeferredLogFactoryConfigDataLoader); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/json/AbstractJsonParserTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/json/AbstractJsonParserTests.java index 446d0ffe236b..fed13ff2b31c 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/json/AbstractJsonParserTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/json/AbstractJsonParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.springframework.util.StreamUtils; @@ -105,7 +106,7 @@ void mapOfLists() { .parseMap("{\"foo\":[{\"foo\":\"bar\",\"spam\":1},{\"foo\":\"baz\",\"spam\":2}]}"); assertThat(map).hasSize(1); assertThat(((List) map.get("foo"))).hasSize(2); - assertThat(map.get("foo")).asList().allMatch(Map.class::isInstance); + assertThat(map.get("foo")).asInstanceOf(InstanceOfAssertFactories.LIST).allMatch(Map.class::isInstance); } @SuppressWarnings("unchecked") @@ -115,7 +116,7 @@ void nestedLeadingAndTrailingWhitespace() { .parseMap(" {\"foo\": [ { \"foo\" : \"bar\" , \"spam\" : 1 } , { \"foo\" : \"baz\" , \"spam\" : 2 } ] } "); assertThat(map).hasSize(1); assertThat(((List) map.get("foo"))).hasSize(2); - assertThat(map.get("foo")).asList().allMatch(Map.class::isInstance); + assertThat(map.get("foo")).asInstanceOf(InstanceOfAssertFactories.LIST).allMatch(Map.class::isInstance); } @Test From 3414f9c02e19b1955cc2615ec20f31ce020d8280 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:31:42 +0000 Subject: [PATCH 0949/1215] Upgrade to Brave 5.17.0 Closes gh-38998 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 69df5fc8cea9..db6140902c7c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -109,7 +109,7 @@ bom { ] } } - library("Brave", "5.16.0") { + library("Brave", "5.17.0") { group("io.zipkin.brave") { imports = [ "brave-bom" From 6ef8dc87d7849b4e48c3a038e7f7953a41f149b4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:31:47 +0000 Subject: [PATCH 0950/1215] Upgrade to Build Helper Maven Plugin 3.5.0 Closes gh-38999 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index db6140902c7c..6a15cbb3b2bf 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -116,7 +116,7 @@ bom { ] } } - library("Build Helper Maven Plugin", "3.4.0") { + library("Build Helper Maven Plugin", "3.5.0") { group("org.codehaus.mojo") { plugins = [ "build-helper-maven-plugin" From a220c5536efa24d9f5b0665b46f1026201bf9fdc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:31:51 +0000 Subject: [PATCH 0951/1215] Upgrade to Byte Buddy 1.14.11 Closes gh-39000 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 6a15cbb3b2bf..757805fcc090 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -123,7 +123,7 @@ bom { ] } } - library("Byte Buddy", "1.14.10") { + library("Byte Buddy", "1.14.11") { group("net.bytebuddy") { modules = [ "byte-buddy", From 93025a0fba5669b65e0fe3a3d6862beb29f5ba5d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:31:56 +0000 Subject: [PATCH 0952/1215] Upgrade to Classmate 1.7.0 Closes gh-39001 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 757805fcc090..5f5f11704a15 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -163,7 +163,7 @@ bom { ] } } - library("Classmate", "1.6.0") { + library("Classmate", "1.7.0") { group("com.fasterxml") { modules = [ "classmate" From 6bd042e83bece976a4581818eb9998881f459131 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:32:01 +0000 Subject: [PATCH 0953/1215] Upgrade to Commons DBCP2 2.11.0 Closes gh-39002 --- .../jdbc/DataSourceAutoConfigurationTests.java | 6 +++--- .../Dbcp2JdbcConnectionDetailsBeanPostProcessorTests.java | 4 ++-- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- .../org/springframework/boot/jdbc/DataSourceBuilder.java | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java index 1e2cbca31086..2c1c29897228 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -267,8 +267,8 @@ void dbcp2UsesCustomConnectionDetailsWhenDefined() { DataSource dataSource = context.getBean(DataSource.class); assertThat(dataSource).asInstanceOf(InstanceOfAssertFactories.type(BasicDataSource.class)) .satisfies((dbcp2) -> { - assertThat(dbcp2.getUsername()).isEqualTo("user-1"); - assertThat(dbcp2.getPassword()).isEqualTo("password-1"); + assertThat(dbcp2.getUserName()).isEqualTo("user-1"); + assertThat(dbcp2).extracting("password").isEqualTo("password-1"); assertThat(dbcp2.getDriverClassName()).isEqualTo(DatabaseDriver.POSTGRESQL.getDriverClassName()); assertThat(dbcp2.getUrl()).isEqualTo("jdbc:customdb://customdb.example.com:12345/database-1"); }); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/Dbcp2JdbcConnectionDetailsBeanPostProcessorTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/Dbcp2JdbcConnectionDetailsBeanPostProcessorTests.java index fd3aef26ad38..cbabcf054d42 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/Dbcp2JdbcConnectionDetailsBeanPostProcessorTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/Dbcp2JdbcConnectionDetailsBeanPostProcessorTests.java @@ -42,8 +42,8 @@ void setUsernamePasswordUrlAndDriverClassName() { new Dbcp2JdbcConnectionDetailsBeanPostProcessor(null).processDataSource(dataSource, new TestJdbcConnectionDetails()); assertThat(dataSource.getUrl()).isEqualTo("jdbc:customdb://customdb.example.com:12345/database-1"); - assertThat(dataSource.getUsername()).isEqualTo("user-1"); - assertThat(dataSource.getPassword()).isEqualTo("password-1"); + assertThat(dataSource.getUserName()).isEqualTo("user-1"); + assertThat(dataSource).extracting("password").isEqualTo("password-1"); assertThat(dataSource.getDriverClassName()).isEqualTo(DatabaseDriver.POSTGRESQL.getDriverClassName()); } diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5f5f11704a15..60e63dfbb227 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -177,7 +177,7 @@ bom { ] } } - library("Commons DBCP2", "2.10.0") { + library("Commons DBCP2", "2.11.0") { group("org.apache.commons") { modules = [ "commons-dbcp2" { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java index 60cc940a8409..65ce19cdec7d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java @@ -644,8 +644,8 @@ private static class MappedDbcp2DataSource extends MappedDataSourceProperties Date: Thu, 4 Jan 2024 16:32:05 +0000 Subject: [PATCH 0954/1215] Upgrade to Commons Lang3 3.14.0 Closes gh-39003 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 60e63dfbb227..6305072973c7 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -186,7 +186,7 @@ bom { ] } } - library("Commons Lang3", "3.13.0") { + library("Commons Lang3", "3.14.0") { group("org.apache.commons") { modules = [ "commons-lang3" From a11ecfffade4f643feb665d9f561ee91af59dc77 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:32:15 +0000 Subject: [PATCH 0955/1215] Upgrade to Groovy 4.0.17 Closes gh-39005 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 6305072973c7..3e81a1631c8e 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -339,7 +339,7 @@ bom { ] } } - library("Groovy", "4.0.16") { + library("Groovy", "4.0.17") { group("org.apache.groovy") { imports = [ "groovy-bom" From 65bbfdcfe8dc1ca70224b09258e3a8c9819c0cd7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:32:19 +0000 Subject: [PATCH 0956/1215] Upgrade to HikariCP 5.1.0 Closes gh-39006 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 3e81a1631c8e..d1d27c37684c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -406,7 +406,7 @@ bom { ] } } - library("HikariCP", "5.0.1") { + library("HikariCP", "5.1.0") { group("com.zaxxer") { modules = [ "HikariCP" From 31f3f31ac1c2e4f3cc5a332de56ff13f05f9434a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:32:24 +0000 Subject: [PATCH 0957/1215] Upgrade to HttpClient5 5.3 Closes gh-39007 --- .../spring-boot-dependencies/build.gradle | 2 +- .../test/web/client/TestRestTemplate.java | 14 ++++++++++++-- .../TomcatServletWebServerFactoryTests.java | 3 +-- .../AbstractServletWebServerFactoryTests.java | 19 +++++++++++++++---- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d1d27c37684c..0b8f23e4b9c2 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -438,7 +438,7 @@ bom { ] } } - library("HttpClient5", "5.2.3") { + library("HttpClient5", "5.3") { group("org.apache.httpcomponents.client5") { modules = [ "httpclient5", diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplate.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplate.java index 7ec967c573de..e9d7b8e72195 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplate.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplate.java @@ -21,6 +21,7 @@ import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; import java.time.Duration; import java.util.Arrays; import java.util.HashSet; @@ -40,11 +41,11 @@ import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder; -import org.apache.hc.client5.http.ssl.TrustSelfSignedStrategy; import org.apache.hc.core5.http.io.SocketConfig; import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.http.ssl.TLS; import org.apache.hc.core5.ssl.SSLContextBuilder; +import org.apache.hc.core5.ssl.TrustStrategy; import org.springframework.boot.web.client.ClientHttpRequestFactorySettings; import org.springframework.boot.web.client.RestTemplateBuilder; @@ -993,7 +994,7 @@ public enum HttpClientOption { ENABLE_REDIRECTS, /** - * Use a {@link SSLConnectionSocketFactory} with {@link TrustSelfSignedStrategy}. + * Use a {@link SSLConnectionSocketFactory} that trusts self-signed certificates. */ SSL @@ -1085,4 +1086,13 @@ public void handleError(ClientHttpResponse response) throws IOException { } + private static class TrustSelfSignedStrategy implements TrustStrategy { + + @Override + public boolean isTrusted(X509Certificate[] chain, String authType) { + return chain.length == 1; + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java index b9a9f58e5c7f..3fe7058a52be 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -64,7 +64,6 @@ import org.apache.hc.client5.http.classic.HttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; -import org.apache.hc.client5.http.ssl.TrustSelfSignedStrategy; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.NoHttpResponseException; import org.apache.hc.core5.ssl.SSLContextBuilder; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java index 29862093ea17..37ea7ad293e1 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -88,7 +88,6 @@ import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; -import org.apache.hc.client5.http.ssl.TrustSelfSignedStrategy; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.io.HttpClientResponseHandler; import org.apache.hc.core5.http.protocol.HttpContext; @@ -1437,7 +1436,7 @@ private String setUpFactoryForCompression(int contentSize, String[] mimeTypes, S compression.setExcludedUserAgents(excludedUserAgents); } factory.setCompression(compression); - factory.addInitializers(new ServletRegistrationBean(new HttpServlet() { + factory.addInitializers(new ServletRegistrationBean<>(new HttpServlet() { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { @@ -1671,7 +1670,7 @@ private SerialNumberValidatingTrustSelfSignedStrategy(String serialNumber) { } @Override - public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException { + public boolean isTrusted(X509Certificate[] chain, String authType) { String hexSerialNumber = chain[0].getSerialNumber().toString(16); boolean isMatch = hexSerialNumber.equalsIgnoreCase(this.serialNumber); return super.isTrusted(chain, authType) && isMatch; @@ -1815,4 +1814,16 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se } + protected static class TrustSelfSignedStrategy implements TrustStrategy { + + public TrustSelfSignedStrategy() { + } + + @Override + public boolean isTrusted(X509Certificate[] chain, String authType) { + return chain.length == 1; + } + + } + } From dca46c75c955454f90667682efca7b2e91fd8345 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:32:29 +0000 Subject: [PATCH 0958/1215] Upgrade to InfluxDB Java 2.24 Closes gh-39008 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 0b8f23e4b9c2..d414d31942b4 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -472,7 +472,7 @@ bom { ] } } - library("InfluxDB Java", "2.23") { + library("InfluxDB Java", "2.24") { group("org.influxdb") { modules = [ "influxdb-java" From be4ed3ffc0d2b4f3b2c39a595b16cd5c74b22c79 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:32:34 +0000 Subject: [PATCH 0959/1215] Upgrade to Jackson Bom 2.16.1 Closes gh-39009 --- buildSrc/build.gradle | 5 ++--- gradle.properties | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 4970e29102db..f570d3c3722a 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -17,11 +17,11 @@ def versions = [:] new File(projectDir.parentFile, "gradle.properties").withInputStream { def properties = new Properties() properties.load(it) - ["assertj", "commonsCodec", "hamcrest", "jackson", "junitJupiter", - "kotlin", "maven"].each { + ["assertj", "commonsCodec", "hamcrest", "junitJupiter", "kotlin", "maven"].each { versions[it] = properties[it + "Version"] } } +versions["jackson"] = "2.15.3" versions["springFramework"] = "6.0.12" ext.set("versions", versions) if (versions.springFramework.contains("-")) { @@ -136,4 +136,3 @@ eclipse.classpath.file.whenMerged { jreEntry.entryAttributes['module'] = 'true' jreEntry.entryAttributes['limit-modules'] = 'java.base' } - diff --git a/gradle.properties b/gradle.properties index 7181ac933d9d..1ad2ed18a868 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 assertjVersion=3.25.1 commonsCodecVersion=1.16.0 hamcrestVersion=2.2 -jacksonVersion=2.15.3 +jacksonVersion=2.16.1 junitJupiterVersion=5.10.1 kotlinVersion=1.9.21 mavenVersion=3.9.4 From fc478d5c25db5c27b408fa701c6bab670b05a70d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:32:43 +0000 Subject: [PATCH 0960/1215] Upgrade to JMustache 1.16 Closes gh-39011 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d414d31942b4..85f19e8dec20 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -690,7 +690,7 @@ bom { ] } } - library("JMustache", "1.15") { + library("JMustache", "1.16") { group("com.samskivert") { modules = [ "jmustache" From a01977b888182c28e339821634f1116e1e7e657a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:32:48 +0000 Subject: [PATCH 0961/1215] Upgrade to jOOQ 3.19.1 Closes gh-39012 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 85f19e8dec20..15dc8a62f043 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -697,7 +697,7 @@ bom { ] } } - library("jOOQ", "3.18.7") { + library("jOOQ", "3.19.1") { group("org.jooq") { modules = [ "jooq", From 7309d65b0941f562652212e90bc6def53988c119 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:32:52 +0000 Subject: [PATCH 0962/1215] Upgrade to Kotlin 1.9.22 Closes gh-39013 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 1ad2ed18a868..16eccbb3f6f8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ commonsCodecVersion=1.16.0 hamcrestVersion=2.2 jacksonVersion=2.16.1 junitJupiterVersion=5.10.1 -kotlinVersion=1.9.21 +kotlinVersion=1.9.22 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.28 springFrameworkVersion=6.1.3-SNAPSHOT From b2c98a028e3af3b0a03e31294d414a35ca0576c6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:32:57 +0000 Subject: [PATCH 0963/1215] Upgrade to Liquibase 4.25.1 Closes gh-39014 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 15dc8a62f043..ad55b5520c69 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -828,7 +828,7 @@ bom { ] } } - library("Liquibase", "4.24.0") { + library("Liquibase", "4.25.1") { group("org.liquibase") { modules = [ "liquibase-cdi", From ce08985bd47dfda647c4e2308aa1cf71ea7a2865 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:33:01 +0000 Subject: [PATCH 0964/1215] Upgrade to Log4j2 2.22.1 Closes gh-39015 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index ad55b5520c69..eba90f5d9e90 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -839,7 +839,7 @@ bom { ] } } - library("Log4j2", "2.21.1") { + library("Log4j2", "2.22.1") { group("org.apache.logging.log4j") { imports = [ "log4j-bom" From 3bc6b9263667bb3cea87ea47670715bbea633a66 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:33:06 +0000 Subject: [PATCH 0965/1215] Upgrade to MariaDB 3.3.2 Closes gh-39016 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index eba90f5d9e90..ca2a52be1542 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -862,7 +862,7 @@ bom { ] } } - library("MariaDB", "3.2.0") { + library("MariaDB", "3.3.2") { group("org.mariadb.jdbc") { modules = [ "mariadb-java-client" From eb18365f906d48eaac4a79ae574f9dcad484db4b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:33:11 +0000 Subject: [PATCH 0966/1215] Upgrade to Maven Compiler Plugin 3.12.1 Closes gh-39017 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index ca2a52be1542..517bb8497982 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -890,7 +890,7 @@ bom { ] } } - library("Maven Compiler Plugin", "3.11.0") { + library("Maven Compiler Plugin", "3.12.1") { group("org.apache.maven.plugins") { plugins = [ "maven-compiler-plugin" From fa5cfa8ec7d81d4d0c4beb5ecdfa47883f416093 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:33:15 +0000 Subject: [PATCH 0967/1215] Upgrade to Maven Failsafe Plugin 3.2.3 Closes gh-39018 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 517bb8497982..5e72222caa45 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -918,7 +918,7 @@ bom { ] } } - library("Maven Failsafe Plugin", "3.1.2") { + library("Maven Failsafe Plugin", "3.2.3") { group("org.apache.maven.plugins") { plugins = [ "maven-failsafe-plugin" From 1517d865e2c33afba02eda6aac3089182c2b4cb9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:33:20 +0000 Subject: [PATCH 0968/1215] Upgrade to Maven Surefire Plugin 3.2.3 Closes gh-39019 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5e72222caa45..ee7c001b3fd4 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -981,7 +981,7 @@ bom { ] } } - library("Maven Surefire Plugin", "3.1.2") { + library("Maven Surefire Plugin", "3.2.3") { group("org.apache.maven.plugins") { plugins = [ "maven-surefire-plugin" From 80ad4930d8b290387402126bc9a6f57cfab47814 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:33:25 +0000 Subject: [PATCH 0969/1215] Upgrade to Mockito 5.8.0 Closes gh-39020 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index ee7c001b3fd4..0eda0a90c6c3 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1017,7 +1017,7 @@ bom { ] } } - library("Mockito", "5.7.0") { + library("Mockito", "5.8.0") { group("org.mockito") { imports = [ "mockito-bom" From 52723d46296add72b337c1e8ccc48d643f03afe7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:33:29 +0000 Subject: [PATCH 0970/1215] Upgrade to MySQL 8.2.0 Closes gh-39021 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 0eda0a90c6c3..3a006531fefa 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1047,7 +1047,7 @@ bom { ] } } - library("MySQL", "8.1.0") { + library("MySQL", "8.2.0") { group("com.mysql") { modules = [ "mysql-connector-j" { From 6fa8094b798addf79458fda07507c22006bff076 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:33:34 +0000 Subject: [PATCH 0971/1215] Upgrade to Neo4j Java Driver 5.15.0 Closes gh-39022 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 3a006531fefa..40cac1f516e8 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1070,7 +1070,7 @@ bom { ] } } - library("Neo4j Java Driver", "5.13.0") { + library("Neo4j Java Driver", "5.15.0") { group("org.neo4j.driver") { modules = [ "neo4j-java-driver" From 2e9ef73cc583929efd2f67ac903f50d1a858fccf Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:33:39 +0000 Subject: [PATCH 0972/1215] Upgrade to OpenTelemetry 1.33.0 Closes gh-39023 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 40cac1f516e8..48fffc241cc8 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1091,7 +1091,7 @@ bom { ] } } - library("OpenTelemetry", "1.31.0") { + library("OpenTelemetry", "1.33.0") { group("io.opentelemetry") { imports = [ "opentelemetry-bom" From cd2c415e6de502d7d81402cac2e7494649016529 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:33:43 +0000 Subject: [PATCH 0973/1215] Upgrade to Oracle R2DBC 1.2.0 Closes gh-39024 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 48fffc241cc8..7c0e92075c9f 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1105,7 +1105,7 @@ bom { ] } } - library("Oracle R2DBC", "1.1.1") { + library("Oracle R2DBC", "1.2.0") { group("com.oracle.database.r2dbc") { modules = [ "oracle-r2dbc" From f355830b094acaef1404c5045c69c5db6274ef98 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:33:48 +0000 Subject: [PATCH 0974/1215] Upgrade to Postgresql 42.7.1 Closes gh-39025 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 7c0e92075c9f..0163223f69aa 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1119,7 +1119,7 @@ bom { ] } } - library("Postgresql", "42.6.0") { + library("Postgresql", "42.7.1") { group("org.postgresql") { modules = [ "postgresql" From 4543a5579045cba8da3d707c13c879c7510e7d39 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:33:53 +0000 Subject: [PATCH 0975/1215] Upgrade to Pulsar 3.1.2 Closes gh-39026 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 0163223f69aa..8aa6ce9e0ab5 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1133,7 +1133,7 @@ bom { ] } } - library("Pulsar", "3.1.1") { + library("Pulsar", "3.1.2") { group("org.apache.pulsar") { modules = [ "bouncy-castle-bc", From 7cc385f57c07c4b6c5ba5ded889fe6e03d9acad6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:33:58 +0000 Subject: [PATCH 0976/1215] Upgrade to Rabbit AMQP Client 5.20.0 Closes gh-39027 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 8aa6ce9e0ab5..7549f932d34c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1302,7 +1302,7 @@ bom { ] } } - library("Rabbit AMQP Client", "5.19.0") { + library("Rabbit AMQP Client", "5.20.0") { group("com.rabbitmq") { modules = [ "amqp-client" From c392a102f23e2304e189fbdb7e04775bed9d7683 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:34:02 +0000 Subject: [PATCH 0977/1215] Upgrade to Rabbit Stream Client 0.15.0 Closes gh-39028 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 7549f932d34c..e21249269144 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1309,7 +1309,7 @@ bom { ] } } - library("Rabbit Stream Client", "0.14.0") { + library("Rabbit Stream Client", "0.15.0") { group("com.rabbitmq") { modules = [ "stream-client" From e733ebcc5a970c2d4b1bedf30de842d24a2591d2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:34:07 +0000 Subject: [PATCH 0978/1215] Upgrade to REST Assured 5.4.0 Closes gh-39029 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e21249269144..25c8dda2cf74 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1332,7 +1332,7 @@ bom { ] } } - library("REST Assured", "5.3.2") { + library("REST Assured", "5.4.0") { group("io.rest-assured") { imports = [ "rest-assured-bom" From de3cae50cdcdd5165fabb46f5907dc3d25fd4987 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:34:12 +0000 Subject: [PATCH 0979/1215] Upgrade to Selenium 4.16.1 Closes gh-39030 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 25c8dda2cf74..d1600aec42bf 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1446,7 +1446,7 @@ bom { ] } } - library("Selenium", "4.14.1") { + library("Selenium", "4.16.1") { group("org.seleniumhq.selenium") { imports = [ "selenium-bom" From 1e6627d45800c0e911d58a779bd373689aa9c397 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:34:16 +0000 Subject: [PATCH 0980/1215] Upgrade to SendGrid 4.10.1 Closes gh-39031 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d1600aec42bf..a6204b42a051 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1460,7 +1460,7 @@ bom { ] } } - library("SendGrid", "4.9.3") { + library("SendGrid", "4.10.1") { group("com.sendgrid") { modules = [ "sendgrid-java" From 383750a3090ddf2c528a2071077971ff1ccdad2e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:34:21 +0000 Subject: [PATCH 0981/1215] Upgrade to SLF4J 2.0.10 Closes gh-39032 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index a6204b42a051..81a39b48d558 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1467,7 +1467,7 @@ bom { ] } } - library("SLF4J", "2.0.9") { + library("SLF4J", "2.0.10") { group("org.slf4j") { modules = [ "jcl-over-slf4j", From 88f4c72ef4b8ce41b0e34505065a69e101eeed3f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:34:26 +0000 Subject: [PATCH 0982/1215] Upgrade to SQLite JDBC 3.44.1.0 Closes gh-39033 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 81a39b48d558..a192116fd34f 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1633,7 +1633,7 @@ bom { ] } } - library("SQLite JDBC", "3.43.2.0") { + library("SQLite JDBC", "3.44.1.0") { group("org.xerial") { modules = [ "sqlite-jdbc" From b2aa7e5e8c1726b510fd6aa259e746b2ff74e43b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Jan 2024 16:32:10 +0000 Subject: [PATCH 0983/1215] Prohibit upgrades to Derby 10.17 Closes gh-39004 --- spring-boot-project/spring-boot-dependencies/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index a192116fd34f..bb3d02e0bdc1 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -236,6 +236,10 @@ bom { } } library("Derby", "10.16.1.1") { + prohibit { + versionRange "[10.17.1.0,)" + because "it requires Java 21" + } group("org.apache.derby") { modules = [ "derby", From 2acb90cbb5e67a294e0cb4ba57ae0c34b5c5e4c3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 5 Jan 2024 10:33:23 +0000 Subject: [PATCH 0984/1215] Prohibit upgrades to Jetty Reactive HTTPClient 4.0.2 Closes gh-39010 --- spring-boot-project/spring-boot-dependencies/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c51ab9e092b4..6bd934ee0030 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -672,6 +672,10 @@ bom { } } library("Jetty Reactive HTTPClient", "4.0.1") { + prohibit { + versionRange "[4.0.2]" + because "it causes problems in Spring Framework (https://github.com/spring-projects/spring-framework/issues/31931#issue-2061468092)" + } group("org.eclipse.jetty") { modules = [ "jetty-reactive-httpclient" From 4b89723861168abec23a47dabffff92988abc796 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 5 Jan 2024 11:04:25 +0000 Subject: [PATCH 0985/1215] Remove APIs that were deprecated for removal in 3.3.0 Closes gh-39039 --- .../couchbase/CouchbaseAutoConfiguration.java | 40 +--- .../couchbase/CouchbaseProperties.java | 46 +--- .../autoconfigure/kafka/KafkaProperties.java | 23 +- ...ertiesClientSettingsBuilderCustomizer.java | 117 ---------- ...h2ClientPropertiesRegistrationAdapter.java | 45 ---- ...itional-spring-configuration-metadata.json | 31 ++- .../CouchbaseAutoConfigurationTests.java | 19 -- .../kafka/KafkaAutoConfigurationTests.java | 16 +- ...sClientSettingsBuilderCustomizerTests.java | 215 ------------------ .../ConfigurationPropertiesBean.java | 44 +--- .../boot/jdbc/DatabaseDriver.java | 25 +- .../netty/NettyRSocketServerFactory.java | 14 +- .../ConfigurableRSocketServerFactory.java | 13 +- .../AbstractConfigurableWebServerFactory.java | 34 +-- .../CertificateFileSslStoreProvider.java | 69 ------ .../server/ConfigurableWebServerFactory.java | 12 +- .../web/server/SslConfigurationValidator.java | 41 ---- .../boot/web/server/SslStoreProvider.java | 58 ----- .../boot/web/server/WebServerSslBundle.java | 62 +---- .../tomcat/SslConnectorCustomizerTests.java | 84 +------ .../SslConfigurationValidatorTests.java | 82 ------- .../web/server/WebServerSslBundleTests.java | 47 +--- .../AbstractServletWebServerFactoryTests.java | 40 ---- 23 files changed, 53 insertions(+), 1124 deletions(-) delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesClientSettingsBuilderCustomizer.java delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesClientSettingsBuilderCustomizerTests.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/CertificateFileSslStoreProvider.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslConfigurationValidator.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslStoreProvider.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/SslConfigurationValidatorTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java index 77a6dc070f37..5b72723e1cb1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -16,11 +16,6 @@ package org.springframework.boot.autoconfigure.couchbase; -import java.io.InputStream; -import java.net.URL; -import java.security.KeyStore; - -import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.TrustManagerFactory; import com.couchbase.client.java.Cluster; @@ -52,7 +47,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.util.Assert; -import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; /** @@ -134,45 +128,17 @@ private void configureSsl(Builder builder, SslBundles sslBundles) { "SSL Options cannot be specified with Couchbase"); builder.securityConfig((config) -> { config.enableTls(true); - TrustManagerFactory trustManagerFactory = getTrustManagerFactory(sslProperties, sslBundle); + TrustManagerFactory trustManagerFactory = getTrustManagerFactory(sslBundle); if (trustManagerFactory != null) { config.trustManagerFactory(trustManagerFactory); } }); } - @SuppressWarnings("removal") - private TrustManagerFactory getTrustManagerFactory(CouchbaseProperties.Ssl sslProperties, SslBundle sslBundle) { - if (sslProperties.getKeyStore() != null) { - return loadTrustManagerFactory(sslProperties); - } + private TrustManagerFactory getTrustManagerFactory(SslBundle sslBundle) { return (sslBundle != null) ? sslBundle.getManagers().getTrustManagerFactory() : null; } - @SuppressWarnings("removal") - private TrustManagerFactory loadTrustManagerFactory(CouchbaseProperties.Ssl ssl) { - String resource = ssl.getKeyStore(); - try { - TrustManagerFactory trustManagerFactory = TrustManagerFactory - .getInstance(KeyManagerFactory.getDefaultAlgorithm()); - KeyStore keyStore = loadKeyStore(resource, ssl.getKeyStorePassword()); - trustManagerFactory.init(keyStore); - return trustManagerFactory; - } - catch (Exception ex) { - throw new IllegalStateException("Could not load Couchbase key store '" + resource + "'", ex); - } - } - - private KeyStore loadKeyStore(String resource, String keyStorePassword) throws Exception { - KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType()); - URL url = ResourceUtils.getURL(resource); - try (InputStream stream = url.openStream()) { - store.load(stream, (keyStorePassword != null) ? keyStorePassword.toCharArray() : null); - } - return store; - } - @Configuration(proxyBeanMethods = false) @ConditionalOnClass(ObjectMapper.class) static class JacksonConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseProperties.java index d1e93181c570..fbe2d5878eaa 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -19,7 +19,6 @@ import java.time.Duration; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.util.StringUtils; /** @@ -149,61 +148,24 @@ public void setIdleHttpConnectionTimeout(Duration idleHttpConnectionTimeout) { public static class Ssl { /** - * Whether to enable SSL support. Enabled automatically if a "keyStore" or - * "bundle" is provided unless specified otherwise. + * Whether to enable SSL support. Enabled automatically if a "bundle" is provided + * unless specified otherwise. */ private Boolean enabled; - /** - * Path to the JVM key store that holds the certificates. - */ - private String keyStore; - - /** - * Password used to access the key store. - */ - private String keyStorePassword; - /** * SSL bundle name. */ private String bundle; public Boolean getEnabled() { - return (this.enabled != null) ? this.enabled - : StringUtils.hasText(this.keyStore) || StringUtils.hasText(this.bundle); + return (this.enabled != null) ? this.enabled : StringUtils.hasText(this.bundle); } public void setEnabled(Boolean enabled) { this.enabled = enabled; } - @Deprecated(since = "3.1.0", forRemoval = true) - @DeprecatedConfigurationProperty( - reason = "SSL bundle support with spring.ssl.bundle and spring.couchbase.env.ssl.bundle should be used instead", - since = "3.1.0") - public String getKeyStore() { - return this.keyStore; - } - - @Deprecated(since = "3.1.0", forRemoval = true) - public void setKeyStore(String keyStore) { - this.keyStore = keyStore; - } - - @Deprecated(since = "3.1.0", forRemoval = true) - @DeprecatedConfigurationProperty( - reason = "SSL bundle support with spring.ssl.bundle and spring.couchbase.env.ssl.bundle should be used instead", - since = "3.1.0") - public String getKeyStorePassword() { - return this.keyStorePassword; - } - - @Deprecated(since = "3.1.0", forRemoval = true) - public void setKeyStorePassword(String keyStorePassword) { - this.keyStorePassword = keyStorePassword; - } - public String getBundle() { return this.bundle; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java index 20085141e501..6dbf503791d3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -34,7 +34,6 @@ import org.apache.kafka.common.serialization.StringSerializer; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.DeprecatedConfigurationProperty; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException; import org.springframework.boot.convert.DurationUnit; @@ -837,11 +836,6 @@ public static class Streams { */ private List bootstrapServers; - /** - * Maximum memory size to be used for buffering across all threads. - */ - private DataSize cacheMaxSizeBuffering; - /** * Maximum size of the in-memory state store cache across all threads. */ @@ -904,18 +898,6 @@ public void setBootstrapServers(List bootstrapServers) { this.bootstrapServers = bootstrapServers; } - @DeprecatedConfigurationProperty(replacement = "spring.kafka.streams.state-store-cache-max-size", - since = "3.1.0") - @Deprecated(since = "3.1.0", forRemoval = true) - public DataSize getCacheMaxSizeBuffering() { - return this.cacheMaxSizeBuffering; - } - - @Deprecated(since = "3.1.0", forRemoval = true) - public void setCacheMaxSizeBuffering(DataSize cacheMaxSizeBuffering) { - this.cacheMaxSizeBuffering = cacheMaxSizeBuffering; - } - public DataSize getStateStoreCacheMaxSize() { return this.stateStoreCacheMaxSize; } @@ -957,9 +939,6 @@ public Map buildProperties(SslBundles sslBundles) { PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(this::getApplicationId).to(properties.in("application.id")); map.from(this::getBootstrapServers).to(properties.in(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG)); - map.from(this::getCacheMaxSizeBuffering) - .asInt(DataSize::toBytes) - .to(properties.in("cache.max.bytes.buffering")); map.from(this::getStateStoreCacheMaxSize) .asInt(DataSize::toBytes) .to(properties.in("statestore.cache.max.bytes")); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesClientSettingsBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesClientSettingsBuilderCustomizer.java deleted file mode 100644 index 691064cd5e69..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesClientSettingsBuilderCustomizer.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2012-2023 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.mongo; - -import java.util.ArrayList; -import java.util.List; - -import com.mongodb.ConnectionString; -import com.mongodb.MongoClientSettings; -import com.mongodb.MongoCredential; -import com.mongodb.ServerAddress; - -import org.springframework.core.Ordered; -import org.springframework.util.CollectionUtils; - -/** - * A {@link MongoClientSettingsBuilderCustomizer} that applies properties from a - * {@link MongoProperties} to a {@link MongoClientSettings}. - * - * @author Scott Frederick - * @author Safeer Ansari - * @since 2.4.0 - * @deprecated since 3.1.0 for removal in 3.3.0 in favor of - * {@link StandardMongoClientSettingsBuilderCustomizer} - */ -@Deprecated(since = "3.1.0", forRemoval = true) -public class MongoPropertiesClientSettingsBuilderCustomizer implements MongoClientSettingsBuilderCustomizer, Ordered { - - private final MongoProperties properties; - - private int order = 0; - - public MongoPropertiesClientSettingsBuilderCustomizer(MongoProperties properties) { - this.properties = properties; - } - - @Override - public void customize(MongoClientSettings.Builder settingsBuilder) { - applyUuidRepresentation(settingsBuilder); - applyHostAndPort(settingsBuilder); - applyCredentials(settingsBuilder); - applyReplicaSet(settingsBuilder); - } - - private void applyUuidRepresentation(MongoClientSettings.Builder settingsBuilder) { - settingsBuilder.uuidRepresentation(this.properties.getUuidRepresentation()); - } - - private void applyHostAndPort(MongoClientSettings.Builder settings) { - if (this.properties.getUri() != null) { - settings.applyConnectionString(new ConnectionString(this.properties.getUri())); - return; - } - if (this.properties.getHost() != null || this.properties.getPort() != null) { - String host = getOrDefault(this.properties.getHost(), "localhost"); - int port = getOrDefault(this.properties.getPort(), MongoProperties.DEFAULT_PORT); - List serverAddresses = new ArrayList<>(); - serverAddresses.add(new ServerAddress(host, port)); - if (!CollectionUtils.isEmpty(this.properties.getAdditionalHosts())) { - this.properties.getAdditionalHosts().stream().map(ServerAddress::new).forEach(serverAddresses::add); - } - settings.applyToClusterSettings((cluster) -> cluster.hosts(serverAddresses)); - return; - } - settings.applyConnectionString(new ConnectionString(MongoProperties.DEFAULT_URI)); - } - - private void applyCredentials(MongoClientSettings.Builder builder) { - if (this.properties.getUri() == null && this.properties.getUsername() != null - && this.properties.getPassword() != null) { - String database = (this.properties.getAuthenticationDatabase() != null) - ? this.properties.getAuthenticationDatabase() : this.properties.getMongoClientDatabase(); - builder.credential((MongoCredential.createCredential(this.properties.getUsername(), database, - this.properties.getPassword()))); - } - } - - private void applyReplicaSet(MongoClientSettings.Builder builder) { - if (this.properties.getReplicaSetName() != null) { - builder.applyToClusterSettings( - (cluster) -> cluster.requiredReplicaSetName(this.properties.getReplicaSetName())); - } - } - - private V getOrDefault(V value, V defaultValue) { - return (value != null) ? value : defaultValue; - } - - @Override - public int getOrder() { - return this.order; - } - - /** - * Set the order value of this object. - * @param order the new order value - * @see #getOrder() - */ - public void setOrder(int order) { - this.order = order; - } - -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java deleted file mode 100644 index 343f511caf87..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2012-2023 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.security.oauth2.client; - -import java.util.Map; - -import org.springframework.security.oauth2.client.registration.ClientRegistration; - -/** - * Adapter class to convert {@link OAuth2ClientProperties} to a - * {@link ClientRegistration}. - * - * @author Phillip Webb - * @author Thiago Hirata - * @author Madhura Bhave - * @author MyeongHyeon Lee - * @since 2.1.0 - * @deprecated since 3.1.0 for removal in 3.3.0 in favor of - * {@link OAuth2ClientPropertiesMapper} - */ -@Deprecated(since = "3.1.0", forRemoval = true) -public final class OAuth2ClientPropertiesRegistrationAdapter { - - private OAuth2ClientPropertiesRegistrationAdapter() { - } - - public static Map getClientRegistrations(OAuth2ClientProperties properties) { - return new OAuth2ClientPropertiesMapper(properties).asClientRegistrations(); - } - -} 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 9308b0862e9e..c61b327b6ee0 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 @@ -670,6 +670,26 @@ "level": "error" } }, + { + "name": "spring.couchbase.env.ssl.key-store", + "type": "java.lang.String", + "description": "Path to the JVM key store that holds the certificates.", + "deprecation": { + "replacement": "spring.couchbase.env.ssl.bundle", + "level": "error", + "since": "3.1.0" + } + }, + { + "name": "spring.couchbase.env.ssl.key-store-password", + "type": "java.lang.String", + "description": "Password used to access the key store.", + "deprecation": { + "replacement": "spring.couchbase.env.ssl.bundle", + "level": "error", + "since": "3.1.0" + } + }, { "name": "spring.couchbase.env.timeouts.socket-connect", "type": "java.time.Duration", @@ -1901,10 +1921,19 @@ "name": "spring.kafka.streams.cache-max-bytes-buffering", "type": "java.lang.Integer", "deprecation": { - "replacement": "spring.kafka.streams.cache-max-size-buffering", + "replacement": "spring.kafka.streams.state-store-cache-max-size", "level": "error" } }, + { + "name": "spring.kafka.streams.cache-max-size-buffering", + "type": "java.lang.Integer", + "deprecation": { + "replacement": "spring.kafka.streams.state-store-cache-max-size", + "level": "error", + "since": "3.1.0" + } + }, { "name": "spring.liquibase.check-change-log-location", "type": "java.lang.Boolean", diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationTests.java index c76ea994d6cb..6b38551d5f8d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationTests.java @@ -189,15 +189,6 @@ void enableSsl() { }, "spring.couchbase.env.ssl.enabled=true"); } - @Test - void enableSslWithKeyStore() { - testClusterEnvironment((env) -> { - SecurityConfig securityConfig = env.securityConfig(); - assertThat(securityConfig.tlsEnabled()).isTrue(); - assertThat(securityConfig.trustManagerFactory()).isNotNull(); - }, "spring.couchbase.env.ssl.keyStore=classpath:test.jks", "spring.couchbase.env.ssl.keyStorePassword=secret"); - } - @Test void enableSslWithBundle() { testClusterEnvironment((env) -> { @@ -222,16 +213,6 @@ void enableSslWithInvalidBundle() { }); } - @Test - void disableSslEvenWithKeyStore() { - testClusterEnvironment((env) -> { - SecurityConfig securityConfig = env.securityConfig(); - assertThat(securityConfig.tlsEnabled()).isFalse(); - assertThat(securityConfig.trustManagerFactory()).isNull(); - }, "spring.couchbase.env.ssl.enabled=false", "spring.couchbase.env.ssl.keyStore=classpath:test.jks", - "spring.couchbase.env.ssl.keyStorePassword=secret"); - } - @Test void disableSslEvenWithBundle() { testClusterEnvironment((env) -> { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java index 4adc774e8d7f..56b627bd8358 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/kafka/KafkaAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -396,20 +396,6 @@ void connectionDetailsAreAppliedToStreams() { }); } - @SuppressWarnings("deprecation") - @Deprecated(since = "3.1.0", forRemoval = true) - void streamsCacheMaxSizeBuffering() { - this.contextRunner.withUserConfiguration(EnableKafkaStreamsConfiguration.class) - .withPropertyValues("spring.kafka.streams.cache-max-size-buffering=1KB") - .run((context) -> { - Properties configs = context - .getBean(KafkaStreamsDefaultConfiguration.DEFAULT_STREAMS_CONFIG_BEAN_NAME, - KafkaStreamsConfiguration.class) - .asProperties(); - assertThat(configs).containsEntry(StreamsConfig.CACHE_MAX_BYTES_BUFFERING_CONFIG, 1024); - }); - } - @SuppressWarnings("unchecked") @Test void streamsApplicationIdUsesMainApplicationNameByDefault() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesClientSettingsBuilderCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesClientSettingsBuilderCustomizerTests.java deleted file mode 100644 index 021d298523c0..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoPropertiesClientSettingsBuilderCustomizerTests.java +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright 2012-2023 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.mongo; - -import java.util.Arrays; -import java.util.List; - -import com.mongodb.MongoClientSettings; -import com.mongodb.MongoCredential; -import com.mongodb.ServerAddress; -import org.bson.UuidRepresentation; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link MongoPropertiesClientSettingsBuilderCustomizer}. - * - * @author Scott Frederick - */ -@Deprecated(since = "3.1.0", forRemoval = true) -class MongoPropertiesClientSettingsBuilderCustomizerTests { - - private final MongoProperties properties = new MongoProperties(); - - @Test - void portCanBeCustomized() { - this.properties.setPort(12345); - MongoClientSettings settings = customizeSettings(); - List allAddresses = getAllAddresses(settings); - assertThat(allAddresses).hasSize(1); - assertServerAddress(allAddresses.get(0), "localhost", 12345); - } - - @Test - void hostCanBeCustomized() { - this.properties.setHost("mongo.example.com"); - MongoClientSettings settings = customizeSettings(); - List allAddresses = getAllAddresses(settings); - assertThat(allAddresses).hasSize(1); - assertServerAddress(allAddresses.get(0), "mongo.example.com", 27017); - } - - @Test - void additionalHostCanBeAdded() { - this.properties.setHost("mongo.example.com"); - this.properties.setAdditionalHosts(Arrays.asList("mongo.example.com:33", "mongo.example2.com")); - MongoClientSettings settings = customizeSettings(); - List allAddresses = getAllAddresses(settings); - assertThat(allAddresses).hasSize(3); - assertServerAddress(allAddresses.get(0), "mongo.example.com", 27017); - assertServerAddress(allAddresses.get(1), "mongo.example.com", 33); - assertServerAddress(allAddresses.get(2), "mongo.example2.com", 27017); - } - - @Test - void credentialsCanBeCustomized() { - this.properties.setUsername("user"); - this.properties.setPassword("secret".toCharArray()); - MongoClientSettings settings = customizeSettings(); - assertMongoCredential(settings.getCredential(), "user", "secret", "test"); - } - - @Test - void replicaSetCanBeCustomized() { - this.properties.setReplicaSetName("test"); - MongoClientSettings settings = customizeSettings(); - assertThat(settings.getClusterSettings().getRequiredReplicaSetName()).isEqualTo("test"); - } - - @Test - void databaseCanBeCustomized() { - this.properties.setDatabase("foo"); - this.properties.setUsername("user"); - this.properties.setPassword("secret".toCharArray()); - MongoClientSettings settings = customizeSettings(); - assertMongoCredential(settings.getCredential(), "user", "secret", "foo"); - } - - @Test - void uuidRepresentationDefaultToJavaLegacy() { - MongoClientSettings settings = customizeSettings(); - assertThat(settings.getUuidRepresentation()).isEqualTo(UuidRepresentation.JAVA_LEGACY); - } - - @Test - void uuidRepresentationCanBeCustomized() { - this.properties.setUuidRepresentation(UuidRepresentation.STANDARD); - MongoClientSettings settings = customizeSettings(); - assertThat(settings.getUuidRepresentation()).isEqualTo(UuidRepresentation.STANDARD); - } - - @Test - void authenticationDatabaseCanBeCustomized() { - this.properties.setAuthenticationDatabase("foo"); - this.properties.setUsername("user"); - this.properties.setPassword("secret".toCharArray()); - MongoClientSettings settings = customizeSettings(); - assertMongoCredential(settings.getCredential(), "user", "secret", "foo"); - } - - @Test - void onlyHostAndPortSetShouldUseThat() { - this.properties.setHost("localhost"); - this.properties.setPort(27017); - MongoClientSettings settings = customizeSettings(); - List allAddresses = getAllAddresses(settings); - assertThat(allAddresses).hasSize(1); - assertServerAddress(allAddresses.get(0), "localhost", 27017); - } - - @Test - void onlyUriSetShouldUseThat() { - this.properties.setUri("mongodb://mongo1.example.com:12345"); - MongoClientSettings settings = customizeSettings(); - List allAddresses = getAllAddresses(settings); - assertThat(allAddresses).hasSize(1); - assertServerAddress(allAddresses.get(0), "mongo1.example.com", 12345); - } - - @Test - void noCustomAddressAndNoUriUsesDefaultUri() { - MongoClientSettings settings = customizeSettings(); - List allAddresses = getAllAddresses(settings); - assertThat(allAddresses).hasSize(1); - assertServerAddress(allAddresses.get(0), "localhost", 27017); - } - - @Test - void uriCanBeCustomized() { - this.properties.setUri("mongodb://user:secret@mongo1.example.com:12345,mongo2.example.com:23456/test"); - MongoClientSettings settings = customizeSettings(); - List allAddresses = getAllAddresses(settings); - assertThat(allAddresses).hasSize(2); - assertServerAddress(allAddresses.get(0), "mongo1.example.com", 12345); - assertServerAddress(allAddresses.get(1), "mongo2.example.com", 23456); - assertMongoCredential(settings.getCredential(), "user", "secret", "test"); - } - - @Test - void uriOverridesUsernameAndPassword() { - this.properties.setUri("mongodb://127.0.0.1:1234/mydb"); - this.properties.setUsername("user"); - this.properties.setPassword("secret".toCharArray()); - MongoClientSettings settings = customizeSettings(); - assertThat(settings.getCredential()).isNull(); - } - - @Test - void uriOverridesDatabase() { - this.properties.setUri("mongodb://secret:password@127.0.0.1:1234/mydb"); - this.properties.setDatabase("test"); - MongoClientSettings settings = customizeSettings(); - List allAddresses = getAllAddresses(settings); - assertThat(allAddresses).hasSize(1); - assertServerAddress(allAddresses.get(0), "127.0.0.1", 1234); - assertThat(settings.getCredential().getSource()).isEqualTo("mydb"); - } - - @Test - void uriOverridesHostAndPort() { - this.properties.setUri("mongodb://127.0.0.1:1234/mydb"); - this.properties.setHost("localhost"); - this.properties.setPort(4567); - MongoClientSettings settings = customizeSettings(); - List addresses = getAllAddresses(settings); - assertThat(addresses.get(0).getHost()).isEqualTo("127.0.0.1"); - assertThat(addresses.get(0).getPort()).isEqualTo(1234); - } - - @Test - void retryWritesIsPropagatedFromUri() { - this.properties.setUri("mongodb://localhost/test?retryWrites=false"); - MongoClientSettings settings = customizeSettings(); - assertThat(settings.getRetryWrites()).isFalse(); - } - - @SuppressWarnings("removal") - private MongoClientSettings customizeSettings() { - MongoClientSettings.Builder settings = MongoClientSettings.builder(); - new MongoPropertiesClientSettingsBuilderCustomizer(this.properties).customize(settings); - return settings.build(); - } - - private List getAllAddresses(MongoClientSettings settings) { - return settings.getClusterSettings().getHosts(); - } - - protected void assertServerAddress(ServerAddress serverAddress, String expectedHost, int expectedPort) { - assertThat(serverAddress.getHost()).isEqualTo(expectedHost); - assertThat(serverAddress.getPort()).isEqualTo(expectedPort); - } - - protected void assertMongoCredential(MongoCredential credentials, String expectedUsername, String expectedPassword, - String expectedSource) { - assertThat(credentials.getUserName()).isEqualTo(expectedUsername); - assertThat(credentials.getPassword()).isEqualTo(expectedPassword.toCharArray()); - assertThat(credentials.getSource()).isEqualTo(expectedSource); - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBean.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBean.java index 3fa38d84391b..a74701d8e775 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBean.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -100,17 +100,6 @@ Class getType() { return this.bindTarget.getType().resolve(); } - /** - * Return the property binding method that was used for the bean. - * @return the bind method - * @deprecated since 3.0.8 for removal in 3.3.0 in favor of {@link #asBindTarget} and - * {@link Bindable#getBindMethod} - */ - @Deprecated(since = "3.0.8", forRemoval = true) - public BindMethod getBindMethod() { - return BindMethod.from(this.bindTarget.getBindMethod()); - } - /** * Return the {@link ConfigurationProperties} annotation for the bean. The annotation * may be defined on the bean itself or from the factory method that create the bean @@ -312,35 +301,4 @@ private static org.springframework.boot.context.properties.bind.BindMethod deduc return (bindConstructor != null) ? VALUE_OBJECT_BIND_METHOD : JAVA_BEAN_BIND_METHOD; } - /** - * The binding method that is used for the bean. - * - * @deprecated since 3.0.8 for removal in 3.3.0 in favor of - * {@link org.springframework.boot.context.properties.bind.BindMethod} - */ - @Deprecated(since = "3.0.8", forRemoval = true) - public enum BindMethod { - - /** - * Java Bean using getter/setter binding. - */ - JAVA_BEAN, - - /** - * Value object using constructor binding. - */ - VALUE_OBJECT; - - static BindMethod from(org.springframework.boot.context.properties.bind.BindMethod bindMethod) { - if (bindMethod == null) { - return null; - } - return switch (bindMethod) { - case VALUE_OBJECT -> BindMethod.VALUE_OBJECT; - case JAVA_BEAN -> BindMethod.JAVA_BEAN; - }; - } - - } - } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/DatabaseDriver.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/DatabaseDriver.java index 573cdf48c48b..0ef89b5ffcfd 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/DatabaseDriver.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/DatabaseDriver.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -16,15 +16,11 @@ package org.springframework.boot.jdbc; -import java.sql.DatabaseMetaData; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Locale; -import javax.sql.DataSource; - -import org.springframework.jdbc.support.JdbcUtils; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -315,23 +311,4 @@ public static DatabaseDriver fromProductName(String productName) { return UNKNOWN; } - /** - * Find a {@link DatabaseDriver} for the given {@code DataSource}. - * @param dataSource data source to inspect - * @return the database driver of {@link #UNKNOWN} if not found - * @since 2.6.0 - * @deprecated since 2.7.15 for removal in 3.3.0 with no replacement - */ - @Deprecated(since = "2.7.15", forRemoval = true) - public static DatabaseDriver fromDataSource(DataSource dataSource) { - try { - String productName = JdbcUtils.commonDatabaseName( - JdbcUtils.extractDatabaseMetaData(dataSource, DatabaseMetaData::getDatabaseProductName)); - return DatabaseDriver.fromProductName(productName); - } - catch (Exception ex) { - return DatabaseDriver.UNKNOWN; - } - } - } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactory.java index e291716cac71..06840ac2ee40 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -43,7 +43,6 @@ import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.web.embedded.netty.SslServerCustomizer; import org.springframework.boot.web.server.Ssl; -import org.springframework.boot.web.server.SslStoreProvider; import org.springframework.boot.web.server.WebServerSslBundle; import org.springframework.http.client.ReactorResourceFactory; import org.springframework.util.Assert; @@ -58,7 +57,6 @@ * @author Scott Frederick * @since 2.2.0 */ -@SuppressWarnings("removal") public class NettyRSocketServerFactory implements RSocketServerFactory, ConfigurableRSocketServerFactory { private int port = 9898; @@ -77,8 +75,6 @@ public class NettyRSocketServerFactory implements RSocketServerFactory, Configur private Ssl ssl; - private SslStoreProvider sslStoreProvider; - private SslBundles sslBundles; @Override @@ -106,11 +102,6 @@ public void setSsl(Ssl ssl) { this.ssl = ssl; } - @Override - public void setSslStoreProvider(SslStoreProvider sslStoreProvider) { - this.sslStoreProvider = sslStoreProvider; - } - @Override public void setSslBundles(SslBundles sslBundles) { this.sslBundles = sslBundles; @@ -204,9 +195,8 @@ private ServerTransport createTcpTransport() { return TcpServerTransport.create(tcpServer.bindAddress(this::getListenAddress)); } - @SuppressWarnings("deprecation") private SslBundle getSslBundle() { - return WebServerSslBundle.get(this.ssl, this.sslBundles, this.sslStoreProvider); + return WebServerSslBundle.get(this.ssl, this.sslBundles); } private InetSocketAddress getListenAddress() { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/server/ConfigurableRSocketServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/server/ConfigurableRSocketServerFactory.java index eb48a9ef475d..671d4bea200d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/server/ConfigurableRSocketServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/server/ConfigurableRSocketServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -20,7 +20,6 @@ import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.web.server.Ssl; -import org.springframework.boot.web.server.SslStoreProvider; import org.springframework.util.unit.DataSize; /** @@ -65,16 +64,6 @@ public interface ConfigurableRSocketServerFactory { */ void setSsl(Ssl ssl); - /** - * Sets a provider that will be used to obtain SSL stores. - * @param sslStoreProvider the SSL store provider - * @deprecated since 3.1.0 for removal in 3.3.0 in favor of - * {@link #setSslBundles(SslBundles)} - */ - @SuppressWarnings("removal") - @Deprecated(since = "3.1.0", forRemoval = true) - void setSslStoreProvider(SslStoreProvider sslStoreProvider); - /** * Sets an SSL bundle that can be used to get SSL configuration. * @param sslBundles the SSL bundles diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java index 21fa14527ae6..b77b4f56c269 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -51,9 +51,6 @@ public abstract class AbstractConfigurableWebServerFactory implements Configurab private Ssl ssl; - @SuppressWarnings("removal") - private SslStoreProvider sslStoreProvider; - private SslBundles sslBundles; private Http2 http2; @@ -135,17 +132,6 @@ public void setSsl(Ssl ssl) { this.ssl = ssl; } - @SuppressWarnings("removal") - public SslStoreProvider getSslStoreProvider() { - return this.sslStoreProvider; - } - - @Override - @SuppressWarnings("removal") - public void setSslStoreProvider(SslStoreProvider sslStoreProvider) { - this.sslStoreProvider = sslStoreProvider; - } - /** * Return the configured {@link SslBundles}. * @return the {@link SslBundles} or {@code null} @@ -201,28 +187,12 @@ public Shutdown getShutdown() { return this.shutdown; } - /** - * Return the provided {@link SslStoreProvider} or create one using {@link Ssl} - * properties. - * @return the {@code SslStoreProvider} - * @deprecated since 3.1.0 for removal in 3.3.0 in favor of {@link #getSslBundle()} - */ - @Deprecated(since = "3.1.0", forRemoval = true) - @SuppressWarnings("removal") - public final SslStoreProvider getOrCreateSslStoreProvider() { - if (this.sslStoreProvider != null) { - return this.sslStoreProvider; - } - return CertificateFileSslStoreProvider.from(this.ssl); - } - /** * Return the {@link SslBundle} that should be used with this server. * @return the SSL bundle */ - @SuppressWarnings("removal") protected final SslBundle getSslBundle() { - return WebServerSslBundle.get(this.ssl, this.sslBundles, this.sslStoreProvider); + return WebServerSslBundle.get(this.ssl, this.sslBundles); } /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/CertificateFileSslStoreProvider.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/CertificateFileSslStoreProvider.java deleted file mode 100644 index d3ed1a091edf..000000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/CertificateFileSslStoreProvider.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2012-2023 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.web.server; - -import java.security.KeyStore; - -import org.springframework.boot.ssl.SslBundle; -import org.springframework.boot.ssl.pem.PemSslStoreBundle; - -/** - * An {@link SslStoreProvider} that creates key and trust stores from certificate and - * private key PEM files. - * - * @author Scott Frederick - * @since 2.7.0 - * @deprecated since 3.1.0 for removal in 3.3.0 in favor of registering a - * {@link SslBundle} backed by a {@link PemSslStoreBundle}. - */ -@Deprecated(since = "3.1.0", forRemoval = true) -@SuppressWarnings({ "deprecation", "removal" }) -public final class CertificateFileSslStoreProvider implements SslStoreProvider { - - private final SslBundle delegate; - - private CertificateFileSslStoreProvider(SslBundle delegate) { - this.delegate = delegate; - } - - @Override - public KeyStore getKeyStore() throws Exception { - return this.delegate.getStores().getKeyStore(); - } - - @Override - public KeyStore getTrustStore() throws Exception { - return this.delegate.getStores().getTrustStore(); - } - - @Override - public String getKeyPassword() { - return this.delegate.getKey().getPassword(); - } - - /** - * Create an {@link SslStoreProvider} if the appropriate SSL properties are - * configured. - * @param ssl the SSL properties - * @return an {@code SslStoreProvider} or {@code null} - */ - public static SslStoreProvider from(Ssl ssl) { - SslBundle delegate = WebServerSslBundle.createCertificateFileSslStoreProviderDelegate(ssl); - return (delegate != null) ? new CertificateFileSslStoreProvider(delegate) : null; - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/ConfigurableWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/ConfigurableWebServerFactory.java index c10580aa3dd7..bc90e66b7600 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/ConfigurableWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/ConfigurableWebServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -58,16 +58,6 @@ public interface ConfigurableWebServerFactory extends WebServerFactory, ErrorPag */ void setSsl(Ssl ssl); - /** - * Sets a provider that will be used to obtain SSL stores. - * @param sslStoreProvider the SSL store provider - * @deprecated since 3.1.0 for removal in 3.3.0, in favor of - * {@link #setSslBundles(SslBundles)} - */ - @Deprecated(since = "3.1.0", forRemoval = true) - @SuppressWarnings("removal") - void setSslStoreProvider(SslStoreProvider sslStoreProvider); - /** * Sets the SSL bundles that can be used to configure SSL connections. * @param sslBundles the SSL bundles diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslConfigurationValidator.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslConfigurationValidator.java deleted file mode 100644 index d044dfa29dc1..000000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslConfigurationValidator.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2012-2023 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.web.server; - -import java.security.KeyStore; - -import org.springframework.boot.ssl.SslBundleKey; - -/** - * Provides utilities around SSL. - * - * @author Chris Bono - * @since 2.1.13 - * @deprecated since 3.1.0 for removal in 3.3.0 in favor of - * {@link SslBundleKey#assertContainsAlias(KeyStore)} - */ -@Deprecated(since = "3.1.0", forRemoval = true) -public final class SslConfigurationValidator { - - private SslConfigurationValidator() { - } - - public static void validateKeyAlias(KeyStore keyStore, String keyAlias) { - SslBundleKey.of(null, keyAlias).assertContainsAlias(keyStore); - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslStoreProvider.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslStoreProvider.java deleted file mode 100644 index 31f2de86de6f..000000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/SslStoreProvider.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2012-2023 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.web.server; - -import java.security.KeyStore; - -import org.springframework.boot.ssl.SslBundle; - -/** - * Interface to provide SSL key stores for an {@link WebServer} to use. Can be used when - * file based key stores cannot be used. - * - * @author Phillip Webb - * @since 2.0.0 - * @deprecated since 3.1.0 for removal in 3.3.0 in favor of registering an - * {@link SslBundle}. - */ -@Deprecated(since = "3.1.0", forRemoval = true) -public interface SslStoreProvider { - - /** - * Return the key store that should be used. - * @return the key store to use - * @throws Exception on load error - */ - KeyStore getKeyStore() throws Exception; - - /** - * Return the trust store that should be used. - * @return the trust store to use - * @throws Exception on load error - */ - KeyStore getTrustStore() throws Exception; - - /** - * Return the password of the private key in the key store. - * @return the key password - * @since 2.7.2 - */ - default String getKeyPassword() { - return null; - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java index adcff6722b0e..f5e6f448b743 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -16,8 +16,6 @@ package org.springframework.boot.web.server; -import java.security.KeyStore; - import org.springframework.boot.ssl.NoSuchSslBundleException; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundleKey; @@ -31,10 +29,9 @@ import org.springframework.boot.ssl.pem.PemSslStoreDetails; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import org.springframework.util.function.ThrowingSupplier; /** - * {@link SslBundle} backed by {@link Ssl} or an {@link SslStoreProvider}. + * {@link SslBundle} backed by {@link Ssl}. * * @author Scott Frederick * @author Phillip Webb @@ -109,7 +106,7 @@ public SslManagerBundle getManagers() { * @throws NoSuchSslBundleException if a bundle lookup fails */ public static SslBundle get(Ssl ssl) throws NoSuchSslBundleException { - return get(ssl, null, null); + return get(ssl, null); } /** @@ -121,30 +118,8 @@ public static SslBundle get(Ssl ssl) throws NoSuchSslBundleException { * @throws NoSuchSslBundleException if a bundle lookup fails */ public static SslBundle get(Ssl ssl, SslBundles sslBundles) throws NoSuchSslBundleException { - return get(ssl, sslBundles, null); - } - - /** - * Get the {@link SslBundle} that should be used for the given {@link Ssl} and - * {@link SslStoreProvider} instances. - * @param ssl the source {@link Ssl} instance - * @param sslBundles the bundles that should be used when {@link Ssl#getBundle()} is - * set - * @param sslStoreProvider the {@link SslStoreProvider} to use or {@code null} - * @return a {@link SslBundle} instance - * @throws NoSuchSslBundleException if a bundle lookup fails - * @deprecated since 3.1.0 for removal in 3.3.0 along with {@link SslStoreProvider} - */ - @Deprecated(since = "3.1.0", forRemoval = true) - @SuppressWarnings("removal") - public static SslBundle get(Ssl ssl, SslBundles sslBundles, SslStoreProvider sslStoreProvider) { Assert.state(Ssl.isEnabled(ssl), "SSL is not enabled"); - String keyPassword = (sslStoreProvider != null) ? sslStoreProvider.getKeyPassword() : null; - keyPassword = (keyPassword != null) ? keyPassword : ssl.getKeyPassword(); - if (sslStoreProvider != null) { - SslStoreBundle stores = new SslStoreProviderBundleAdapter(sslStoreProvider); - return new WebServerSslBundle(stores, keyPassword, ssl); - } + String keyPassword = ssl.getKeyPassword(); String bundleName = ssl.getBundle(); if (StringUtils.hasText(bundleName)) { Assert.state(sslBundles != null, @@ -183,33 +158,4 @@ private static boolean hasJavaKeyStoreProperties(Ssl ssl) { || (ssl.getKeyStoreType() != null && ssl.getKeyStoreType().equals("PKCS11")); } - /** - * Class to adapt a {@link SslStoreProvider} into a {@link SslStoreBundle}. - */ - @SuppressWarnings("removal") - private static class SslStoreProviderBundleAdapter implements SslStoreBundle { - - private final SslStoreProvider sslStoreProvider; - - SslStoreProviderBundleAdapter(SslStoreProvider sslStoreProvider) { - this.sslStoreProvider = sslStoreProvider; - } - - @Override - public KeyStore getKeyStore() { - return ThrowingSupplier.of(this.sslStoreProvider::getKeyStore).get(); - } - - @Override - public String getKeyStorePassword() { - return null; - } - - @Override - public KeyStore getTrustStore() { - return ThrowingSupplier.of(this.sslStoreProvider::getTrustStore).get(); - } - - } - } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java index a845ffb1e7e4..0c1545b258a3 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -16,42 +16,26 @@ package org.springframework.boot.web.embedded.tomcat; -import java.io.IOException; -import java.io.InputStream; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.util.Set; - -import org.apache.catalina.LifecycleState; import org.apache.catalina.connector.Connector; import org.apache.catalina.startup.Tomcat; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.tomcat.util.net.SSLHostConfig; -import org.apache.tomcat.util.net.SSLHostConfigCertificate; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.testsupport.system.OutputCaptureExtension; import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories; import org.springframework.boot.web.embedded.test.MockPkcs11Security; import org.springframework.boot.web.embedded.test.MockPkcs11SecurityProvider; import org.springframework.boot.web.server.Ssl; -import org.springframework.boot.web.server.SslStoreProvider; import org.springframework.boot.web.server.WebServerSslBundle; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatNoException; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; /** * Tests for {@link SslConnectorCustomizer} @@ -61,7 +45,6 @@ * @author Scott Frederick * @author Cyril Dangerville */ -@SuppressWarnings("removal") @ExtendWith(OutputCaptureExtension.class) @DirtiesUrlFactories @MockPkcs11Security @@ -131,62 +114,6 @@ void sslEnabledProtocolsConfiguration() throws Exception { assertThat(sslHostConfig.getEnabledProtocols()).containsExactly("TLSv1.2"); } - @Test - @Deprecated(since = "3.1.0", forRemoval = true) - void customizeWhenSslStoreProviderProvidesOnlyKeyStoreShouldUseDefaultTruststore() throws Exception { - Ssl ssl = new Ssl(); - ssl.setKeyPassword("password"); - ssl.setTrustStore("src/test/resources/test.jks"); - SslStoreProvider sslStoreProvider = mock(SslStoreProvider.class); - KeyStore keyStore = loadStore(); - given(sslStoreProvider.getKeyStore()).willReturn(keyStore); - Connector connector = this.tomcat.getConnector(); - SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth()); - customizer.customize(WebServerSslBundle.get(ssl, null, sslStoreProvider)); - this.tomcat.start(); - SSLHostConfig sslHostConfig = connector.getProtocolHandler().findSslHostConfigs()[0]; - SSLHostConfig sslHostConfigWithDefaults = new SSLHostConfig(); - assertThat(sslHostConfig.getTruststoreFile()).isEqualTo(sslHostConfigWithDefaults.getTruststoreFile()); - Set certificates = sslHostConfig.getCertificates(); - assertThat(certificates).hasSize(1); - assertThat(certificates.iterator().next().getCertificateKeystore()).isEqualTo(keyStore); - } - - @Test - @Deprecated(since = "3.1.0", forRemoval = true) - void customizeWhenSslStoreProviderProvidesOnlyTrustStoreShouldUseDefaultKeystore() throws Exception { - Ssl ssl = new Ssl(); - ssl.setKeyPassword("password"); - ssl.setKeyStore("src/test/resources/test.jks"); - SslStoreProvider sslStoreProvider = mock(SslStoreProvider.class); - KeyStore trustStore = loadStore(); - given(sslStoreProvider.getTrustStore()).willReturn(trustStore); - Connector connector = this.tomcat.getConnector(); - SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth()); - customizer.customize(WebServerSslBundle.get(ssl, null, sslStoreProvider)); - this.tomcat.start(); - SSLHostConfig sslHostConfig = connector.getProtocolHandler().findSslHostConfigs()[0]; - assertThat(sslHostConfig.getTruststore()).isEqualTo(trustStore); - } - - @Test - @Deprecated(since = "3.1.0", forRemoval = true) - void customizeWhenSslStoreProviderPresentShouldIgnorePasswordFromSsl(CapturedOutput output) throws Exception { - System.setProperty("javax.net.ssl.trustStorePassword", "trustStoreSecret"); - Ssl ssl = new Ssl(); - ssl.setKeyPassword("password"); - ssl.setKeyStorePassword("secret"); - SslStoreProvider sslStoreProvider = mock(SslStoreProvider.class); - given(sslStoreProvider.getTrustStore()).willReturn(loadStore()); - given(sslStoreProvider.getKeyStore()).willReturn(loadStore()); - Connector connector = this.tomcat.getConnector(); - SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth()); - customizer.customize(WebServerSslBundle.get(ssl, null, sslStoreProvider)); - this.tomcat.start(); - assertThat(connector.getState()).isEqualTo(LifecycleState.STARTED); - assertThat(output).doesNotContain("Password verification failed"); - } - @Test void customizeWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsException() { assertThatIllegalStateException().isThrownBy(() -> { @@ -221,13 +148,4 @@ void customizeWhenSslIsEnabledWithPkcs11AndKeyStoreProvider() { assertThatNoException().isThrownBy(() -> customizer.customize(WebServerSslBundle.get(ssl))); } - private KeyStore loadStore() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { - KeyStore keyStore = KeyStore.getInstance("JKS"); - Resource resource = new ClassPathResource("test.jks"); - try (InputStream stream = resource.getInputStream()) { - keyStore.load(stream, "secret".toCharArray()); - return keyStore; - } - } - } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/SslConfigurationValidatorTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/SslConfigurationValidatorTests.java deleted file mode 100644 index df9697747967..000000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/SslConfigurationValidatorTests.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2012-2023 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.web.server; - -import java.io.FileInputStream; -import java.io.InputStream; -import java.security.KeyStore; -import java.security.KeyStoreException; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; - -/** - * Tests for {@link SslConfigurationValidator}. - * - * @author Chris Bono - */ -@SuppressWarnings("removal") -@Deprecated(since = "3.1.0", forRemoval = true) -class SslConfigurationValidatorTests { - - private static final String VALID_ALIAS = "test-alias"; - - private static final String INVALID_ALIAS = "test-alias-5150"; - - private KeyStore keyStore; - - @BeforeEach - void loadKeystore() throws Exception { - this.keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - try (InputStream stream = new FileInputStream("src/test/resources/test.jks")) { - this.keyStore.load(stream, "secret".toCharArray()); - } - } - - @Test - void validateKeyAliasWhenAliasFoundShouldNotFail() { - SslConfigurationValidator.validateKeyAlias(this.keyStore, VALID_ALIAS); - } - - @Test - void validateKeyAliasWhenNullAliasShouldNotFail() { - SslConfigurationValidator.validateKeyAlias(this.keyStore, null); - } - - @Test - void validateKeyAliasWhenEmptyAliasShouldNotFail() { - SslConfigurationValidator.validateKeyAlias(this.keyStore, ""); - } - - @Test - void validateKeyAliasWhenAliasNotFoundShouldThrowException() { - assertThatIllegalStateException() - .isThrownBy(() -> SslConfigurationValidator.validateKeyAlias(this.keyStore, INVALID_ALIAS)) - .withMessage("Keystore does not contain alias '" + INVALID_ALIAS + "'"); - } - - @Test - void validateKeyAliasWhenKeyStoreThrowsExceptionOnContains() throws KeyStoreException { - KeyStore uninitializedKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - assertThatIllegalStateException() - .isThrownBy(() -> SslConfigurationValidator.validateKeyAlias(uninitializedKeyStore, "alias")) - .withMessage("Could not determine if keystore contains alias 'alias'"); - } - -} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/WebServerSslBundleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/WebServerSslBundleTests.java index c6a22a22fa8e..66ce3353d391 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/WebServerSslBundleTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/server/WebServerSslBundleTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -16,9 +16,6 @@ package org.springframework.boot.web.server; -import java.io.InputStream; -import java.security.KeyStore; - import org.junit.jupiter.api.Test; import org.springframework.boot.ssl.SslBundle; @@ -27,13 +24,9 @@ import org.springframework.boot.ssl.SslStoreBundle; import org.springframework.boot.web.embedded.test.MockPkcs11Security; import org.springframework.boot.web.embedded.test.MockPkcs11SecurityProvider; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; /** * Tests for {@link WebServerSslBundle}. @@ -122,35 +115,6 @@ void whenFromPemProperties() { assertThat(options.getEnabledProtocols()).containsExactly("TLSv1.1", "TLSv1.2"); } - @Test - @Deprecated(since = "3.1.0", forRemoval = true) - @SuppressWarnings("removal") - void whenFromCustomSslStoreProvider() throws Exception { - SslStoreProvider sslStoreProvider = mock(SslStoreProvider.class); - KeyStore keyStore = loadStore(); - given(sslStoreProvider.getKeyStore()).willReturn(keyStore); - given(sslStoreProvider.getTrustStore()).willReturn(keyStore); - Ssl ssl = new Ssl(); - ssl.setKeyStoreType("PKCS12"); - ssl.setTrustStoreType("PKCS12"); - ssl.setKeyPassword("password"); - ssl.setClientAuth(Ssl.ClientAuth.NONE); - ssl.setCiphers(new String[] { "ONE", "TWO", "THREE" }); - ssl.setEnabledProtocols(new String[] { "TLSv1.1", "TLSv1.2" }); - ssl.setProtocol("TLSv1.1"); - SslBundle bundle = WebServerSslBundle.get(ssl, null, sslStoreProvider); - assertThat(bundle).isNotNull(); - SslBundleKey key = bundle.getKey(); - assertThat(key.getPassword()).isEqualTo("password"); - assertThat(key.getAlias()).isNull(); - SslStoreBundle stores = bundle.getStores(); - assertThat(stores.getKeyStore()).isNotNull(); - assertThat(stores.getTrustStore()).isNotNull(); - SslOptions options = bundle.getOptions(); - assertThat(options.getCiphers()).containsExactly("ONE", "TWO", "THREE"); - assertThat(options.getEnabledProtocols()).containsExactly("TLSv1.1", "TLSv1.2"); - } - @Test void whenMissingPropertiesThrowsException() { Ssl ssl = new Ssl(); @@ -158,13 +122,4 @@ void whenMissingPropertiesThrowsException() { .withMessageContaining("SSL is enabled but no trust material is configured"); } - private KeyStore loadStore() throws Exception { - Resource resource = new ClassPathResource("test.p12"); - try (InputStream stream = resource.getInputStream()) { - KeyStore keyStore = KeyStore.getInstance("PKCS12"); - keyStore.load(stream, "secret".toCharArray()); - return keyStore; - } - } - } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java index 37ea7ad293e1..6db36575b5bd 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java @@ -30,7 +30,6 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.security.KeyStore; -import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; @@ -134,14 +133,12 @@ import org.springframework.boot.web.server.Shutdown; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.Ssl.ClientAuth; -import org.springframework.boot.web.server.SslStoreProvider; import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.server.WebServerException; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletContextInitializer; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.boot.web.servlet.server.Session.SessionTrackingMode; -import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpMethod; @@ -161,9 +158,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -177,7 +172,6 @@ * @author Raja Kolli * @author Scott Frederick */ -@SuppressWarnings("removal") @ExtendWith(OutputCaptureExtension.class) @DirtiesUrlFactories public abstract class AbstractServletWebServerFactoryTests { @@ -682,33 +676,6 @@ void sslWantsClientAuthenticationSucceedsWithoutClientCertificate() throws Excep assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory)).isEqualTo("test"); } - @Test - @Deprecated(since = "3.1.0", forRemoval = true) - void sslWithCustomSslStoreProvider() throws Exception { - AbstractServletWebServerFactory factory = getFactory(); - addTestTxtFile(factory); - Ssl ssl = new Ssl(); - ssl.setClientAuth(ClientAuth.NEED); - ssl.setKeyPassword("password"); - factory.setSsl(ssl); - SslStoreProvider sslStoreProvider = mock(SslStoreProvider.class); - given(sslStoreProvider.getKeyStore()).willReturn(loadStore()); - given(sslStoreProvider.getTrustStore()).willReturn(loadStore()); - factory.setSslStoreProvider(sslStoreProvider); - this.webServer = factory.getWebServer(); - this.webServer.start(); - KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - loadStore(keyStore, new FileSystemResource("src/test/resources/test.jks")); - SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory( - new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy()) - .loadKeyMaterial(keyStore, "password".toCharArray()) - .build()); - HttpComponentsClientHttpRequestFactory requestFactory = createHttpComponentsRequestFactory(socketFactory); - assertThat(getResponse(getLocalUrl("https", "/test.txt"), requestFactory)).isEqualTo("test"); - then(sslStoreProvider).should(atLeastOnce()).getKeyStore(); - then(sslStoreProvider).should(atLeastOnce()).getTrustStore(); - } - @Test void disableJspServletRegistration() throws Exception { AbstractServletWebServerFactory factory = getFactory(); @@ -1597,13 +1564,6 @@ protected final void doWithBlockedPort(BlockedPortAction action) throws Exceptio } } - private KeyStore loadStore() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { - KeyStore keyStore = KeyStore.getInstance("JKS"); - Resource resource = new ClassPathResource("test.jks"); - loadStore(keyStore, resource); - return keyStore; - } - private void loadStore(KeyStore keyStore, Resource resource) throws IOException, NoSuchAlgorithmException, CertificateException { try (InputStream stream = resource.getInputStream()) { From 28490738ae34f5db52ed255424636f3e3ad3e940 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 5 Jan 2024 11:40:41 +0000 Subject: [PATCH 0986/1215] Revert "Upgrade to HttpClient5 5.3" This reverts commit 31f3f31ac1c2e4f3cc5a332de56ff13f05f9434a. See gh-39007 --- .../spring-boot-dependencies/build.gradle | 2 +- .../test/web/client/TestRestTemplate.java | 14 ++------------ .../TomcatServletWebServerFactoryTests.java | 3 ++- .../AbstractServletWebServerFactoryTests.java | 19 ++++--------------- 4 files changed, 9 insertions(+), 29 deletions(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index be2ddace7891..74927f78fd7a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -442,7 +442,7 @@ bom { ] } } - library("HttpClient5", "5.3") { + library("HttpClient5", "5.2.3") { group("org.apache.httpcomponents.client5") { modules = [ "httpclient5", diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplate.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplate.java index e9d7b8e72195..7ec967c573de 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplate.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplate.java @@ -21,7 +21,6 @@ import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; -import java.security.cert.X509Certificate; import java.time.Duration; import java.util.Arrays; import java.util.HashSet; @@ -41,11 +40,11 @@ import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder; +import org.apache.hc.client5.http.ssl.TrustSelfSignedStrategy; import org.apache.hc.core5.http.io.SocketConfig; import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.http.ssl.TLS; import org.apache.hc.core5.ssl.SSLContextBuilder; -import org.apache.hc.core5.ssl.TrustStrategy; import org.springframework.boot.web.client.ClientHttpRequestFactorySettings; import org.springframework.boot.web.client.RestTemplateBuilder; @@ -994,7 +993,7 @@ public enum HttpClientOption { ENABLE_REDIRECTS, /** - * Use a {@link SSLConnectionSocketFactory} that trusts self-signed certificates. + * Use a {@link SSLConnectionSocketFactory} with {@link TrustSelfSignedStrategy}. */ SSL @@ -1086,13 +1085,4 @@ public void handleError(ClientHttpResponse response) throws IOException { } - private static class TrustSelfSignedStrategy implements TrustStrategy { - - @Override - public boolean isTrusted(X509Certificate[] chain, String authType) { - return chain.length == 1; - } - - } - } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java index 3fe7058a52be..b9a9f58e5c7f 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2023 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. @@ -64,6 +64,7 @@ import org.apache.hc.client5.http.classic.HttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; +import org.apache.hc.client5.http.ssl.TrustSelfSignedStrategy; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.NoHttpResponseException; import org.apache.hc.core5.ssl.SSLContextBuilder; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java index 6db36575b5bd..46798f2720c4 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2023 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. @@ -87,6 +87,7 @@ import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; +import org.apache.hc.client5.http.ssl.TrustSelfSignedStrategy; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.io.HttpClientResponseHandler; import org.apache.hc.core5.http.protocol.HttpContext; @@ -1403,7 +1404,7 @@ private String setUpFactoryForCompression(int contentSize, String[] mimeTypes, S compression.setExcludedUserAgents(excludedUserAgents); } factory.setCompression(compression); - factory.addInitializers(new ServletRegistrationBean<>(new HttpServlet() { + factory.addInitializers(new ServletRegistrationBean(new HttpServlet() { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { @@ -1630,7 +1631,7 @@ private SerialNumberValidatingTrustSelfSignedStrategy(String serialNumber) { } @Override - public boolean isTrusted(X509Certificate[] chain, String authType) { + public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException { String hexSerialNumber = chain[0].getSerialNumber().toString(16); boolean isMatch = hexSerialNumber.equalsIgnoreCase(this.serialNumber); return super.isTrusted(chain, authType) && isMatch; @@ -1774,16 +1775,4 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se } - protected static class TrustSelfSignedStrategy implements TrustStrategy { - - public TrustSelfSignedStrategy() { - } - - @Override - public boolean isTrusted(X509Certificate[] chain, String authType) { - return chain.length == 1; - } - - } - } From c87c710f791ac3084e84665ddef2381feff68985 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 5 Jan 2024 11:41:47 +0000 Subject: [PATCH 0987/1215] Prohibit upgrades to HttpClient5 5.3 Closes gh-39007 --- spring-boot-project/spring-boot-dependencies/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 74927f78fd7a..70fe374a2d20 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -443,6 +443,10 @@ bom { } } library("HttpClient5", "5.2.3") { + prohibit { + versionRange "[5.3]" + because "it can NPE when discarding a connection (https://issues.apache.org/jira/browse/HTTPCLIENT-2313)" + } group("org.apache.httpcomponents.client5") { modules = [ "httpclient5", From af89c2bb5f677869ef75281af546e04db4e72aaa Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Wed, 3 Jan 2024 16:44:03 -0600 Subject: [PATCH 0988/1215] Use Spring Pulsar BOM See gh-38966 --- spring-boot-project/spring-boot-dependencies/build.gradle | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 6bd934ee0030..dc780c96470e 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1585,11 +1585,8 @@ bom { library("Spring Pulsar", "1.0.2-SNAPSHOT") { considerSnapshots() group("org.springframework.pulsar") { - modules = [ - "spring-pulsar", - "spring-pulsar-cache-provider", - "spring-pulsar-cache-provider-caffeine", - "spring-pulsar-reactive" + imports = [ + "spring-pulsar-bom" ] } } From 49e9fe66a73196d4218e9b239a2c5185bb401a3e Mon Sep 17 00:00:00 2001 From: Andrei Navrotski <6005070+anavrotski@users.noreply.github.com> Date: Sat, 25 Nov 2023 17:34:28 +0200 Subject: [PATCH 0989/1215] Align Health.down with Health.Builder.down See gh-38550 --- .../java/org/springframework/boot/actuate/health/Health.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/Health.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/Health.java index 690c75356e52..511ab4eae6d2 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/Health.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/Health.java @@ -145,7 +145,7 @@ public static Builder up() { * @param ex the exception * @return a new {@link Builder} instance */ - public static Builder down(Exception ex) { + public static Builder down(Throwable ex) { return down().withException(ex); } From 01bb806672c4f9c844c51d1645e7ac78650ee8e4 Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Thu, 23 Nov 2023 10:40:01 +0800 Subject: [PATCH 0990/1215] Treat null as CloudPlatform.NONE See gh-38510 --- .../boot/context/config/ConfigDataProperties.java | 7 ++++--- .../boot/context/config/ConfigDataPropertiesTests.java | 8 ++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataProperties.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataProperties.java index 24f45d58f7b4..6edecf415475 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataProperties.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataProperties.java @@ -32,6 +32,7 @@ * * @author Phillip Webb * @author Madhura Bhave + * @author Yanming Zhou */ class ConfigDataProperties { @@ -118,14 +119,14 @@ boolean isActive(ConfigDataActivationContext activationContext) { if (activationContext == null) { return false; } - boolean activate = true; - activate = activate && isActive(activationContext.getCloudPlatform()); + boolean activate = isActive(activationContext.getCloudPlatform()); activate = activate && isActive(activationContext.getProfiles()); return activate; } private boolean isActive(CloudPlatform cloudPlatform) { - return this.onCloudPlatform == null || this.onCloudPlatform == cloudPlatform; + return this.onCloudPlatform == null || this.onCloudPlatform == CloudPlatform.NONE && cloudPlatform == null + || this.onCloudPlatform == cloudPlatform; } private boolean isActive(Profiles profiles) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataPropertiesTests.java index e4027f513a76..3b45e84f7e36 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataPropertiesTests.java @@ -35,6 +35,7 @@ * * @author Phillip Webb * @author Madhura Bhave + * @author Yanming Zhou */ class ConfigDataPropertiesTests { @@ -98,6 +99,13 @@ void isActiveWhenSpecificCloudPlatformAgainstDifferentSpecificCloudPlatform() { assertThat(properties.isActive(context)).isFalse(); } + @Test + void isActiveWhenSpecificNoneCloudPlatformAgainstNullCloudPlatform() { + ConfigDataProperties properties = new ConfigDataProperties(NO_IMPORTS, new Activate(CloudPlatform.NONE, null)); + ConfigDataActivationContext context = new ConfigDataActivationContext(NULL_CLOUD_PLATFORM, NULL_PROFILES); + assertThat(properties.isActive(context)).isTrue(); + } + @Test void isActiveWhenNullProfilesAgainstNullProfiles() { ConfigDataProperties properties = new ConfigDataProperties(NO_IMPORTS, new Activate(null, null)); From c3a5e7695a75e0ac8b0ad2a933c170642f2a09a5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 5 Jan 2024 16:23:18 +0000 Subject: [PATCH 0991/1215] Polish "Treat null as CloudPlatform.NONE" See gh-38510 --- .../boot/context/config/ConfigDataProperties.java | 8 ++++---- .../boot/context/config/ConfigDataPropertiesTests.java | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataProperties.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataProperties.java index 6edecf415475..cdf18ae00a37 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataProperties.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -119,14 +119,14 @@ boolean isActive(ConfigDataActivationContext activationContext) { if (activationContext == null) { return false; } - boolean activate = isActive(activationContext.getCloudPlatform()); + CloudPlatform cloudPlatform = activationContext.getCloudPlatform(); + boolean activate = isActive((cloudPlatform != null) ? cloudPlatform : CloudPlatform.NONE); activate = activate && isActive(activationContext.getProfiles()); return activate; } private boolean isActive(CloudPlatform cloudPlatform) { - return this.onCloudPlatform == null || this.onCloudPlatform == CloudPlatform.NONE && cloudPlatform == null - || this.onCloudPlatform == cloudPlatform; + return this.onCloudPlatform == null || this.onCloudPlatform == cloudPlatform; } private boolean isActive(Profiles profiles) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataPropertiesTests.java index 3b45e84f7e36..b3635903f2e2 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataPropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -100,7 +100,7 @@ void isActiveWhenSpecificCloudPlatformAgainstDifferentSpecificCloudPlatform() { } @Test - void isActiveWhenSpecificNoneCloudPlatformAgainstNullCloudPlatform() { + void isActiveWhenNoneCloudPlatformAgainstNullCloudPlatform() { ConfigDataProperties properties = new ConfigDataProperties(NO_IMPORTS, new Activate(CloudPlatform.NONE, null)); ConfigDataActivationContext context = new ConfigDataActivationContext(NULL_CLOUD_PLATFORM, NULL_PROFILES); assertThat(properties.isActive(context)).isTrue(); From 93a2b1cda0ccaddcc4b930caca530e4a544640d8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 8 Jan 2024 09:43:37 +0000 Subject: [PATCH 0992/1215] Remove dependency management for Dropwizard Metrics Closes gh-39034 --- .../spring-boot-actuator-autoconfigure/build.gradle | 1 - spring-boot-project/spring-boot-dependencies/build.gradle | 7 ------- 2 files changed, 8 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index c901a3fc0af2..272e36bbc9f9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -34,7 +34,6 @@ dependencies { optional("com.hazelcast:hazelcast") optional("com.hazelcast:hazelcast-spring") optional("com.zaxxer:HikariCP") - optional("io.dropwizard.metrics:metrics-jmx") optional("io.lettuce:lettuce-core") optional("io.micrometer:micrometer-observation") optional("io.micrometer:micrometer-jakarta9") diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f2c03ded7688..0b7a0a6145b3 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -251,13 +251,6 @@ bom { ] } } - library("Dropwizard Metrics", "4.2.23") { - group("io.dropwizard.metrics") { - imports = [ - "metrics-bom" - ] - } - } library("Ehcache3", "3.10.8") { group("org.ehcache") { modules = [ From a7d88b69d422bd0f8c657007cfcfc5bde3c42332 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 12 Dec 2023 14:55:50 +0100 Subject: [PATCH 0993/1215] Add RabbitMQ properties to enable observations Observations can be enabled for the simple, direct and stream listener and on the RabbitTemplate. Closes gh-36451 --- ...bitListenerContainerFactoryConfigurer.java | 1 + .../autoconfigure/amqp/RabbitProperties.java | 26 +++++++++++++++++++ .../amqp/RabbitStreamConfiguration.java | 5 +++- .../amqp/RabbitTemplateConfigurer.java | 1 + .../amqp/RabbitAutoConfigurationTests.java | 19 ++++++++++++-- .../amqp/RabbitStreamConfigurationTests.java | 11 ++++++++ 6 files changed, 60 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractRabbitListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractRabbitListenerContainerFactoryConfigurer.java index bcfe04a6748f..c60f98999e62 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractRabbitListenerContainerFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractRabbitListenerContainerFactoryConfigurer.java @@ -134,6 +134,7 @@ protected void configure(T factory, ConnectionFactory connectionFactory, if (this.taskExecutor != null) { factory.setTaskExecutor(this.taskExecutor); } + factory.setObservationEnabled(configuration.isObservationEnabled()); ListenerRetry retryConfig = configuration.getRetry(); if (retryConfig.isEnabled()) { RetryInterceptorBuilder builder = (retryConfig.isStateless()) ? RetryInterceptorBuilder.stateless() diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java index 4279fb30df0f..4c0081ec70a5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java @@ -723,6 +723,11 @@ public abstract static class BaseContainer { */ private boolean autoStartup = true; + /** + * Whether to enable observation. + */ + private boolean observationEnabled; + public boolean isAutoStartup() { return this.autoStartup; } @@ -731,6 +736,14 @@ public void setAutoStartup(boolean autoStartup) { this.autoStartup = autoStartup; } + public boolean isObservationEnabled() { + return this.observationEnabled; + } + + public void setObservationEnabled(boolean observationEnabled) { + this.observationEnabled = observationEnabled; + } + } public abstract static class AmqpContainer extends BaseContainer { @@ -996,6 +1009,11 @@ public static class Template { */ private String defaultReceiveQueue; + /** + * Whether to enable observation. + */ + private boolean observationEnabled; + public Retry getRetry() { return this.retry; } @@ -1048,6 +1066,14 @@ public void setDefaultReceiveQueue(String defaultReceiveQueue) { this.defaultReceiveQueue = defaultReceiveQueue; } + public boolean isObservationEnabled() { + return this.observationEnabled; + } + + public void setObservationEnabled(boolean observationEnabled) { + this.observationEnabled = observationEnabled; + } + } public static class Retry { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfiguration.java index 569cdb2bf664..a94911e94e1a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfiguration.java @@ -25,6 +25,7 @@ import org.springframework.amqp.rabbit.config.ContainerCustomizer; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.amqp.RabbitProperties.StreamContainer; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -57,7 +58,9 @@ StreamRabbitListenerContainerFactory streamRabbitListenerContainerFactory(Enviro ObjectProvider> containerCustomizer) { StreamRabbitListenerContainerFactory factory = new StreamRabbitListenerContainerFactory( rabbitStreamEnvironment); - factory.setNativeListener(properties.getListener().getStream().isNativeListener()); + StreamContainer stream = properties.getListener().getStream(); + factory.setObservationEnabled(stream.isObservationEnabled()); + factory.setNativeListener(stream.isNativeListener()); consumerCustomizer.ifUnique(factory::setConsumerCustomizer); containerCustomizer.ifUnique(factory::setContainerCustomizer); return factory; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitTemplateConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitTemplateConfigurer.java index 6d20e66c7b4c..6ade448ebb30 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitTemplateConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitTemplateConfigurer.java @@ -101,6 +101,7 @@ public void configure(RabbitTemplate template, ConnectionFactory connectionFacto map.from(templateProperties::getExchange).to(template::setExchange); map.from(templateProperties::getRoutingKey).to(template::setRoutingKey); map.from(templateProperties::getDefaultReceiveQueue).whenNonNull().to(template::setDefaultReceiveQueue); + map.from(templateProperties::isObservationEnabled).to(template::setObservationEnabled); } private boolean determineMandatoryFlag() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java index de41da0fe10a..64e6a0f26bef 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java @@ -29,6 +29,7 @@ import com.rabbitmq.client.impl.CredentialsRefreshService; import com.rabbitmq.client.impl.DefaultCredentialsProvider; import org.aopalliance.aop.Advice; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledForJreRange; import org.junit.jupiter.api.condition.JRE; @@ -371,6 +372,16 @@ void testRabbitTemplateExchangeAndRoutingKey() { }); } + @Test + void shouldConfigureObservationEnabledOnTemplate() { + this.contextRunner.withUserConfiguration(TestConfiguration.class) + .withPropertyValues("spring.rabbitmq.template.observation-enabled:true") + .run((context) -> { + RabbitTemplate rabbitTemplate = context.getBean(RabbitTemplate.class); + assertThat(rabbitTemplate).extracting("observationEnabled", InstanceOfAssertFactories.BOOLEAN).isTrue(); + }); + } + @Test void testRabbitTemplateDefaultReceiveQueue() { this.contextRunner.withUserConfiguration(TestConfiguration.class) @@ -531,7 +542,8 @@ void testSimpleRabbitListenerContainerFactoryWithCustomSettings() { "spring.rabbitmq.listener.simple.idleEventInterval:5", "spring.rabbitmq.listener.simple.batchSize:20", "spring.rabbitmq.listener.simple.missingQueuesFatal:false", - "spring.rabbitmq.listener.simple.force-stop:true") + "spring.rabbitmq.listener.simple.force-stop:true", + "spring.rabbitmq.listener.simple.observation-enabled:true") .run((context) -> { SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory = context .getBean("rabbitListenerContainerFactory", SimpleRabbitListenerContainerFactory.class); @@ -539,6 +551,7 @@ void testSimpleRabbitListenerContainerFactoryWithCustomSettings() { assertThat(rabbitListenerContainerFactory).hasFieldOrPropertyWithValue("maxConcurrentConsumers", 10); assertThat(rabbitListenerContainerFactory).hasFieldOrPropertyWithValue("batchSize", 20); assertThat(rabbitListenerContainerFactory).hasFieldOrPropertyWithValue("missingQueuesFatal", false); + assertThat(rabbitListenerContainerFactory).hasFieldOrPropertyWithValue("observationEnabled", true); checkCommonProps(context, rabbitListenerContainerFactory); }); } @@ -582,12 +595,14 @@ void testDirectRabbitListenerContainerFactoryWithCustomSettings() { "spring.rabbitmq.listener.direct.defaultRequeueRejected:false", "spring.rabbitmq.listener.direct.idleEventInterval:5", "spring.rabbitmq.listener.direct.missingQueuesFatal:true", - "spring.rabbitmq.listener.direct.force-stop:true") + "spring.rabbitmq.listener.direct.force-stop:true", + "spring.rabbitmq.listener.direct.observation-enabled:true") .run((context) -> { DirectRabbitListenerContainerFactory rabbitListenerContainerFactory = context .getBean("rabbitListenerContainerFactory", DirectRabbitListenerContainerFactory.class); assertThat(rabbitListenerContainerFactory).hasFieldOrPropertyWithValue("consumersPerQueue", 5); assertThat(rabbitListenerContainerFactory).hasFieldOrPropertyWithValue("missingQueuesFatal", true); + assertThat(rabbitListenerContainerFactory).hasFieldOrPropertyWithValue("observationEnabled", true); checkCommonProps(context, rabbitListenerContainerFactory); }); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfigurationTests.java index 95549628d1e7..205abcbe0b13 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitStreamConfigurationTests.java @@ -54,6 +54,7 @@ * @author Gary Russell * @author Andy Wilkinson * @author Eddú Meléndez + * @author Moritz Halbritter */ class RabbitStreamConfigurationTests { @@ -88,6 +89,16 @@ void whenNativeListenerIsEnabledThenContainerFactoryIsConfiguredToUseNativeListe .isTrue()); } + @Test + void shouldConfigureObservations() { + this.contextRunner + .withPropertyValues("spring.rabbitmq.listener.type:stream", + "spring.rabbitmq.listener.stream.observation-enabled:true") + .run((context) -> assertThat(context.getBean(StreamRabbitListenerContainerFactory.class)) + .extracting("observationEnabled", InstanceOfAssertFactories.BOOLEAN) + .isTrue()); + } + @Test void environmentIsAutoConfiguredByDefault() { this.contextRunner.run((context) -> assertThat(context).hasSingleBean(Environment.class)); From c4be302fdbe515266a674ce9b25739627f3e71fa Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 6 Dec 2023 13:52:56 +0100 Subject: [PATCH 0994/1215] Auto-configure SpanTagAnnotationHandler Closes gh-38662 --- .../MicrometerTracingAutoConfiguration.java | 40 +++++++++++++++---- ...crometerTracingAutoConfigurationTests.java | 13 +++++- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java index 93d8acaa0e3d..e255f47c6f99 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.tracing; +import io.micrometer.common.annotation.ValueExpressionResolver; import io.micrometer.tracing.Tracer; import io.micrometer.tracing.annotation.DefaultNewSpanParser; import io.micrometer.tracing.annotation.ImperativeMethodInvocationProcessor; @@ -29,7 +30,7 @@ import io.micrometer.tracing.propagation.Propagator; import org.aspectj.weaver.Advice; -import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.BeanFactory; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -39,6 +40,10 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.SimpleEvaluationContext; /** * {@link EnableAutoConfiguration Auto-configuration} for the Micrometer Tracing API. @@ -104,14 +109,18 @@ DefaultNewSpanParser newSpanParser() { return new DefaultNewSpanParser(); } + @Bean + @ConditionalOnMissingBean + SpanTagAnnotationHandler spanTagAnnotationHandler(BeanFactory beanFactory) { + ValueExpressionResolver valueExpressionResolver = new SpelTagValueExpressionResolver(); + return new SpanTagAnnotationHandler(beanFactory::getBean, (ignored) -> valueExpressionResolver); + } + @Bean @ConditionalOnMissingBean(MethodInvocationProcessor.class) ImperativeMethodInvocationProcessor imperativeMethodInvocationProcessor(NewSpanParser newSpanParser, - Tracer tracer, ObjectProvider spanTagAnnotationHandler) { - ImperativeMethodInvocationProcessor methodInvocationProcessor = new ImperativeMethodInvocationProcessor( - newSpanParser, tracer); - spanTagAnnotationHandler.ifAvailable(methodInvocationProcessor::setSpanTagAnnotationHandler); - return methodInvocationProcessor; + Tracer tracer, SpanTagAnnotationHandler spanTagAnnotationHandler) { + return new ImperativeMethodInvocationProcessor(newSpanParser, tracer, spanTagAnnotationHandler); } @Bean @@ -122,4 +131,21 @@ SpanAspect spanAspect(MethodInvocationProcessor methodInvocationProcessor) { } + private static class SpelTagValueExpressionResolver implements ValueExpressionResolver { + + @Override + public String resolve(String expression, Object parameter) { + try { + SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); + ExpressionParser expressionParser = new SpelExpressionParser(); + Expression expressionToEvaluate = expressionParser.parseExpression(expression); + return expressionToEvaluate.getValue(context, parameter, String.class); + } + catch (Exception ex) { + throw new IllegalStateException("Unable to evaluate SpEL expression '%s'".formatted(expression), ex); + } + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java index 9c6c61f6f012..70024c57ba53 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -18,6 +18,8 @@ import java.util.List; +import io.micrometer.common.annotation.ValueExpressionResolver; +import io.micrometer.common.annotation.ValueResolver; import io.micrometer.tracing.Tracer; import io.micrometer.tracing.annotation.DefaultNewSpanParser; import io.micrometer.tracing.annotation.ImperativeMethodInvocationProcessor; @@ -63,6 +65,7 @@ void shouldSupplyBeans() { assertThat(context).hasSingleBean(DefaultNewSpanParser.class); assertThat(context).hasSingleBean(ImperativeMethodInvocationProcessor.class); assertThat(context).hasSingleBean(SpanAspect.class); + assertThat(context).hasSingleBean(SpanTagAnnotationHandler.class); }); } @@ -100,6 +103,8 @@ void shouldBackOffOnCustomBeans() { assertThat(context).hasSingleBean(ImperativeMethodInvocationProcessor.class); assertThat(context).hasBean("customSpanAspect"); assertThat(context).hasSingleBean(SpanAspect.class); + assertThat(context).hasBean("customSpanTagAnnotationHandler"); + assertThat(context).hasSingleBean(SpanTagAnnotationHandler.class); }); } @@ -215,6 +220,12 @@ SpanAspect customSpanAspect(MethodInvocationProcessor methodInvocationProcessor) return new SpanAspect(methodInvocationProcessor); } + @Bean + SpanTagAnnotationHandler customSpanTagAnnotationHandler() { + return new SpanTagAnnotationHandler((aClass) -> mock(ValueResolver.class), + (aClass) -> mock(ValueExpressionResolver.class)); + } + } @Configuration(proxyBeanMethods = false) From 2cce123bb5ebd6a7a6cd274319c7e06b103f1f7a Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 5 Dec 2023 08:37:13 +0100 Subject: [PATCH 0995/1215] Add property to control 'path' field inclusion in error responses By default it is included. Closes gh-38619 --- .../web/servlet/ManagementErrorEndpoint.java | 16 +++++++++--- .../autoconfigure/web/ErrorProperties.java | 13 ++++++++++ .../AbstractErrorWebExceptionHandler.java | 12 +++++++++ .../DefaultErrorWebExceptionHandler.java | 23 +++++++++++++--- .../error/AbstractErrorController.java | 26 +++++++++++++++++++ .../servlet/error/BasicErrorController.java | 23 +++++++++++++--- .../error/ErrorMvcAutoConfigurationTests.java | 3 +-- .../boot/web/error/ErrorAttributeOptions.java | 10 +++++-- .../error/DefaultErrorAttributes.java | 4 +++ .../servlet/error/DefaultErrorAttributes.java | 4 +++ .../error/DefaultErrorAttributesTests.java | 19 +++++++++++++- .../error/DefaultErrorAttributesTests.java | 19 +++++++++++++- 12 files changed, 157 insertions(+), 15 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ManagementErrorEndpoint.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ManagementErrorEndpoint.java index da3da1131938..970337fb6213 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ManagementErrorEndpoint.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/ManagementErrorEndpoint.java @@ -36,6 +36,7 @@ * * @author Dave Syer * @author Scott Frederick + * @author Moritz Halbritter * @since 2.0.0 */ @Controller @@ -72,6 +73,7 @@ private ErrorAttributeOptions getErrorAttributeOptions(ServletWebRequest request if (includeBindingErrors(request)) { options = options.including(Include.BINDING_ERRORS); } + options = includePath(request) ? options.including(Include.PATH) : options.excluding(Include.PATH); return options; } @@ -79,7 +81,7 @@ private boolean includeStackTrace(ServletWebRequest request) { return switch (this.errorProperties.getIncludeStacktrace()) { case ALWAYS -> true; case ON_PARAM -> getBooleanParameter(request, "trace"); - default -> false; + case NEVER -> false; }; } @@ -87,7 +89,7 @@ private boolean includeMessage(ServletWebRequest request) { return switch (this.errorProperties.getIncludeMessage()) { case ALWAYS -> true; case ON_PARAM -> getBooleanParameter(request, "message"); - default -> false; + case NEVER -> false; }; } @@ -95,7 +97,15 @@ private boolean includeBindingErrors(ServletWebRequest request) { return switch (this.errorProperties.getIncludeBindingErrors()) { case ALWAYS -> true; case ON_PARAM -> getBooleanParameter(request, "errors"); - default -> false; + case NEVER -> false; + }; + } + + private boolean includePath(ServletWebRequest request) { + return switch (this.errorProperties.getIncludePath()) { + case ALWAYS -> true; + case ON_PARAM -> getBooleanParameter(request, "path"); + case NEVER -> false; }; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorProperties.java index 9900a86157b9..03db7cc4025e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorProperties.java @@ -55,6 +55,11 @@ public class ErrorProperties { */ private IncludeAttribute includeBindingErrors = IncludeAttribute.NEVER; + /** + * When to include "path" attribute. + */ + private IncludeAttribute includePath = IncludeAttribute.ALWAYS; + private final Whitelabel whitelabel = new Whitelabel(); public String getPath() { @@ -97,6 +102,14 @@ public void setIncludeBindingErrors(IncludeAttribute includeBindingErrors) { this.includeBindingErrors = includeBindingErrors; } + public IncludeAttribute getIncludePath() { + return this.includePath; + } + + public void setIncludePath(IncludeAttribute includePath) { + this.includePath = includePath; + } + public Whitelabel getWhitelabel() { return this.whitelabel; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java index 8afe4a624fdd..e8ec35bb4979 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.java @@ -54,6 +54,7 @@ * * @author Brian Clozel * @author Scott Frederick + * @author Moritz Halbritter * @since 2.0.0 * @see ErrorAttributes */ @@ -168,6 +169,17 @@ protected boolean isBindingErrorsEnabled(ServerRequest request) { return getBooleanParameter(request, "errors"); } + /** + * Check whether the path attribute has been set on the given request. + * @param request the source request + * @return {@code true} if the path attribute has been requested, {@code false} + * otherwise + * @since 3.3.0 + */ + protected boolean isPathEnabled(ServerRequest request) { + return getBooleanParameter(request, "path"); + } + private boolean getBooleanParameter(ServerRequest request, String parameterName) { String parameter = request.queryParam(parameterName).orElse("false"); return !"false".equalsIgnoreCase(parameter); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandler.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandler.java index 5bc52de7c76f..82f2fd240aa6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandler.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandler.java @@ -75,6 +75,7 @@ * * @author Brian Clozel * @author Scott Frederick + * @author Moritz Halbritter * @since 2.0.0 */ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler { @@ -164,6 +165,7 @@ protected ErrorAttributeOptions getErrorAttributeOptions(ServerRequest request, if (isIncludeBindingErrors(request, mediaType)) { options = options.including(Include.BINDING_ERRORS); } + options = isIncludePath(request, mediaType) ? options.including(Include.PATH) : options.excluding(Include.PATH); return options; } @@ -177,7 +179,7 @@ protected boolean isIncludeStackTrace(ServerRequest request, MediaType produces) return switch (this.errorProperties.getIncludeStacktrace()) { case ALWAYS -> true; case ON_PARAM -> isTraceEnabled(request); - default -> false; + case NEVER -> false; }; } @@ -191,7 +193,7 @@ protected boolean isIncludeMessage(ServerRequest request, MediaType produces) { return switch (this.errorProperties.getIncludeMessage()) { case ALWAYS -> true; case ON_PARAM -> isMessageEnabled(request); - default -> false; + case NEVER -> false; }; } @@ -205,7 +207,22 @@ protected boolean isIncludeBindingErrors(ServerRequest request, MediaType produc return switch (this.errorProperties.getIncludeBindingErrors()) { case ALWAYS -> true; case ON_PARAM -> isBindingErrorsEnabled(request); - default -> false; + case NEVER -> false; + }; + } + + /** + * Determine if the path attribute should be included. + * @param request the source request + * @param produces the media type produced (or {@code MediaType.ALL}) + * @return if the path attribute should be included + * @since 3.3.0 + */ + protected boolean isIncludePath(ServerRequest request, MediaType produces) { + return switch (this.errorProperties.getIncludePath()) { + case ALWAYS -> true; + case ON_PARAM -> isPathEnabled(request); + case NEVER -> false; }; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/AbstractErrorController.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/AbstractErrorController.java index 4777a492d666..474b5c5519ca 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/AbstractErrorController.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/AbstractErrorController.java @@ -41,6 +41,7 @@ * @author Dave Syer * @author Phillip Webb * @author Scott Frederick + * @author Moritz Halbritter * @since 1.3.0 * @see ErrorAttributes */ @@ -74,18 +75,43 @@ protected Map getErrorAttributes(HttpServletRequest request, Err return this.errorAttributes.getErrorAttributes(webRequest, options); } + /** + * Returns whether the trace parameter is set. + * @param request the request + * @return whether the trace parameter is set + */ protected boolean getTraceParameter(HttpServletRequest request) { return getBooleanParameter(request, "trace"); } + /** + * Returns whether the message parameter is set. + * @param request the request + * @return whether the message parameter is set + */ protected boolean getMessageParameter(HttpServletRequest request) { return getBooleanParameter(request, "message"); } + /** + * Returns whether the errors parameter is set. + * @param request the request + * @return whether the errors parameter is set + */ protected boolean getErrorsParameter(HttpServletRequest request) { return getBooleanParameter(request, "errors"); } + /** + * Returns whether the path parameter is set. + * @param request the request + * @return whether the path parameter is set + * @since 3.3.0 + */ + protected boolean getPathParameter(HttpServletRequest request) { + return getBooleanParameter(request, "path"); + } + protected boolean getBooleanParameter(HttpServletRequest request, String parameterName) { String parameter = request.getParameter(parameterName); if (parameter == null) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorController.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorController.java index 610f24517f3f..3b2e4731277c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorController.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorController.java @@ -49,6 +49,7 @@ * @author Michael Stummvoll * @author Stephane Nicoll * @author Scott Frederick + * @author Moritz Halbritter * @since 1.0.0 * @see ErrorAttributes * @see ErrorProperties @@ -121,6 +122,7 @@ protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest requ if (isIncludeBindingErrors(request, mediaType)) { options = options.including(Include.BINDING_ERRORS); } + options = isIncludePath(request, mediaType) ? options.including(Include.PATH) : options.excluding(Include.PATH); return options; } @@ -134,7 +136,7 @@ protected boolean isIncludeStackTrace(HttpServletRequest request, MediaType prod return switch (getErrorProperties().getIncludeStacktrace()) { case ALWAYS -> true; case ON_PARAM -> getTraceParameter(request); - default -> false; + case NEVER -> false; }; } @@ -148,7 +150,7 @@ protected boolean isIncludeMessage(HttpServletRequest request, MediaType produce return switch (getErrorProperties().getIncludeMessage()) { case ALWAYS -> true; case ON_PARAM -> getMessageParameter(request); - default -> false; + case NEVER -> false; }; } @@ -162,7 +164,22 @@ protected boolean isIncludeBindingErrors(HttpServletRequest request, MediaType p return switch (getErrorProperties().getIncludeBindingErrors()) { case ALWAYS -> true; case ON_PARAM -> getErrorsParameter(request); - default -> false; + case NEVER -> false; + }; + } + + /** + * Determine if the path attribute should be included. + * @param request the source request + * @param produces the media type produced (or {@code MediaType.ALL}) + * @return if the path attribute should be included + * @since 3.3.0 + */ + protected boolean isIncludePath(HttpServletRequest request, MediaType produces) { + return switch (getErrorProperties().getIncludePath()) { + case ALWAYS -> true; + case ON_PARAM -> getPathParameter(request); + case NEVER -> false; }; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfigurationTests.java index e4d211f54634..a0099198d6a3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/ErrorMvcAutoConfigurationTests.java @@ -112,8 +112,7 @@ private DispatcherServletWebRequest createWebRequest(Exception ex, boolean commi } private ErrorAttributeOptions withAllOptions() { - return ErrorAttributeOptions.of(Include.EXCEPTION, Include.STACK_TRACE, Include.MESSAGE, - Include.BINDING_ERRORS); + return ErrorAttributeOptions.of(Include.values()); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorAttributeOptions.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorAttributeOptions.java index c32f683276e3..04622711e892 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorAttributeOptions.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorAttributeOptions.java @@ -88,7 +88,7 @@ private EnumSet copyIncludes() { * @return an {@code ErrorAttributeOptions} */ public static ErrorAttributeOptions defaults() { - return of(); + return of(Include.PATH); } /** @@ -135,7 +135,13 @@ public enum Include { /** * Include the binding errors attribute. */ - BINDING_ERRORS + BINDING_ERRORS, + + /** + * Include the request path. + * @since 3.3.0 + */ + PATH } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java index 354b46988a8f..3fb955851f1d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java @@ -57,6 +57,7 @@ * @author Stephane Nicoll * @author Michele Mancioppi * @author Scott Frederick + * @author Moritz Halbritter * @since 2.0.0 * @see ErrorAttributes */ @@ -79,6 +80,9 @@ public Map getErrorAttributes(ServerRequest request, ErrorAttrib if (!options.isIncluded(Include.BINDING_ERRORS)) { errorAttributes.remove("errors"); } + if (!options.isIncluded(Include.PATH)) { + errorAttributes.remove("path"); + } return errorAttributes; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java index ef02be96709b..bfe3f0b71eaf 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java @@ -61,6 +61,7 @@ * @author Stephane Nicoll * @author Vedran Pavic * @author Scott Frederick + * @author Moritz Halbritter * @since 2.0.0 * @see ErrorAttributes */ @@ -100,6 +101,9 @@ public Map getErrorAttributes(WebRequest webRequest, ErrorAttrib if (!options.isIncluded(Include.BINDING_ERRORS)) { errorAttributes.remove("errors"); } + if (!options.isIncluded(Include.PATH)) { + errorAttributes.remove("path"); + } return errorAttributes; } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java index 9b3d63375c5f..00de9d00bb3a 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -50,6 +50,7 @@ * @author Brian Clozel * @author Stephane Nicoll * @author Scott Frederick + * @author Moritz Halbritter */ class DefaultErrorAttributesTests { @@ -212,6 +213,14 @@ void includeTrace() { assertThat(attributes.get("trace").toString()).startsWith("java.lang"); } + @Test + void includePathByDefault() { + MockServerHttpRequest request = MockServerHttpRequest.get("/test").build(); + Map attributes = this.errorAttributes.getErrorAttributes(buildServerRequest(request, NOT_FOUND), + ErrorAttributeOptions.defaults()); + assertThat(attributes).containsEntry("path", "/test"); + } + @Test void includePath() { MockServerHttpRequest request = MockServerHttpRequest.get("/test").build(); @@ -220,6 +229,14 @@ void includePath() { assertThat(attributes).containsEntry("path", "/test"); } + @Test + void excludePath() { + MockServerHttpRequest request = MockServerHttpRequest.get("/test").build(); + Map attributes = this.errorAttributes.getErrorAttributes(buildServerRequest(request, NOT_FOUND), + ErrorAttributeOptions.of()); + assertThat(attributes).doesNotContainEntry("path", "/test"); + } + @Test void includeLogPrefix() { MockServerHttpRequest request = MockServerHttpRequest.get("/test").build(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributesTests.java index fe6848d0cb3f..53fa5104270f 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributesTests.java @@ -47,6 +47,7 @@ * @author Phillip Webb * @author Vedran Pavic * @author Scott Frederick + * @author Moritz Halbritter */ class DefaultErrorAttributesTests { @@ -249,13 +250,29 @@ void withoutStackTraceAttribute() { } @Test - void path() { + void shouldIncludePathByDefault() { this.request.setAttribute("jakarta.servlet.error.request_uri", "path"); Map attributes = this.errorAttributes.getErrorAttributes(this.webRequest, ErrorAttributeOptions.defaults()); assertThat(attributes).containsEntry("path", "path"); } + @Test + void shouldIncludePath() { + this.request.setAttribute("jakarta.servlet.error.request_uri", "path"); + Map attributes = this.errorAttributes.getErrorAttributes(this.webRequest, + ErrorAttributeOptions.of(Include.PATH)); + assertThat(attributes).containsEntry("path", "path"); + } + + @Test + void shouldExcludePath() { + this.request.setAttribute("jakarta.servlet.error.request_uri", "path"); + Map attributes = this.errorAttributes.getErrorAttributes(this.webRequest, + ErrorAttributeOptions.of()); + assertThat(attributes).doesNotContainEntry("path", "path"); + } + @Test void whenGetMessageIsOverriddenThenMessageAttributeContainsValueReturnedFromIt() { Map attributes = new DefaultErrorAttributes() { From 98609e875d841b4fdb9153603f0e6320ddd6e934 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 9 Jan 2024 09:54:59 +0100 Subject: [PATCH 0996/1215] Include context path in reactive DefaultErrorAttributes Closes gh-37269 --- .../web/reactive/error/DefaultErrorAttributes.java | 4 ++-- .../reactive/error/DefaultErrorAttributesTests.java | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java index 3fb955851f1d..564f16e8fa86 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -89,7 +89,7 @@ public Map getErrorAttributes(ServerRequest request, ErrorAttrib private Map getErrorAttributes(ServerRequest request, boolean includeStackTrace) { Map errorAttributes = new LinkedHashMap<>(); errorAttributes.put("timestamp", new Date()); - errorAttributes.put("path", request.path()); + errorAttributes.put("path", request.requestPath().value()); Throwable error = getError(request); MergedAnnotation responseStatusAnnotation = MergedAnnotations .from(error.getClass(), SearchStrategy.TYPE_HIERARCHY) diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java index 00de9d00bb3a..9fa2d22fcd39 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java @@ -225,10 +225,18 @@ void includePathByDefault() { void includePath() { MockServerHttpRequest request = MockServerHttpRequest.get("/test").build(); Map attributes = this.errorAttributes.getErrorAttributes(buildServerRequest(request, NOT_FOUND), - ErrorAttributeOptions.defaults()); + ErrorAttributeOptions.of(Include.PATH)); assertThat(attributes).containsEntry("path", "/test"); } + @Test + void pathShouldIncludeContext() { + MockServerHttpRequest request = MockServerHttpRequest.get("/context/test").contextPath("/context").build(); + Map attributes = this.errorAttributes.getErrorAttributes(buildServerRequest(request, NOT_FOUND), + ErrorAttributeOptions.of(Include.PATH)); + assertThat(attributes).containsEntry("path", "/context/test"); + } + @Test void excludePath() { MockServerHttpRequest request = MockServerHttpRequest.get("/test").build(); From 91d187ca38f526ae64698a8a241e77d1e63ecae1 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 6 Sep 2023 16:30:37 +0200 Subject: [PATCH 0997/1215] Add property for max queue size for Tomcat Co-authored-by: Ahmed A. Hussein Closes gh-36087 --- .../autoconfigure/web/ServerProperties.java | 15 +++++++- .../TomcatWebServerFactoryCustomizer.java | 35 ++++++++++--------- ...TomcatWebServerFactoryCustomizerTests.java | 19 +++++++++- .../TomcatReactiveWebServerFactory.java | 9 +++++ .../tomcat/TomcatServletWebServerFactory.java | 9 +++++ 5 files changed, 68 insertions(+), 19 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index e8742b414f6a..ca10a7335352 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -905,6 +905,11 @@ public static class Threads { */ private int minSpare = 10; + /** + * Maximum capacity of the thread pool's backing queue. + */ + private int maxQueueCapacity = 2147483647; + public int getMax() { return this.max; } @@ -921,6 +926,14 @@ public void setMinSpare(int minSpare) { this.minSpare = minSpare; } + public int getMaxQueueCapacity() { + return this.maxQueueCapacity; + } + + public void setMaxQueueCapacity(int maxQueueCapacity) { + this.maxQueueCapacity = maxQueueCapacity; + } + } /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java index 1c357d2d0ac0..4c5340ca5129 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizer.java @@ -21,7 +21,10 @@ import java.util.function.ObjIntConsumer; import java.util.stream.Collectors; +import javax.management.ObjectName; + import org.apache.catalina.Lifecycle; +import org.apache.catalina.core.StandardThreadExecutor; import org.apache.catalina.valves.AccessLogValve; import org.apache.catalina.valves.ErrorReportValve; import org.apache.catalina.valves.RemoteIpValve; @@ -36,6 +39,7 @@ import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.ServerProperties.Tomcat.Accesslog; import org.springframework.boot.autoconfigure.web.ServerProperties.Tomcat.Remoteip; +import org.springframework.boot.autoconfigure.web.ServerProperties.Tomcat.Threads; import org.springframework.boot.cloud.CloudPlatform; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.web.embedded.tomcat.ConfigurableTomcatWebServerFactory; @@ -94,13 +98,7 @@ public void customize(ConfigurableTomcatWebServerFactory factory) { .as(Long::intValue) .to(factory::setBackgroundProcessorDelay); customizeRemoteIpValve(factory); - ServerProperties.Tomcat.Threads threadProperties = properties.getThreads(); - map.from(threadProperties::getMax) - .when(this::isPositive) - .to((maxThreads) -> customizeMaxThreads(factory, threadProperties.getMax())); - map.from(threadProperties::getMinSpare) - .when(this::isPositive) - .to((minSpareThreads) -> customizeMinThreads(factory, minSpareThreads)); + configureExecutor(factory, properties.getThreads()); map.from(this.serverProperties.getMaxHttpRequestHeaderSize()) .asInt(DataSize::toBytes) .when(this::isPositive) @@ -148,6 +146,19 @@ public void customize(ConfigurableTomcatWebServerFactory factory) { customizeErrorReportValve(this.serverProperties.getError(), factory); } + private void configureExecutor(ConfigurableTomcatWebServerFactory factory, Threads threadProperties) { + factory.addProtocolHandlerCustomizers((handler) -> { + StandardThreadExecutor executor = new StandardThreadExecutor(); + executor.setMinSpareThreads(threadProperties.getMinSpare()); + executor.setMaxThreads(threadProperties.getMax()); + executor.setMaxQueueSize(threadProperties.getMaxQueueCapacity()); + if (handler instanceof AbstractProtocol protocol) { + executor.setNamePrefix(ObjectName.unquote(protocol.getName()) + "-exec-"); + } + handler.setExecutor(executor); + }); + } + private boolean isPositive(int value) { return value > 0; } @@ -252,16 +263,6 @@ private boolean getOrDeduceUseForwardHeaders() { return this.serverProperties.getForwardHeadersStrategy() == ServerProperties.ForwardHeadersStrategy.NATIVE; } - @SuppressWarnings("rawtypes") - private void customizeMaxThreads(ConfigurableTomcatWebServerFactory factory, int maxThreads) { - customizeHandler(factory, maxThreads, AbstractProtocol.class, AbstractProtocol::setMaxThreads); - } - - @SuppressWarnings("rawtypes") - private void customizeMinThreads(ConfigurableTomcatWebServerFactory factory, int minSpareThreads) { - customizeHandler(factory, minSpareThreads, AbstractProtocol.class, AbstractProtocol::setMinSpareThreads); - } - @SuppressWarnings("rawtypes") private void customizeMaxHttpRequestHeaderSize(ConfigurableTomcatWebServerFactory factory, int maxHttpRequestHeaderSize) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java index f885699393ad..205d98db53d8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/TomcatWebServerFactoryCustomizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -17,10 +17,12 @@ package org.springframework.boot.autoconfigure.web.embedded; import java.util.Locale; +import java.util.concurrent.Executor; import java.util.function.Consumer; import org.apache.catalina.Context; import org.apache.catalina.Valve; +import org.apache.catalina.core.StandardThreadExecutor; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.valves.AccessLogValve; import org.apache.catalina.valves.ErrorReportValve; @@ -58,6 +60,7 @@ * @author Rafiullah Hamedy * @author Victor Mandujano * @author Parviz Rozikov + * @author Moritz Halbritter */ class TomcatWebServerFactoryCustomizerTests { @@ -564,6 +567,20 @@ void ajpConnectorCanBeCustomized() { server.stop(); } + @Test + void configureExecutor() { + bind("server.tomcat.threads.max=10", "server.tomcat.threads.min-spare=2", + "server.tomcat.threads.max-queue-capacity=20"); + customizeAndRunServer((server) -> { + Executor executor = server.getTomcat().getConnector().getProtocolHandler().getExecutor(); + assertThat(executor).isInstanceOf(StandardThreadExecutor.class); + StandardThreadExecutor standardThreadExecutor = (StandardThreadExecutor) executor; + assertThat(standardThreadExecutor.getMaxThreads()).isEqualTo(10); + assertThat(standardThreadExecutor.getMinSpareThreads()).isEqualTo(2); + assertThat(standardThreadExecutor.getMaxQueueSize()).isEqualTo(20); + }); + } + private void bind(String... inlinedProperties) { TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, inlinedProperties); new Binder(ConfigurationPropertySources.get(this.environment)).bind("server", diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java index 4981bb81e9f1..df94ecf9e485 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java @@ -28,6 +28,7 @@ import org.apache.catalina.Context; import org.apache.catalina.Engine; +import org.apache.catalina.Executor; import org.apache.catalina.Host; import org.apache.catalina.LifecycleListener; import org.apache.catalina.Valve; @@ -135,16 +136,24 @@ public WebServer getWebServer(HttpHandler httpHandler) { tomcat.getService().addConnector(connector); customizeConnector(connector); tomcat.setConnector(connector); + registerConnectorExecutor(tomcat, connector); tomcat.getHost().setAutoDeploy(false); configureEngine(tomcat.getEngine()); for (Connector additionalConnector : this.additionalTomcatConnectors) { tomcat.getService().addConnector(additionalConnector); + registerConnectorExecutor(tomcat, additionalConnector); } TomcatHttpHandlerAdapter servlet = new TomcatHttpHandlerAdapter(httpHandler); prepareContext(tomcat.getHost(), servlet); return getTomcatWebServer(tomcat); } + private void registerConnectorExecutor(Tomcat tomcat, Connector connector) { + if (connector.getProtocolHandler().getExecutor() instanceof Executor executor) { + tomcat.getService().addExecutor(executor); + } + } + private void configureEngine(Engine engine) { engine.setBackgroundProcessorDelay(this.backgroundProcessorDelay); for (Valve valve : this.engineValves) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java index 5faed28060ab..fff2cd0100c6 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java @@ -39,6 +39,7 @@ import jakarta.servlet.http.HttpServletRequest; import org.apache.catalina.Context; import org.apache.catalina.Engine; +import org.apache.catalina.Executor; import org.apache.catalina.Host; import org.apache.catalina.Lifecycle; import org.apache.catalina.LifecycleEvent; @@ -209,15 +210,23 @@ public WebServer getWebServer(ServletContextInitializer... initializers) { tomcat.getService().addConnector(connector); customizeConnector(connector); tomcat.setConnector(connector); + registerConnectorExecutor(tomcat, connector); tomcat.getHost().setAutoDeploy(false); configureEngine(tomcat.getEngine()); for (Connector additionalConnector : this.additionalTomcatConnectors) { tomcat.getService().addConnector(additionalConnector); + registerConnectorExecutor(tomcat, additionalConnector); } prepareContext(tomcat.getHost(), initializers); return getTomcatWebServer(tomcat); } + private void registerConnectorExecutor(Tomcat tomcat, Connector connector) { + if (connector.getProtocolHandler().getExecutor() instanceof Executor executor) { + tomcat.getService().addExecutor(executor); + } + } + private void configureEngine(Engine engine) { engine.setBackgroundProcessorDelay(this.backgroundProcessorDelay); for (Valve valve : this.engineValves) { From 0ca55bf0a6d27e16a5abda6d84776cfd2e1b7e60 Mon Sep 17 00:00:00 2001 From: adispezo <80221409+adispezo@users.noreply.github.com> Date: Sat, 16 Sep 2023 10:11:19 +0200 Subject: [PATCH 0998/1215] Add local and tagged correlation fields Local fields only work in Brave and not with OpenTelemetry. Tagged fields work both with Brave and with OpenTelemetry. See gh-37435 --- .../tracing/BaggageTagSpanHandler.java | 43 +++++++++++++++++++ .../BravePropagationConfigurations.java | 25 ++++++++++- .../OpenTelemetryAutoConfiguration.java | 14 ++++-- ...penTelemetryPropagationConfigurations.java | 4 +- .../tracing/TracingProperties.java | 29 ++++++++++++- .../BaggagePropagationIntegrationTests.java | 10 +++++ .../tracing/BraveAutoConfigurationTests.java | 17 ++++++++ .../OpenTelemetryAutoConfigurationTests.java | 19 +++++++- 8 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggageTagSpanHandler.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggageTagSpanHandler.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggageTagSpanHandler.java new file mode 100644 index 000000000000..4a551215f629 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggageTagSpanHandler.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2023 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.actuate.autoconfigure.tracing; + +import java.util.Collection; + +import brave.Tags; +import brave.baggage.BaggageField; +import brave.handler.MutableSpan; +import brave.handler.SpanHandler; +import brave.propagation.TraceContext; + +class BaggageTagSpanHandler extends SpanHandler { + + private final Collection fieldsToTag; + + BaggageTagSpanHandler(Collection fieldsToTag) { + this.fieldsToTag = fieldsToTag; + } + + @Override + public boolean end(TraceContext context, MutableSpan span, Cause cause) { + for (BaggageField field : this.fieldsToTag) { + Tags.BAGGAGE_FIELD.tag(field, context, span); + } + return true; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BravePropagationConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BravePropagationConfigurations.java index 17bc93021945..203f8fb15d7a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BravePropagationConfigurations.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BravePropagationConfigurations.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -27,6 +27,7 @@ import brave.baggage.CorrelationScopeCustomizer; import brave.baggage.CorrelationScopeDecorator; import brave.context.slf4j.MDCScopeDecorator; +import brave.handler.SpanHandler; import brave.propagation.CurrentTraceContext.ScopeDecorator; import brave.propagation.Propagation; import brave.propagation.Propagation.Factory; @@ -40,6 +41,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; +import org.springframework.util.CollectionUtils; /** * Brave propagation configurations. They are imported by {@link BraveAutoConfiguration}. @@ -120,6 +122,27 @@ BaggagePropagationCustomizer remoteFieldsBaggagePropagationCustomizer() { }; } + @Bean + @Order(1) + BaggagePropagationCustomizer localFieldsBaggagePropagationCustomizer() { + return (builder) -> { + List localFields = this.tracingProperties.getBaggage().getLocalFields(); + for (String localFieldName : localFields) { + builder.add(BaggagePropagationConfig.SingleBaggageField.local(BaggageField.create(localFieldName))); + } + }; + } + + @Bean + @Order(2) + SpanHandler baggageTagSpanHandler() { + List tagFields = this.tracingProperties.getBaggage().getTagFields(); + if (CollectionUtils.isEmpty(tagFields)) { + return SpanHandler.NOOP; + } + return new BaggageTagSpanHandler(tagFields.stream().map(BaggageField::create).toList()); + } + @Bean @ConditionalOnMissingBean @ConditionalOnEnabledTracing diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java index 6e5de4b51c6c..8322ff0209d7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java @@ -16,7 +16,6 @@ package org.springframework.boot.actuate.autoconfigure.tracing; -import java.util.Collections; import java.util.List; import io.micrometer.tracing.SpanCustomizer; @@ -47,6 +46,8 @@ import io.opentelemetry.sdk.trace.export.BatchSpanProcessorBuilder; import io.opentelemetry.sdk.trace.export.SpanExporter; import io.opentelemetry.sdk.trace.samplers.Sampler; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.SpringBootVersion; @@ -57,6 +58,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; +import org.springframework.util.CollectionUtils; /** * {@link EnableAutoConfiguration Auto-configuration} for OpenTelemetry tracing. @@ -75,10 +77,15 @@ OpenTelemetryPropagationConfigurations.NoPropagation.class }) public class OpenTelemetryAutoConfiguration { + private static final Log logger = LogFactory.getLog(OpenTelemetryAutoConfiguration.class); + private final TracingProperties tracingProperties; OpenTelemetryAutoConfiguration(TracingProperties tracingProperties) { this.tracingProperties = tracingProperties; + if (!CollectionUtils.isEmpty(this.tracingProperties.getBaggage().getLocalFields())) { + logger.warn("Local fields are not supported when using OpenTelemetry!"); + } } @Bean @@ -137,9 +144,10 @@ Tracer otelTracer(OpenTelemetry openTelemetry) { @ConditionalOnMissingBean(io.micrometer.tracing.Tracer.class) OtelTracer micrometerOtelTracer(Tracer tracer, EventPublisher eventPublisher, OtelCurrentTraceContext otelCurrentTraceContext) { + List remoteFields = this.tracingProperties.getBaggage().getRemoteFields(); + List tagFields = this.tracingProperties.getBaggage().getTagFields(); return new OtelTracer(tracer, otelCurrentTraceContext, eventPublisher, - new OtelBaggageManager(otelCurrentTraceContext, this.tracingProperties.getBaggage().getRemoteFields(), - Collections.emptyList())); + new OtelBaggageManager(otelCurrentTraceContext, remoteFields, tagFields)); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryPropagationConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryPropagationConfigurations.java index 4b9fcad16613..4cc44a85de7c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryPropagationConfigurations.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryPropagationConfigurations.java @@ -16,7 +16,6 @@ package org.springframework.boot.actuate.autoconfigure.tracing; -import java.util.Collections; import java.util.List; import io.micrometer.tracing.otel.bridge.OtelBaggageManager; @@ -73,8 +72,9 @@ static class PropagationWithBaggage { @ConditionalOnEnabledTracing TextMapPropagator textMapPropagatorWithBaggage(OtelCurrentTraceContext otelCurrentTraceContext) { List remoteFields = this.tracingProperties.getBaggage().getRemoteFields(); + List tagFields = this.tracingProperties.getBaggage().getTagFields(); BaggageTextMapPropagator baggagePropagator = new BaggageTextMapPropagator(remoteFields, - new OtelBaggageManager(otelCurrentTraceContext, remoteFields, Collections.emptyList())); + new OtelBaggageManager(otelCurrentTraceContext, remoteFields, tagFields)); return CompositeTextMapPropagator.create(this.tracingProperties.getPropagation(), baggagePropagator); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/TracingProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/TracingProperties.java index 6f598cc41b95..6155abb2f742 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/TracingProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/TracingProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -103,6 +103,17 @@ public static class Baggage { */ private List remoteFields = new ArrayList<>(); + /** + * List of fields that should be accessible within the JVM process but not + * propagated over the wire. Local fields are not supported with OpenTelemetry. + */ + private List localFields = new ArrayList<>(); + + /** + * List of fields that should automatically become tags. + */ + private List tagFields = new ArrayList<>(); + public boolean isEnabled() { return this.enabled; } @@ -123,10 +134,26 @@ public List getRemoteFields() { return this.remoteFields; } + public List getLocalFields() { + return this.localFields; + } + + public List getTagFields() { + return this.tagFields; + } + public void setRemoteFields(List remoteFields) { this.remoteFields = remoteFields; } + public void setLocalFields(List localFields) { + this.localFields = localFields; + } + + public void setTagFields(List tagFields) { + this.tagFields = tagFields; + } + public static class Correlation { /** diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggagePropagationIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggagePropagationIntegrationTests.java index 77c997cd196c..de32f48b4a9a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggagePropagationIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggagePropagationIntegrationTests.java @@ -227,6 +227,16 @@ public ApplicationContextRunner get() { "management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp", "management.tracing.baggage.correlation.fields=country-code,bp"); } + }, + + BRAVE_LOCAL_FIELDS { + @Override + public ApplicationContextRunner get() { + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(BraveAutoConfiguration.class)) + .withPropertyValues("management.tracing.baggage.local-fields=country-code,bp", + "management.tracing.baggage.correlation.fields=country-code,bp"); + } } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java index ee476a85b619..b6b5d356aadf 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java @@ -26,6 +26,7 @@ import brave.SpanCustomizer; import brave.Tracer; import brave.Tracing; +import brave.baggage.BaggageField; import brave.baggage.BaggagePropagation; import brave.baggage.CorrelationScopeConfig.SingleCorrelationField; import brave.handler.SpanHandler; @@ -344,6 +345,22 @@ void compositeSpanHandlerUsesFilterPredicateAndReportersInOrder() { }); } + @Test + void shouldCreateTagHandler() { + this.contextRunner.withPropertyValues("management.tracing.baggage.tag-fields=country-code,bp") + .run((context) -> assertThat(context.getBean(BaggageTagSpanHandler.class)).extracting("fieldsToTag") + .asInstanceOf(InstanceOfAssertFactories.list(BaggageField.class)) + .extracting(BaggageField::name) + .containsExactlyInAnyOrder("country-code", "bp")); + } + + @Test + void noopOnNoTagFields() { + this.contextRunner.withPropertyValues("management.tracing.baggage.tag-fields=") + .run((context) -> assertThat(context.getBean("baggageTagSpanHandler", SpanHandler.class)) + .isSameAs(SpanHandler.NOOP)); + } + @Test void shouldDisablePropagationIfTracingIsDisabled() { this.contextRunner.withPropertyValues("management.tracing.enabled=false").run((context) -> { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java index 0d7d8b171bed..1e332bedaa64 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -51,6 +51,7 @@ import io.opentelemetry.sdk.trace.export.SpanExporter; import io.opentelemetry.sdk.trace.samplers.Sampler; import io.opentelemetry.semconv.ResourceAttributes; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -272,6 +273,22 @@ void shouldSupplyW3CPropagationWithoutBaggageWhenDisabled() { }); } + @Test + void shouldConfigureRemoteAndTaggedFields() { + this.contextRunner + .withPropertyValues("management.tracing.baggage.remote-fields=r1", + "management.tracing.baggage.tag-fields=t1") + .run((context) -> { + CompositeTextMapPropagator propagator = context.getBean(CompositeTextMapPropagator.class); + assertThat(propagator).extracting("baggagePropagator.baggageManager.remoteFields") + .asInstanceOf(InstanceOfAssertFactories.list(String.class)) + .containsExactly("r1"); + assertThat(propagator).extracting("baggagePropagator.baggageManager.tagFields") + .asInstanceOf(InstanceOfAssertFactories.list(String.class)) + .containsExactly("t1"); + }); + } + @Test void shouldCustomizeSdkTracerProvider() { this.contextRunner.withUserConfiguration(SdkTracerProviderCustomizationConfiguration.class).run((context) -> { From 7120dc07aef2d2382b56f140edfc264bf6f624e3 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 9 Jan 2024 11:37:15 +0100 Subject: [PATCH 0999/1215] Adapt to changes in Brave tagged fields handling See gh-38724 See gh-37435 --- .../tracing/BaggageTagSpanHandler.java | 43 ------------------- .../tracing/BraveAutoConfiguration.java | 39 +++++++++-------- .../BravePropagationConfigurations.java | 24 ++--------- .../tracing/BraveAutoConfigurationTests.java | 27 +++++------- 4 files changed, 35 insertions(+), 98 deletions(-) delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggageTagSpanHandler.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggageTagSpanHandler.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggageTagSpanHandler.java deleted file mode 100644 index 4a551215f629..000000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BaggageTagSpanHandler.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2012-2023 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.actuate.autoconfigure.tracing; - -import java.util.Collection; - -import brave.Tags; -import brave.baggage.BaggageField; -import brave.handler.MutableSpan; -import brave.handler.SpanHandler; -import brave.propagation.TraceContext; - -class BaggageTagSpanHandler extends SpanHandler { - - private final Collection fieldsToTag; - - BaggageTagSpanHandler(Collection fieldsToTag) { - this.fieldsToTag = fieldsToTag; - } - - @Override - public boolean end(TraceContext context, MutableSpan span, Cause cause) { - for (BaggageField field : this.fieldsToTag) { - Tags.BAGGAGE_FIELD.tag(field, context, span); - } - return true; - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfiguration.java index b1fbb798c6da..eaa754c5e1ec 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -70,13 +70,17 @@ BravePropagationConfigurations.NoPropagation.class }) public class BraveAutoConfiguration { - static final BraveBaggageManager BRAVE_BAGGAGE_MANAGER = new BraveBaggageManager(); - /** * Default value for application name if {@code spring.application.name} is not set. */ private static final String DEFAULT_APPLICATION_NAME = "application"; + private final TracingProperties tracingProperties; + + BraveAutoConfiguration(TracingProperties tracingProperties) { + this.tracingProperties = tracingProperties; + } + @Bean @ConditionalOnMissingBean @Order(Ordered.HIGHEST_PRECEDENCE) @@ -88,22 +92,22 @@ CompositeSpanHandler compositeSpanHandler(ObjectProvider @Bean @ConditionalOnMissingBean - public Tracing braveTracing(Environment environment, TracingProperties properties, List spanHandlers, + Tracing braveTracing(Environment environment, List spanHandlers, List tracingCustomizers, CurrentTraceContext currentTraceContext, Factory propagationFactory, Sampler sampler) { - if (properties.getBrave().isSpanJoiningSupported()) { - if (properties.getPropagation().getType() != null - && properties.getPropagation().getType().contains(PropagationType.W3C)) { + if (this.tracingProperties.getBrave().isSpanJoiningSupported()) { + if (this.tracingProperties.getPropagation().getType() != null + && this.tracingProperties.getPropagation().getType().contains(PropagationType.W3C)) { throw new IncompatibleConfigurationException("management.tracing.propagation.type", "management.tracing.brave.span-joining-supported"); } - if (properties.getPropagation().getType() == null - && properties.getPropagation().getProduce().contains(PropagationType.W3C)) { + if (this.tracingProperties.getPropagation().getType() == null + && this.tracingProperties.getPropagation().getProduce().contains(PropagationType.W3C)) { throw new IncompatibleConfigurationException("management.tracing.propagation.produce", "management.tracing.brave.span-joining-supported"); } - if (properties.getPropagation().getType() == null - && properties.getPropagation().getConsume().contains(PropagationType.W3C)) { + if (this.tracingProperties.getPropagation().getType() == null + && this.tracingProperties.getPropagation().getConsume().contains(PropagationType.W3C)) { throw new IncompatibleConfigurationException("management.tracing.propagation.consume", "management.tracing.brave.span-joining-supported"); } @@ -112,7 +116,7 @@ public Tracing braveTracing(Environment environment, TracingProperties propertie Builder builder = Tracing.newBuilder() .currentTraceContext(currentTraceContext) .traceId128Bit(true) - .supportsJoin(properties.getBrave().isSpanJoiningSupported()) + .supportsJoin(this.tracingProperties.getBrave().isSpanJoiningSupported()) .propagationFactory(propagationFactory) .sampler(sampler) .localServiceName(applicationName); @@ -125,13 +129,13 @@ public Tracing braveTracing(Environment environment, TracingProperties propertie @Bean @ConditionalOnMissingBean - public brave.Tracer braveTracer(Tracing tracing) { + brave.Tracer braveTracer(Tracing tracing) { return tracing.tracer(); } @Bean @ConditionalOnMissingBean - public CurrentTraceContext braveCurrentTraceContext(List scopeDecorators, + CurrentTraceContext braveCurrentTraceContext(List scopeDecorators, List currentTraceContextCustomizers) { ThreadLocalCurrentTraceContext.Builder builder = ThreadLocalCurrentTraceContext.newBuilder(); scopeDecorators.forEach(builder::addScopeDecorator); @@ -143,14 +147,15 @@ public CurrentTraceContext braveCurrentTraceContext(List customizer.customize(throwAwayBuilder)); CompositePropagationFactory propagationFactory = CompositePropagationFactory.create( - this.tracingProperties.getPropagation(), BraveAutoConfiguration.BRAVE_BAGGAGE_MANAGER, + this.tracingProperties.getPropagation(), + new BraveBaggageManager(this.tracingProperties.getBaggage().getTagFields()), LocalBaggageFields.extractFrom(throwAwayBuilder)); FactoryBuilder builder = BaggagePropagation.newFactoryBuilder(propagationFactory); throwAwayBuilder.configs().forEach(builder::add); @@ -112,20 +112,12 @@ public Propagation create(KeyFactory keyFactory) { } @Bean - @Order(0) BaggagePropagationCustomizer remoteFieldsBaggagePropagationCustomizer() { return (builder) -> { List remoteFields = this.tracingProperties.getBaggage().getRemoteFields(); for (String fieldName : remoteFields) { builder.add(BaggagePropagationConfig.SingleBaggageField.remote(BaggageField.create(fieldName))); } - }; - } - - @Bean - @Order(1) - BaggagePropagationCustomizer localFieldsBaggagePropagationCustomizer() { - return (builder) -> { List localFields = this.tracingProperties.getBaggage().getLocalFields(); for (String localFieldName : localFields) { builder.add(BaggagePropagationConfig.SingleBaggageField.local(BaggageField.create(localFieldName))); @@ -133,16 +125,6 @@ BaggagePropagationCustomizer localFieldsBaggagePropagationCustomizer() { }; } - @Bean - @Order(2) - SpanHandler baggageTagSpanHandler() { - List tagFields = this.tracingProperties.getBaggage().getTagFields(); - if (CollectionUtils.isEmpty(tagFields)) { - return SpanHandler.NOOP; - } - return new BaggageTagSpanHandler(tagFields.stream().map(BaggageField::create).toList()); - } - @Bean @ConditionalOnMissingBean @ConditionalOnEnabledTracing diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java index b6b5d356aadf..f09abe23163c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/BraveAutoConfigurationTests.java @@ -26,7 +26,6 @@ import brave.SpanCustomizer; import brave.Tracer; import brave.Tracing; -import brave.baggage.BaggageField; import brave.baggage.BaggagePropagation; import brave.baggage.CorrelationScopeConfig.SingleCorrelationField; import brave.handler.SpanHandler; @@ -345,22 +344,6 @@ void compositeSpanHandlerUsesFilterPredicateAndReportersInOrder() { }); } - @Test - void shouldCreateTagHandler() { - this.contextRunner.withPropertyValues("management.tracing.baggage.tag-fields=country-code,bp") - .run((context) -> assertThat(context.getBean(BaggageTagSpanHandler.class)).extracting("fieldsToTag") - .asInstanceOf(InstanceOfAssertFactories.list(BaggageField.class)) - .extracting(BaggageField::name) - .containsExactlyInAnyOrder("country-code", "bp")); - } - - @Test - void noopOnNoTagFields() { - this.contextRunner.withPropertyValues("management.tracing.baggage.tag-fields=") - .run((context) -> assertThat(context.getBean("baggageTagSpanHandler", SpanHandler.class)) - .isSameAs(SpanHandler.NOOP)); - } - @Test void shouldDisablePropagationIfTracingIsDisabled() { this.contextRunner.withPropertyValues("management.tracing.enabled=false").run((context) -> { @@ -371,6 +354,16 @@ void shouldDisablePropagationIfTracingIsDisabled() { }); } + @Test + void shouldConfigureTaggedFields() { + this.contextRunner.withPropertyValues("management.tracing.baggage.tag-fields=t1").run((context) -> { + BraveTracer braveTracer = context.getBean(BraveTracer.class); + assertThat(braveTracer).extracting("braveBaggageManager.tagFields") + .asInstanceOf(InstanceOfAssertFactories.list(String.class)) + .containsExactly("t1"); + }); + } + private void injectToMap(Map map, String key, String value) { map.put(key, value); } From bfa84f2355dad88202a8c18e9f5f567e6b669e4c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jan 2024 12:46:34 +0000 Subject: [PATCH 1000/1215] Revert "Start building against Micrometer 1.13.0 snapshots" This reverts commit 2e7e8cf61ae4a681ae17cce4333099a900f1270e. See gh-38984 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 0b7a0a6145b3..763d0a798aaf 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1000,7 +1000,7 @@ bom { ] } } - library("Micrometer", "1.13.0-SNAPSHOT") { + library("Micrometer", "1.12.1") { considerSnapshots() group("io.micrometer") { modules = [ From c805f6ad0fdb20d71b759d8dc847becf0b8fdb0a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jan 2024 12:47:37 +0000 Subject: [PATCH 1001/1215] Revert "Start building against Micrometer Tracing 1.3.0 snapshots" This reverts commit f31bbbbeaa0f535b74bcf9aaf037b6799e2cec21. See gh-38985 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 763d0a798aaf..e1708ab8cd83 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1013,7 +1013,7 @@ bom { ] } } - library("Micrometer Tracing", "1.3.0-SNAPSHOT") { + library("Micrometer Tracing", "1.2.1") { considerSnapshots() calendarName = "Tracing" group("io.micrometer") { From 1f636adb342b1bc42ec039af9d8b50e5f7181a68 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jan 2024 12:54:50 +0000 Subject: [PATCH 1002/1215] Upgrade to Micrometer 1.12.2 Closes gh-38978 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index dc780c96470e..07ac81a0a17f 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -999,7 +999,7 @@ bom { ] } } - library("Micrometer", "1.12.2-SNAPSHOT") { + library("Micrometer", "1.12.2") { considerSnapshots() group("io.micrometer") { modules = [ From bef0ce244ebc22c67b5cd11de9ad4ea2e9ddf579 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 9 Jan 2024 12:54:51 +0000 Subject: [PATCH 1003/1215] Upgrade to Micrometer Tracing 1.2.2 Closes gh-38979 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 07ac81a0a17f..1ad6750decf7 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1012,7 +1012,7 @@ bom { ] } } - library("Micrometer Tracing", "1.2.2-SNAPSHOT") { + library("Micrometer Tracing", "1.2.2") { considerSnapshots() calendarName = "Tracing" group("io.micrometer") { From 970c2268471fe22ec9035e65932eaba9d4d31cf1 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 30 Nov 2023 14:28:55 +0100 Subject: [PATCH 1004/1215] Polish --- .../ConfigurationMetadataAnnotationProcessor.java | 3 +-- .../configurationprocessor/metadata/ConfigurationMetadata.java | 2 +- .../boot/configurationprocessor/metadata/ItemMetadata.java | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java index 4c8b0fcb1bb4..93fe3392d29c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java @@ -104,8 +104,7 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor static final String AUTO_CONFIGURATION_ANNOTATION = "org.springframework.boot.autoconfigure.AutoConfiguration"; - private static final Set SUPPORTED_OPTIONS = Collections - .unmodifiableSet(Collections.singleton(ADDITIONAL_METADATA_LOCATIONS_OPTION)); + private static final Set SUPPORTED_OPTIONS = Collections.singleton(ADDITIONAL_METADATA_LOCATIONS_OPTION); private MetadataStore metadataStore; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java index 79c62bd3ed62..ccdc4927b168 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java @@ -185,7 +185,7 @@ private boolean nullSafeEquals(Object o1, Object o2) { public static String nestedPrefix(String prefix, String name) { String nestedPrefix = (prefix != null) ? prefix : ""; String dashedName = toDashedCase(name); - nestedPrefix += (nestedPrefix == null || nestedPrefix.isEmpty()) ? dashedName : "." + dashedName; + nestedPrefix += nestedPrefix.isEmpty() ? dashedName : "." + dashedName; return nestedPrefix; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemMetadata.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemMetadata.java index f2f1e7e5e2fb..a7af0d36c96e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemMetadata.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemMetadata.java @@ -196,7 +196,7 @@ public String toString() { return string.toString(); } - protected void buildToStringProperty(StringBuilder string, String property, Object value) { + private void buildToStringProperty(StringBuilder string, String property, Object value) { if (value != null) { string.append(" ").append(property).append(":").append(value); } From 25614710d54f0dc42e0926a70cd98b4c34b9e45f Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 30 Nov 2023 14:29:19 +0100 Subject: [PATCH 1005/1215] Fail if superfluous properties are used in property metadata Closes gh-37597 --- .../metadata/JsonMarshaller.java | 95 +++++++--- .../metadata/JsonMarshallerTests.java | 163 +++++++++++++++++- 2 files changed, 233 insertions(+), 25 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java index 9f049bb72b23..f5e28187fb0e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java @@ -18,30 +18,31 @@ import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.TreeSet; import org.springframework.boot.configurationprocessor.json.JSONArray; import org.springframework.boot.configurationprocessor.json.JSONObject; import org.springframework.boot.configurationprocessor.metadata.ItemMetadata.ItemType; /** - * Marshaller to write {@link ConfigurationMetadata} as JSON. + * Marshaller to read and write {@link ConfigurationMetadata} as JSON. * * @author Stephane Nicoll * @author Phillip Webb + * @author Moritz Halbritter * @since 1.2.0 */ public class JsonMarshaller { - private static final int BUFFER_SIZE = 4098; - public void write(ConfigurationMetadata metadata, OutputStream outputStream) throws IOException { try { JSONObject object = new JSONObject(); @@ -65,42 +66,53 @@ public void write(ConfigurationMetadata metadata, OutputStream outputStream) thr public ConfigurationMetadata read(InputStream inputStream) throws Exception { ConfigurationMetadata metadata = new ConfigurationMetadata(); JSONObject object = new JSONObject(toString(inputStream)); + JsonPath path = JsonPath.root(); + checkAllowedKeys(object, path, "groups", "properties", "hints"); JSONArray groups = object.optJSONArray("groups"); if (groups != null) { for (int i = 0; i < groups.length(); i++) { - metadata.add(toItemMetadata((JSONObject) groups.get(i), ItemType.GROUP)); + metadata + .add(toItemMetadata((JSONObject) groups.get(i), path.resolve("groups").index(i), ItemType.GROUP)); } } JSONArray properties = object.optJSONArray("properties"); if (properties != null) { for (int i = 0; i < properties.length(); i++) { - metadata.add(toItemMetadata((JSONObject) properties.get(i), ItemType.PROPERTY)); + metadata.add(toItemMetadata((JSONObject) properties.get(i), path.resolve("properties").index(i), + ItemType.PROPERTY)); } } JSONArray hints = object.optJSONArray("hints"); if (hints != null) { for (int i = 0; i < hints.length(); i++) { - metadata.add(toItemHint((JSONObject) hints.get(i))); + metadata.add(toItemHint((JSONObject) hints.get(i), path.resolve("hints").index(i))); } } return metadata; } - private ItemMetadata toItemMetadata(JSONObject object, ItemType itemType) throws Exception { + private ItemMetadata toItemMetadata(JSONObject object, JsonPath path, ItemType itemType) throws Exception { + switch (itemType) { + case GROUP -> checkAllowedKeys(object, path, "name", "type", "description", "sourceType", "sourceMethod"); + case PROPERTY -> checkAllowedKeys(object, path, "name", "type", "description", "sourceType", "defaultValue", + "deprecation", "deprecated"); + } String name = object.getString("name"); String type = object.optString("type", null); String description = object.optString("description", null); String sourceType = object.optString("sourceType", null); String sourceMethod = object.optString("sourceMethod", null); Object defaultValue = readItemValue(object.opt("defaultValue")); - ItemDeprecation deprecation = toItemDeprecation(object); + ItemDeprecation deprecation = toItemDeprecation(object, path); return new ItemMetadata(itemType, name, null, type, sourceType, sourceMethod, description, defaultValue, deprecation); } - private ItemDeprecation toItemDeprecation(JSONObject object) throws Exception { + private ItemDeprecation toItemDeprecation(JSONObject object, JsonPath path) throws Exception { if (object.has("deprecation")) { JSONObject deprecationJsonObject = object.getJSONObject("deprecation"); + checkAllowedKeys(deprecationJsonObject, path.resolve("deprecation"), "level", "reason", "replacement", + "since"); ItemDeprecation deprecation = new ItemDeprecation(); deprecation.setLevel(deprecationJsonObject.optString("level", null)); deprecation.setReason(deprecationJsonObject.optString("reason", null)); @@ -111,32 +123,35 @@ private ItemDeprecation toItemDeprecation(JSONObject object) throws Exception { return object.optBoolean("deprecated") ? new ItemDeprecation() : null; } - private ItemHint toItemHint(JSONObject object) throws Exception { + private ItemHint toItemHint(JSONObject object, JsonPath path) throws Exception { + checkAllowedKeys(object, path, "name", "values", "providers"); String name = object.getString("name"); List values = new ArrayList<>(); if (object.has("values")) { JSONArray valuesArray = object.getJSONArray("values"); for (int i = 0; i < valuesArray.length(); i++) { - values.add(toValueHint((JSONObject) valuesArray.get(i))); + values.add(toValueHint((JSONObject) valuesArray.get(i), path.resolve("values").index(i))); } } List providers = new ArrayList<>(); if (object.has("providers")) { JSONArray providersObject = object.getJSONArray("providers"); for (int i = 0; i < providersObject.length(); i++) { - providers.add(toValueProvider((JSONObject) providersObject.get(i))); + providers.add(toValueProvider((JSONObject) providersObject.get(i), path.resolve("providers").index(i))); } } return new ItemHint(name, values, providers); } - private ItemHint.ValueHint toValueHint(JSONObject object) throws Exception { + private ItemHint.ValueHint toValueHint(JSONObject object, JsonPath path) throws Exception { + checkAllowedKeys(object, path, "value", "description"); Object value = readItemValue(object.get("value")); String description = object.optString("description", null); return new ItemHint.ValueHint(value, description); } - private ItemHint.ValueProvider toValueProvider(JSONObject object) throws Exception { + private ItemHint.ValueProvider toValueProvider(JSONObject object, JsonPath path) throws Exception { + checkAllowedKeys(object, path, "name", "parameters"); String name = object.getString("name"); Map parameters = new HashMap<>(); if (object.has("parameters")) { @@ -162,14 +177,48 @@ private Object readItemValue(Object value) throws Exception { } private String toString(InputStream inputStream) throws IOException { - StringBuilder out = new StringBuilder(); - InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8); - char[] buffer = new char[BUFFER_SIZE]; - int bytesRead; - while ((bytesRead = reader.read(buffer)) != -1) { - out.append(buffer, 0, bytesRead); - } - return out.toString(); + return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } + + @SuppressWarnings("unchecked") + private void checkAllowedKeys(JSONObject object, JsonPath path, String... allowedKeys) { + Set availableKeys = new TreeSet<>(); + object.keys().forEachRemaining((key) -> availableKeys.add((String) key)); + Arrays.stream(allowedKeys).forEach(availableKeys::remove); + if (!availableKeys.isEmpty()) { + throw new IllegalStateException("Expected only keys %s, but found additional keys %s. Path: %s" + .formatted(new TreeSet<>(Arrays.asList(allowedKeys)), availableKeys, path)); + } + } + + private static final class JsonPath { + + private final String path; + + private JsonPath(String path) { + this.path = path; + } + + JsonPath resolve(String path) { + if (this.path.endsWith(".")) { + return new JsonPath(this.path + path); + } + return new JsonPath(this.path + "." + path); + } + + JsonPath index(int index) { + return resolve("[%d]".formatted(index)); + } + + @Override + public String toString() { + return this.path; + } + + static JsonPath root() { + return new JsonPath("."); + } + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java index f7054f721200..e458efd52163 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java @@ -20,12 +20,14 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatException; /** * Tests for {@link JsonMarshaller}. @@ -38,14 +40,15 @@ class JsonMarshallerTests { @Test void marshallAndUnmarshal() throws Exception { ConfigurationMetadata metadata = new ConfigurationMetadata(); - metadata.add(ItemMetadata.newProperty("a", "b", StringBuffer.class.getName(), InputStream.class.getName(), - "sourceMethod", "desc", "x", new ItemDeprecation("Deprecation comment", "b.c.d", "1.2.3"))); + metadata.add(ItemMetadata.newProperty("a", "b", StringBuffer.class.getName(), InputStream.class.getName(), null, + "desc", "x", new ItemDeprecation("Deprecation comment", "b.c.d", "1.2.3"))); metadata.add(ItemMetadata.newProperty("b.c.d", null, null, null, null, null, null, null)); metadata.add(ItemMetadata.newProperty("c", null, null, null, null, null, 123, null)); metadata.add(ItemMetadata.newProperty("d", null, null, null, null, null, true, null)); metadata.add(ItemMetadata.newProperty("e", null, null, null, null, null, new String[] { "y", "n" }, null)); metadata.add(ItemMetadata.newProperty("f", null, null, null, null, null, new Boolean[] { true, false }, null)); metadata.add(ItemMetadata.newGroup("d", null, null, null)); + metadata.add(ItemMetadata.newGroup("e", null, null, "sourceMethod")); metadata.add(ItemHint.newHint("a.b")); metadata.add(ItemHint.newHint("c", new ItemHint.ValueHint(123, "hey"), new ItemHint.ValueHint(456, null))); metadata.add(new ItemHint("d", null, @@ -66,6 +69,7 @@ void marshallAndUnmarshal() throws Exception { assertThat(read).has(Metadata.withProperty("e").withDefaultValue(new String[] { "y", "n" })); assertThat(read).has(Metadata.withProperty("f").withDefaultValue(new Object[] { true, false })); assertThat(read).has(Metadata.withGroup("d")); + assertThat(read).has(Metadata.withGroup("e").fromSourceMethod("sourceMethod")); assertThat(read).has(Metadata.withHint("a.b")); assertThat(read).has(Metadata.withHint("c").withValue(0, 123, "hey").withValue(1, 456, null)); assertThat(read).has(Metadata.withHint("d").withProvider("first", "target", "foo").withProvider("second")); @@ -170,4 +174,159 @@ void orderingForSamePropertyNamesWithNullSourceType() throws IOException { "\"java.lang.Boolean\"", "\"com.example.bravo.aaa\"", "\"java.lang.Integer\"", "\"com.example.Bar"); } + @Test + void shouldCheckRootFields() { + String json = """ + { + "groups": [], "properties": [], "hints": [], "dummy": [] + }"""; + assertThatException().isThrownBy(() -> read(json)) + .withMessage("Expected only keys [groups, hints, properties], but found additional keys [dummy]. Path: ."); + } + + @Test + void shouldCheckGroupFields() { + String json = """ + { + "groups": [ + { + "name": "g", + "type": "java.lang.String", + "description": "Some description", + "sourceType": "java.lang.String", + "sourceMethod": "some()", + "dummy": "dummy" + } + ], "properties": [], "hints": [] + }"""; + assertThatException().isThrownBy(() -> read(json)) + .withMessage( + "Expected only keys [description, name, sourceMethod, sourceType, type], but found additional keys [dummy]. Path: .groups.[0]"); + } + + @Test + void shouldCheckPropertyFields() { + String json = """ + { + "groups": [], "properties": [ + { + "name": "name", + "type": "java.lang.String", + "description": "Some description", + "sourceType": "java.lang.String", + "defaultValue": "value", + "deprecation": { + "level": "warning", + "reason": "some reason", + "replacement": "name-new", + "since": "v17" + }, + "deprecated": true, + "dummy": "dummy" + } + ], "hints": [] + }"""; + assertThatException().isThrownBy(() -> read(json)) + .withMessage( + "Expected only keys [defaultValue, deprecated, deprecation, description, name, sourceType, type], but found additional keys [dummy]. Path: .properties.[0]"); + } + + @Test + void shouldCheckPropertyDeprecationFields() { + String json = """ + { + "groups": [], "properties": [ + { + "name": "name", + "type": "java.lang.String", + "description": "Some description", + "sourceType": "java.lang.String", + "defaultValue": "value", + "deprecation": { + "level": "warning", + "reason": "some reason", + "replacement": "name-new", + "since": "v17", + "dummy": "dummy" + }, + "deprecated": true + } + ], "hints": [] + }"""; + assertThatException().isThrownBy(() -> read(json)) + .withMessage( + "Expected only keys [level, reason, replacement, since], but found additional keys [dummy]. Path: .properties.[0].deprecation"); + } + + @Test + void shouldCheckHintFields() { + String json = """ + { + "groups": [], "properties": [], "hints": [ + { + "name": "name", + "values": [], + "providers": [], + "dummy": "dummy" + } + ] + }"""; + assertThatException().isThrownBy(() -> read(json)) + .withMessage( + "Expected only keys [name, providers, values], but found additional keys [dummy]. Path: .hints.[0]"); + } + + @Test + void shouldCheckHintValueFields() { + String json = """ + { + "groups": [], "properties": [], "hints": [ + { + "name": "name", + "values": [ + { + "value": "value", + "description": "some description", + "dummy": "dummy" + } + ], + "providers": [] + } + ] + }"""; + assertThatException().isThrownBy(() -> read(json)) + .withMessage( + "Expected only keys [description, value], but found additional keys [dummy]. Path: .hints.[0].values.[0]"); + } + + @Test + void shouldCheckHintProviderFields() { + String json = """ + { + "groups": [], "properties": [], "hints": [ + { + "name": "name", + "values": [], + "providers": [ + { + "name": "name", + "parameters": { + "target": "jakarta.servlet.http.HttpServlet" + }, + "dummy": "dummy" + } + ] + } + ] + }"""; + assertThatException().isThrownBy(() -> read(json)) + .withMessage( + "Expected only keys [name, parameters], but found additional keys [dummy]. Path: .hints.[0].providers.[0]"); + } + + private void read(String json) throws Exception { + JsonMarshaller marshaller = new JsonMarshaller(); + marshaller.read(new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8))); + } + } From e5f489f338a185f9a6b34d8f320774c6810bb7ff Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 9 Jan 2024 12:33:10 -0800 Subject: [PATCH 1006/1215] Restore manifest support for nested directory jars Update `NestedJarFile` so that the `getManifest()` method returns the manifest from the parent jar file for nested jars based on directory entries. This restores the previous behavior supported by Spring Boot 3.1 and allows class methods such as `getPackage().getImplementationVersion()` to return non `null` results. Fixes gh-38996 --- .../boot/loader/jar/NestedJarFile.java | 4 +- .../loader/jar/NestedJarFileResources.java | 31 ++++++++++ .../boot/loader/zip/ZipContent.java | 56 +++++++++++++++---- .../boot/loader/jar/NestedJarFileTests.java | 20 +++++++ .../boot/loader/zip/ZipContentTests.java | 20 +++++++ 5 files changed, 120 insertions(+), 11 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java index 401157b17d95..676eb046b1e1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java @@ -154,7 +154,9 @@ public InputStream getRawZipDataInputStream() throws IOException { @Override public Manifest getManifest() throws IOException { try { - return this.resources.zipContent().getInfo(ManifestInfo.class, this::getManifestInfo).getManifest(); + return this.resources.zipContentForManifest() + .getInfo(ManifestInfo.class, this::getManifestInfo) + .getManifest(); } catch (UncheckedIOException ex) { throw ex.getCause(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFileResources.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFileResources.java index 4f57e03497f8..e1fb4d8d0930 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFileResources.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFileResources.java @@ -30,6 +30,7 @@ import org.springframework.boot.loader.ref.Cleaner; import org.springframework.boot.loader.zip.ZipContent; +import org.springframework.boot.loader.zip.ZipContent.Kind; /** * Resources created managed and cleaned by a {@link NestedJarFile} instance and suitable @@ -43,6 +44,8 @@ class NestedJarFileResources implements Runnable { private ZipContent zipContent; + private ZipContent zipContentForManifest; + private final Set inputStreams = Collections.newSetFromMap(new WeakHashMap<>()); private Deque inflaterCache = new ArrayDeque<>(); @@ -55,6 +58,8 @@ class NestedJarFileResources implements Runnable { */ NestedJarFileResources(File file, String nestedEntryName) throws IOException { this.zipContent = ZipContent.open(file.toPath(), nestedEntryName); + this.zipContentForManifest = (this.zipContent.getKind() != Kind.NESTED_DIRECTORY) ? null + : ZipContent.open(file.toPath()); } /** @@ -65,6 +70,15 @@ ZipContent zipContent() { return this.zipContent; } + /** + * Return the underlying {@link ZipContent} that should be used to load manifest + * content. + * @return the zip content to use when loading the manifest + */ + ZipContent zipContentForManifest() { + return (this.zipContentForManifest != null) ? this.zipContentForManifest : this.zipContent; + } + /** * Add a managed input stream resource. * @param inputStream the input stream @@ -144,6 +158,7 @@ private void releaseAll() { exceptionChain = releaseInflators(exceptionChain); exceptionChain = releaseInputStreams(exceptionChain); exceptionChain = releaseZipContent(exceptionChain); + exceptionChain = releaseZipContentForManifest(exceptionChain); if (exceptionChain != null) { throw new UncheckedIOException(exceptionChain); } @@ -195,6 +210,22 @@ private IOException releaseZipContent(IOException exceptionChain) { return exceptionChain; } + private IOException releaseZipContentForManifest(IOException exceptionChain) { + ZipContent zipContentForManifest = this.zipContentForManifest; + if (zipContentForManifest != null) { + try { + zipContentForManifest.close(); + } + catch (IOException ex) { + exceptionChain = addToExceptionChain(exceptionChain, ex); + } + finally { + this.zipContentForManifest = null; + } + } + return exceptionChain; + } + private IOException addToExceptionChain(IOException exceptionChain, IOException ex) { if (exceptionChain != null) { exceptionChain.addSuppressed(ex); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java index ca0f3d7a713b..4e7298ffb4b9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java @@ -72,6 +72,8 @@ public final class ZipContent implements Closeable { private final Source source; + private final Kind kind; + private final FileChannelDataBlock data; private final long centralDirectoryPos; @@ -94,10 +96,11 @@ public final class ZipContent implements Closeable { private SoftReference, Object>> info; - private ZipContent(Source source, FileChannelDataBlock data, long centralDirectoryPos, long commentPos, + private ZipContent(Source source, Kind kind, FileChannelDataBlock data, long centralDirectoryPos, long commentPos, long commentLength, int[] lookupIndexes, int[] nameHashLookups, int[] relativeCentralDirectoryOffsetLookups, NameOffsetLookups nameOffsetLookups, boolean hasJarSignatureFile) { this.source = source; + this.kind = kind; this.data = data; this.centralDirectoryPos = centralDirectoryPos; this.commentPos = commentPos; @@ -109,6 +112,15 @@ private ZipContent(Source source, FileChannelDataBlock data, long centralDirecto this.hasJarSignatureFile = hasJarSignatureFile; } + /** + * Return the kind of content that was loaded. + * @return the content kind + * @since 3.2.2 + */ + public Kind getKind() { + return this.kind; + } + /** * Open a {@link DataBlock} containing the raw zip data. For container zip files, this * may be smaller than the original file since additional bytes are permitted at the @@ -380,6 +392,30 @@ private static ZipContent open(Source source) throws IOException { return zipContent; } + /** + * Zip content kinds. + * + * @since 3.2.2 + */ + public enum Kind { + + /** + * Content from a standard zip file. + */ + ZIP, + + /** + * Content from nested zip content. + */ + NESTED_ZIP, + + /** + * Content from a nested zip directory. + */ + NESTED_DIRECTORY + + } + /** * The source of {@link ZipContent}. Used as a cache key. * @@ -451,7 +487,7 @@ private void add(ZipCentralDirectoryFileHeaderRecord centralRecord, long pos, bo this.cursor++; } - private ZipContent finish(long commentPos, long commentLength, boolean hasJarSignatureFile) { + private ZipContent finish(Kind kind, long commentPos, long commentLength, boolean hasJarSignatureFile) { if (this.cursor != this.nameHashLookups.length) { this.nameHashLookups = Arrays.copyOf(this.nameHashLookups, this.cursor); this.relativeCentralDirectoryOffsetLookups = Arrays.copyOf(this.relativeCentralDirectoryOffsetLookups, @@ -463,7 +499,7 @@ private ZipContent finish(long commentPos, long commentLength, boolean hasJarSig for (int i = 0; i < size; i++) { lookupIndexes[this.index[i]] = i; } - return new ZipContent(this.source, this.data, this.centralDirectoryPos, commentPos, commentLength, + return new ZipContent(this.source, kind, this.data, this.centralDirectoryPos, commentPos, commentLength, lookupIndexes, this.nameHashLookups, this.relativeCentralDirectoryOffsetLookups, this.nameOffsetLookups, hasJarSignatureFile); } @@ -525,7 +561,7 @@ static ZipContent load(Source source) throws IOException { private static ZipContent loadNonNested(Source source) throws IOException { debug.log("Loading non-nested zip '%s'", source.path()); - return openAndLoad(source, new FileChannelDataBlock(source.path())); + return openAndLoad(source, Kind.ZIP, new FileChannelDataBlock(source.path())); } private static ZipContent loadNestedZip(Source source, Entry entry) throws IOException { @@ -534,13 +570,13 @@ private static ZipContent loadNestedZip(Source source, Entry entry) throws IOExc .formatted(source.nestedEntryName(), source.path())); } debug.log("Loading nested zip entry '%s' from '%s'", source.nestedEntryName(), source.path()); - return openAndLoad(source, entry.getContent()); + return openAndLoad(source, Kind.NESTED_ZIP, entry.getContent()); } - private static ZipContent openAndLoad(Source source, FileChannelDataBlock data) throws IOException { + private static ZipContent openAndLoad(Source source, Kind kind, FileChannelDataBlock data) throws IOException { try { data.open(); - return loadContent(source, data); + return loadContent(source, kind, data); } catch (IOException | RuntimeException ex) { data.close(); @@ -548,7 +584,7 @@ private static ZipContent openAndLoad(Source source, FileChannelDataBlock data) } } - private static ZipContent loadContent(Source source, FileChannelDataBlock data) throws IOException { + private static ZipContent loadContent(Source source, Kind kind, FileChannelDataBlock data) throws IOException { ZipEndOfCentralDirectoryRecord.Located locatedEocd = ZipEndOfCentralDirectoryRecord.load(data); ZipEndOfCentralDirectoryRecord eocd = locatedEocd.endOfCentralDirectoryRecord(); long eocdPos = locatedEocd.pos(); @@ -585,7 +621,7 @@ private static ZipContent loadContent(Source source, FileChannelDataBlock data) pos += centralRecord.size(); } long commentPos = locatedEocd.pos() + ZipEndOfCentralDirectoryRecord.COMMENT_OFFSET; - return loader.finish(commentPos, eocd.commentLength(), hasJarSignatureFile); + return loader.finish(kind, commentPos, eocd.commentLength(), hasJarSignatureFile); } /** @@ -642,7 +678,7 @@ private static ZipContent loadNestedDirectory(Source source, ZipContent zip, Ent } } } - return loader.finish(zip.commentPos, zip.commentLength, zip.hasJarSignatureFile); + return loader.finish(Kind.NESTED_DIRECTORY, zip.commentPos, zip.commentLength, zip.hasJarSignatureFile); } catch (IOException | RuntimeException ex) { zip.data.close(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java index 5c2aed4d5c4e..ac6da2187b87 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java @@ -110,6 +110,26 @@ void createWhenNestedJarDirectoryOpensJar() throws IOException { } } + @Test + void getManifestWhenNestedJarReturnsManifestOfNestedJar() throws Exception { + try (JarFile jar = new JarFile(this.file)) { + try (NestedJarFile nestedJar = new NestedJarFile(this.file, "nested.jar")) { + Manifest manifest = nestedJar.getManifest(); + assertThat(manifest).isNotEqualTo(jar.getManifest()); + assertThat(manifest.getMainAttributes().getValue("Built-By")).isEqualTo("j2"); + } + } + } + + @Test + void getManifestWhenNestedJarDirectoryReturnsManifestOfParent() throws Exception { + try (JarFile jar = new JarFile(this.file)) { + try (NestedJarFile nestedJar = new NestedJarFile(this.file, "d/")) { + assertThat(nestedJar.getManifest()).isEqualTo(jar.getManifest()); + } + } + } + @Test void createWhenJarHasFrontMatterOpensJar() throws IOException { File file = new File(this.tempDir, "frontmatter.jar"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipContentTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipContentTests.java index 8681c430d6ce..c2d8ff02e55a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipContentTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipContentTests.java @@ -48,6 +48,7 @@ import org.springframework.boot.loader.testsupport.TestJar; import org.springframework.boot.loader.zip.ZipContent.Entry; +import org.springframework.boot.loader.zip.ZipContent.Kind; import org.springframework.util.FileCopyUtils; import org.springframework.util.StreamUtils; @@ -168,6 +169,25 @@ void getEntryAsCreatesCompatibleEntries() throws IOException { } } + @Test + void getKindWhenZipReturnsZip() { + assertThat(this.zipContent.getKind()).isEqualTo(Kind.ZIP); + } + + @Test + void getKindWhenNestedZipReturnsNestedZip() throws IOException { + try (ZipContent nested = ZipContent.open(this.file.toPath(), "nested.jar")) { + assertThat(nested.getKind()).isEqualTo(Kind.NESTED_ZIP); + } + } + + @Test + void getKindWhenNestedDirectoryReturnsNestedDirectory() throws IOException { + try (ZipContent nested = ZipContent.open(this.file.toPath(), "d/")) { + assertThat(nested.getKind()).isEqualTo(Kind.NESTED_DIRECTORY); + } + } + private void assertThatFieldsAreEqual(ZipEntry actual, ZipEntry expected) { assertThat(actual.getName()).isEqualTo(expected.getName()); assertThat(actual.getTime()).isEqualTo(expected.getTime()); From 820396fdff80aed95c5b3dfcc1d8eb4ee601e7bb Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Wed, 15 Nov 2023 14:09:37 -0800 Subject: [PATCH 1007/1215] Add ProcessInfoContributor This InfoContributor exposes information about the process of the application. See gh-38371 --- .../InfoContributorAutoConfiguration.java | 10 ++- ...itional-spring-configuration-metadata.json | 6 ++ ...InfoContributorAutoConfigurationTests.java | 12 ++++ .../actuate/info/ProcessInfoContributor.java | 58 +++++++++++++++++ .../info/ProcessInfoContributorTests.java | 55 ++++++++++++++++ .../src/docs/asciidoc/actuator/endpoints.adoc | 13 +++- .../boot/info/ProcessInfo.java | 65 +++++++++++++++++++ .../boot/info/ProcessInfoTests.java | 40 ++++++++++++ 8 files changed, 257 insertions(+), 2 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/ProcessInfoContributor.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/info/ProcessInfoContributorTests.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/info/ProcessInfo.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/info/ProcessInfoTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/info/InfoContributorAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/info/InfoContributorAutoConfiguration.java index ae674854af1a..cbad4b851b09 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/info/InfoContributorAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/info/InfoContributorAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -22,6 +22,7 @@ import org.springframework.boot.actuate.info.InfoContributor; import org.springframework.boot.actuate.info.JavaInfoContributor; import org.springframework.boot.actuate.info.OsInfoContributor; +import org.springframework.boot.actuate.info.ProcessInfoContributor; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -92,4 +93,11 @@ public OsInfoContributor osInfoContributor() { return new OsInfoContributor(); } + @Bean + @ConditionalOnEnabledInfoContributor(value = "process", fallback = InfoContributorFallback.DISABLE) + @Order(DEFAULT_ORDER) + public ProcessInfoContributor processInfoContributor() { + return new ProcessInfoContributor(); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index bfdc19a9445c..cc6ae2934c11 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -309,6 +309,12 @@ "description": "Whether to enable Operating System info.", "defaultValue": false }, + { + "name": "management.info.process.enabled", + "type": "java.lang.Boolean", + "description": "Whether to enable process info.", + "defaultValue": false + }, { "name": "management.metrics.binders.files.enabled", "type": "java.lang.Boolean", diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/info/InfoContributorAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/info/InfoContributorAutoConfigurationTests.java index 62801fdd6415..cf2447a6d122 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/info/InfoContributorAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/info/InfoContributorAutoConfigurationTests.java @@ -28,11 +28,13 @@ import org.springframework.boot.actuate.info.InfoContributor; import org.springframework.boot.actuate.info.JavaInfoContributor; import org.springframework.boot.actuate.info.OsInfoContributor; +import org.springframework.boot.actuate.info.ProcessInfoContributor; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.info.BuildProperties; import org.springframework.boot.info.GitProperties; import org.springframework.boot.info.JavaInfo; import org.springframework.boot.info.OsInfo; +import org.springframework.boot.info.ProcessInfo; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -164,6 +166,16 @@ void osInfoContributor() { }); } + @Test + void processInfoContributor() { + this.contextRunner.withPropertyValues("management.info.process.enabled=true").run((context) -> { + assertThat(context).hasSingleBean(ProcessInfoContributor.class); + Map content = invokeContributor(context.getBean(ProcessInfoContributor.class)); + assertThat(content).containsKey("process"); + assertThat(content.get("process")).isInstanceOf(ProcessInfo.class); + }); + } + private Map invokeContributor(InfoContributor contributor) { Info.Builder builder = new Info.Builder(); contributor.contribute(builder); diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/ProcessInfoContributor.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/ProcessInfoContributor.java new file mode 100644 index 000000000000..65e8ec41ef7b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/ProcessInfoContributor.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2023 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.actuate.info; + +import org.springframework.aot.hint.BindingReflectionHintsRegistrar; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.boot.actuate.info.Info.Builder; +import org.springframework.boot.actuate.info.ProcessInfoContributor.ProcessInfoContributorRuntimeHints; +import org.springframework.boot.info.ProcessInfo; +import org.springframework.context.annotation.ImportRuntimeHints; + +/** + * An {@link InfoContributor} that exposes {@link ProcessInfo}. + * + * @author Jonatan Ivanov + * @since 3.3.0 + */ +@ImportRuntimeHints(ProcessInfoContributorRuntimeHints.class) +public class ProcessInfoContributor implements InfoContributor { + + private final ProcessInfo processInfo; + + public ProcessInfoContributor() { + this.processInfo = new ProcessInfo(); + } + + @Override + public void contribute(Builder builder) { + builder.withDetail("process", this.processInfo); + } + + static class ProcessInfoContributorRuntimeHints implements RuntimeHintsRegistrar { + + private final BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar(); + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + this.bindingRegistrar.registerReflectionHints(hints.reflection(), ProcessInfo.class); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/info/ProcessInfoContributorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/info/ProcessInfoContributorTests.java new file mode 100644 index 000000000000..389a3ca2bbb0 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/info/ProcessInfoContributorTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2023 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.actuate.info; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +import org.springframework.boot.actuate.info.ProcessInfoContributor.ProcessInfoContributorRuntimeHints; +import org.springframework.boot.info.ProcessInfo; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ProcessInfoContributor}. + * + * @author Jonatan Ivanov + */ +class ProcessInfoContributorTests { + + @Test + void processInfoShouldBeAdded() { + ProcessInfoContributor processInfoContributor = new ProcessInfoContributor(); + Info.Builder builder = new Info.Builder(); + processInfoContributor.contribute(builder); + Info info = builder.build(); + assertThat(info.get("process")).isInstanceOf(ProcessInfo.class); + } + + @Test + void shouldRegisterHints() { + RuntimeHints runtimeHints = new RuntimeHints(); + new ProcessInfoContributorRuntimeHints().registerHints(runtimeHints, getClass().getClassLoader()); + assertThat(RuntimeHintsPredicates.reflection() + .onType(ProcessInfo.class) + .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS)) + .accepts(runtimeHints); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/endpoints.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/endpoints.adoc index 92989a7bdd9b..641d44452187 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/endpoints.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/endpoints.adoc @@ -1087,12 +1087,17 @@ When appropriate, Spring auto-configures the following `InfoContributor` beans: | Exposes Operating System information. | None. +| `process` +| {spring-boot-actuator-module-code}/info/ProcessInfoContributor.java[`ProcessInfoContributor`] +| Exposes process information. +| None. + |=== Whether an individual contributor is enabled is controlled by its `management.info..enabled` property. Different contributors have different defaults for this property, depending on their prerequisites and the nature of the information that they expose. -With no prerequisites to indicate that they should be enabled, the `env`, `java`, and `os` contributors are disabled by default. +With no prerequisites to indicate that they should be enabled, the `env`, `java`, `os`, and `process` contributors are disabled by default. Each can be enabled by setting its `management.info..enabled` property to `true`. The `build` and `git` info contributors are enabled by default. @@ -1190,6 +1195,12 @@ The `info` endpoint publishes information about your Operating System, see {spri +[[actuator.endpoints.info.process-information]] +==== Process Information +The `info` endpoint publishes information about your process, see {spring-boot-module-api}/info/ProcessInfo.html[`Process`] for more details. + + + [[actuator.endpoints.info.writing-custom-info-contributors]] ==== Writing Custom InfoContributors To provide custom application information, you can register Spring beans that implement the {spring-boot-actuator-module-code}/info/InfoContributor.java[`InfoContributor`] interface. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/info/ProcessInfo.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/info/ProcessInfo.java new file mode 100644 index 000000000000..37fc0b027736 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/info/ProcessInfo.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2023 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.info; + +/** + * Information about the process of the application. + * + * @author Jonatan Ivanov + * @since 3.3.0 + */ +public class ProcessInfo { + + private static final Runtime runtime = Runtime.getRuntime(); + + private final long pid; + + private final long parentPid; + + private final String owner; + + public ProcessInfo() { + ProcessHandle process = ProcessHandle.current(); + this.pid = process.pid(); + this.parentPid = process.parent().map(ProcessHandle::pid).orElse(-1L); + this.owner = process.info().user().orElse(null); + } + + /** + * Number of processors available to the process. This value may change between + * invocations especially in (containerized) environments where resource usage can be + * isolated (for example using control groups). + * @return result of {@link Runtime#availableProcessors()} + * @see Runtime#availableProcessors() + */ + public int getCpus() { + return runtime.availableProcessors(); + } + + public long getPid() { + return this.pid; + } + + public long getParentPid() { + return this.parentPid; + } + + public String getOwner() { + return this.owner; + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/info/ProcessInfoTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/info/ProcessInfoTests.java new file mode 100644 index 000000000000..8337d2d243aa --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/info/ProcessInfoTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2023 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.info; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ProcessInfo}. + * + * @author Jonatan Ivanov + */ +class ProcessInfoTests { + + @Test + void processInfoIsAvailable() { + ProcessInfo processInfo = new ProcessInfo(); + assertThat(processInfo.getCpus()).isEqualTo(Runtime.getRuntime().availableProcessors()); + assertThat(processInfo.getOwner()).isEqualTo(ProcessHandle.current().info().user().orElse(null)); + assertThat(processInfo.getPid()).isEqualTo(ProcessHandle.current().pid()); + assertThat(processInfo.getParentPid()) + .isEqualTo(ProcessHandle.current().parent().map(ProcessHandle::pid).orElse(null)); + } + +} From b7c9c82180e637dc57150f2ab77768b99d1a31ca Mon Sep 17 00:00:00 2001 From: Christoph Dreis Date: Thu, 4 Jan 2024 14:29:10 +0100 Subject: [PATCH 1008/1215] Fix typos See gh-38983 --- .../boot/build/bom/bomr/version/DependencyVersion.java | 4 ++-- .../boot/autoconfigure/pulsar/PulsarProperties.java | 4 ++-- .../src/docs/asciidoc/deployment/installing.adoc | 2 +- .../spring-boot-gradle-plugin/src/docs/asciidoc/aot.adoc | 2 +- .../spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc | 2 +- .../boot/loader/nio/file/NestedFileSystem.java | 4 ++-- .../java/org/springframework/boot/loader/zip/ZipContent.java | 4 ++-- .../boot/loader/nio/file/NestedFileSystemTests.java | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/DependencyVersion.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/DependencyVersion.java index f4b9b897a1ba..d82d5b8a50f5 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/DependencyVersion.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/DependencyVersion.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -48,7 +48,7 @@ public interface DependencyVersion extends Comparable { * Returns whether the given {@code candidate} is an upgrade of this version. * @param candidate the version to consider * @param movingToSnapshots whether the upgrade is to be considered as part of moving - * to snaphots + * to snapshots * @return {@code true} if the candidate is an upgrade, otherwise false */ boolean isUpgrade(DependencyVersion candidate, boolean movingToSnapshots); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java index 7597da0c08d1..bd87c9f2f6e3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -778,7 +778,7 @@ public static class Reader { private String name; /** - * Topis the reader subscribes to. + * Topics the reader subscribes to. */ private List topics; diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/installing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/installing.adoc index e4af64af3f02..9461d903a34a 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/installing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/installing.adoc @@ -355,7 +355,7 @@ See the https://www.freedesktop.org/software/systemd/man/systemd.service.html[se [[deployment.installing.init-d.script-customization.when-running.conf-file]] -====== Using a Conf Gile +====== Using a Conf File With the exception of `JARFILE` and `APP_NAME`, the settings listed in the preceding section can be configured by using a `.conf` file. The file is expected to be next to the jar file and have the same name but suffixed with `.conf` rather than `.jar`. For example, a jar named `/var/myapp/myapp.jar` uses the configuration file named `/var/myapp/myapp.conf`, as shown in the following example: diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/aot.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/aot.adoc index 96dca8bf6df1..e5f108d917f4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/aot.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/aot.adoc @@ -36,7 +36,7 @@ The `nativeCompile` task of the GraalVM Native Image plugin is automatically con [[aot.processing-tests]] == Processing Tests The AOT engine can be applied to JUnit 5 tests that use Spring's Test Context Framework. -Suitable tests are processed by the `processTestAot` task to generate `ApplicationContextInitialzer` code. +Suitable tests are processed by the `processTestAot` task to generate `ApplicationContextInitializer` code. As with application AOT processing, the `BeanFactory` is fully prepared at build-time. As with `processAot`, the `processTestAot` task is `JavaExec` subclass and can be configured as needed to influence this processing. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc index 31d1f2c1b32b..8cec77b48924 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/reacting.adoc @@ -18,7 +18,7 @@ When Gradle's {java-plugin}[`java` plugin] is applied to a project, the Spring B 6. Creates a {boot-run-javadoc}['BootRun`] task named `bootTestRun` that can be used to run your application using the `test` source set to find its main method and provide its runtime classpath. 7. Creates a configuration named `bootArchives` that contains the artifact produced by the `bootJar` task. 8. Creates a configuration named `developmentOnly` for dependencies that are only required at development time, such as Spring Boot's Devtools, and should not be packaged in executable jars and wars. -9. Creats a configuration named `testAndDevelopmentOnly` for dependencies that are only required at development time and when writing and running tests and that should not be packaged in executable jars and wars. +9. Creates a configuration named `testAndDevelopmentOnly` for dependencies that are only required at development time and when writing and running tests and that should not be packaged in executable jars and wars. 10. Creates a configuration named `productionRuntimeClasspath`. It is equivalent to `runtimeClasspath` minus any dependencies that only appear in the `developmentOnly` or `testDevelopmentOnly` configurations. 11. Configures any `JavaCompile` tasks with no configured encoding to use `UTF-8`. 12. Configures any `JavaCompile` tasks to use the `-parameters` compiler argument. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystem.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystem.java index 1b4e94871132..31dd3905f1dd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystem.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystem.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -195,7 +195,7 @@ public UserPrincipalLookupService getUserPrincipalLookupService() { @Override public WatchService newWatchService() throws IOException { - throw new UnsupportedOperationException("Nested paths do not support the WacherService"); + throw new UnsupportedOperationException("Nested paths do not support the WatchService"); } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java index 4e7298ffb4b9..cdc99d50117a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipContent.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -657,7 +657,7 @@ private static long getSizeOfCentralDirectoryAndEndRecords(ZipEndOfCentralDirect private static ZipContent loadNestedDirectory(Source source, ZipContent zip, Entry directoryEntry) throws IOException { - debug.log("Loading nested directry entry '%s' from '%s'", source.nestedEntryName(), source.path()); + debug.log("Loading nested directory entry '%s' from '%s'", source.nestedEntryName(), source.path()); if (!source.nestedEntryName().endsWith("/")) { throw new IllegalArgumentException("Nested entry name must end with '/'"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemTests.java index 8eb79c743f43..6f67adf0748a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemTests.java @@ -167,7 +167,7 @@ void getUserPrincipalLookupServiceThrowsException() { void newWatchServiceThrowsException() { assertThatExceptionOfType(UnsupportedOperationException.class) .isThrownBy(() -> this.fileSystem.newWatchService()) - .withMessage("Nested paths do not support the WacherService"); + .withMessage("Nested paths do not support the WatchService"); } @Test From dc8b55c0efb10650a0a82747bed322522b625d2f Mon Sep 17 00:00:00 2001 From: skcskitano <150210799+skcskitano@users.noreply.github.com> Date: Mon, 25 Dec 2023 09:55:33 +0900 Subject: [PATCH 1009/1215] Fix connection leak in SqlDialectLookup See gh-38924 --- .../boot/autoconfigure/jooq/SqlDialectLookup.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookup.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookup.java index 4213e39c50bb..bfd53a5dacf9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookup.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookup.java @@ -46,8 +46,7 @@ private SqlDialectLookup() { * @return the most suitable {@link SQLDialect} */ static SQLDialect getDialect(DataSource dataSource) { - try { - Connection connection = (dataSource != null) ? dataSource.getConnection() : null; + try (Connection connection = (dataSource != null) ? dataSource.getConnection() : null) { return JDBCUtils.dialect(connection); } catch (SQLException ex) { From 7113c10b0889d17770d8b20cc88cb81e73f30d63 Mon Sep 17 00:00:00 2001 From: Onur Kagan Ozcan Date: Sun, 7 Jan 2024 16:38:41 +0300 Subject: [PATCH 1010/1215] Fix Jetty ConnectionLimit configuration See gh-39052 --- .../jetty/JettyServletWebServerFactory.java | 3 ++- .../jetty/JettyServletWebServerFactoryTests.java | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java index 89d0159d52ae..6644d775fc4e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java @@ -116,6 +116,7 @@ * @author Venil Noronha * @author Henri Kerola * @author Moritz Halbritter + * @author Onur Kagan Ozcan * @since 2.0.0 * @see #setPort(int) * @see #setConfigurations(Collection) @@ -183,7 +184,7 @@ public WebServer getWebServer(ServletContextInitializer... initializers) { server.setHandler(addHandlerWrappers(context)); this.logger.info("Server initialized with port: " + port); if (this.maxConnections > -1) { - server.addBean(new ConnectionLimit(this.maxConnections, server)); + server.addBean(new ConnectionLimit(this.maxConnections, server.getConnectors())); } if (Ssl.isEnabled(getSsl())) { customizeSsl(server, address); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java index dbb73701e06c..62f04c0d7bbd 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java @@ -72,6 +72,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.InstanceOfAssertFactories.LIST; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.inOrder; @@ -85,6 +86,7 @@ * @author Andy Wilkinson * @author Henri Kerola * @author Moritz Halbritter + * @author Onur Kagan Ozcan */ class JettyServletWebServerFactoryTests extends AbstractServletWebServerFactoryTests { @@ -541,6 +543,17 @@ void shouldApplyMaxConnections() { assertThat(connectionLimit.getMaxConnections()).isOne(); } + @Test + void shouldApplyingMaxConnectionUseConnector() throws Exception { + JettyServletWebServerFactory factory = getFactory(); + factory.setMaxConnections(1); + this.webServer = factory.getWebServer(); + Server server = ((JettyWebServer) this.webServer).getServer(); + assertThat(server.getConnectors()).isEmpty(); + ConnectionLimit connectionLimit = server.getBean(ConnectionLimit.class); + assertThat(connectionLimit).extracting("_connectors", LIST).hasSize(1); + } + @Override protected String startedLogMessage() { return ((JettyWebServer) this.webServer).getStartedLogMessage(); From 66dc72da4638cc7feb05da35b1b12edccfbf58ef Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 10 Jan 2024 10:17:38 +0100 Subject: [PATCH 1011/1215] Polish "Fix Jetty ConnectionLimit configuration" See gh-39052 --- .../embedded/jetty/JettyServletWebServerFactoryTests.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java index 62f04c0d7bbd..07f030593476 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java @@ -39,6 +39,7 @@ import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpResponse; import org.apache.jasper.servlet.JspServlet; +import org.assertj.core.api.InstanceOfAssertFactories; import org.awaitility.Awaitility; import org.eclipse.jetty.ee10.servlet.ErrorPageErrorHandler; import org.eclipse.jetty.ee10.servlet.ServletHolder; @@ -46,6 +47,7 @@ import org.eclipse.jetty.ee10.webapp.ClassMatcher; import org.eclipse.jetty.ee10.webapp.Configuration; import org.eclipse.jetty.ee10.webapp.WebAppContext; +import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.ConnectionLimit; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Handler; @@ -72,7 +74,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.InstanceOfAssertFactories.LIST; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.inOrder; @@ -551,7 +552,9 @@ void shouldApplyingMaxConnectionUseConnector() throws Exception { Server server = ((JettyWebServer) this.webServer).getServer(); assertThat(server.getConnectors()).isEmpty(); ConnectionLimit connectionLimit = server.getBean(ConnectionLimit.class); - assertThat(connectionLimit).extracting("_connectors", LIST).hasSize(1); + assertThat(connectionLimit).extracting("_connectors") + .asInstanceOf(InstanceOfAssertFactories.list(AbstractConnector.class)) + .hasSize(1); } @Override From 0e53c0098f826f62a8783e558d494fac3909e6d9 Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Thu, 4 Jan 2024 10:36:27 +0800 Subject: [PATCH 1012/1215] Add configuration property "spring.task.execution.pool.shutdown.accept-tasks-after-context-close" ExecutorConfigurationSupport::setAcceptTasksAfterContextClose is introduced since Spring Framework 6.1 See gh-38968 --- .../task/TaskExecutionProperties.java | 27 ++++++- .../task/TaskExecutorConfigurations.java | 4 +- .../TaskExecutionAutoConfigurationTests.java | 18 ++--- .../task/ThreadPoolTaskExecutorBuilder.java | 72 ++++++++++++------- .../ThreadPoolTaskExecutorBuilderTests.java | 9 ++- 5 files changed, 93 insertions(+), 37 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java index 9530f198289a..2781e99a0c6f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -25,6 +25,7 @@ * * @author Stephane Nicoll * @author Filip Hrisafov + * @author Yanming Zhou * @since 2.1.0 */ @ConfigurationProperties("spring.task.execution") @@ -110,6 +111,8 @@ public static class Pool { */ private Duration keepAlive = Duration.ofSeconds(60); + private final Shutdown shutdown = new Shutdown(); + public int getQueueCapacity() { return this.queueCapacity; } @@ -150,6 +153,28 @@ public void setKeepAlive(Duration keepAlive) { this.keepAlive = keepAlive; } + public Shutdown getShutdown() { + return this.shutdown; + } + + public static class Shutdown { + + /** + * Whether to accept further tasks after the application context close phase + * has begun. + */ + private boolean acceptTasksAfterContextClose; + + public boolean isAcceptTasksAfterContextClose() { + return this.acceptTasksAfterContextClose; + } + + public void setAcceptTasksAfterContextClose(boolean acceptTasksAfterContextClose) { + this.acceptTasksAfterContextClose = acceptTasksAfterContextClose; + } + + } + } public static class Shutdown { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java index 805d5b336ce5..9e46e1063915 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -43,6 +43,7 @@ * * @author Andy Wilkinson * @author Moritz Halbritter + * @author Yanming Zhou */ class TaskExecutorConfigurations { @@ -119,6 +120,7 @@ ThreadPoolTaskExecutorBuilder threadPoolTaskExecutorBuilder(TaskExecutionPropert builder = builder.maxPoolSize(pool.getMaxSize()); builder = builder.allowCoreThreadTimeOut(pool.isAllowCoreThreadTimeout()); builder = builder.keepAlive(pool.getKeepAlive()); + builder = builder.acceptTasksAfterContextClose(pool.getShutdown().isAcceptTasksAfterContextClose()); TaskExecutionProperties.Shutdown shutdown = properties.getShutdown(); builder = builder.awaitTermination(shutdown.isAwaitTermination()); builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod()); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java index 88df814860ce..2ba81d5afeaa 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -60,6 +60,7 @@ * @author Stephane Nicoll * @author Camille Vienot * @author Moritz Halbritter + * @author Yanming Zhou */ @ExtendWith(OutputCaptureExtension.class) @SuppressWarnings("removal") @@ -124,19 +125,20 @@ void simpleAsyncTaskExecutorBuilderShouldReadProperties() { @Test void threadPoolTaskExecutorBuilderShouldApplyCustomSettings() { - this.contextRunner - .withPropertyValues("spring.task.execution.pool.queue-capacity=10", - "spring.task.execution.pool.core-size=2", "spring.task.execution.pool.max-size=4", - "spring.task.execution.pool.allow-core-thread-timeout=true", - "spring.task.execution.pool.keep-alive=5s", "spring.task.execution.shutdown.await-termination=true", - "spring.task.execution.shutdown.await-termination-period=30s", - "spring.task.execution.thread-name-prefix=mytest-") + this.contextRunner.withPropertyValues("spring.task.execution.pool.queue-capacity=10", + "spring.task.execution.pool.core-size=2", "spring.task.execution.pool.max-size=4", + "spring.task.execution.pool.allow-core-thread-timeout=true", "spring.task.execution.pool.keep-alive=5s", + "spring.task.execution.pool.shutdown.accept-tasks-after-context-close=true", + "spring.task.execution.shutdown.await-termination=true", + "spring.task.execution.shutdown.await-termination-period=30s", + "spring.task.execution.thread-name-prefix=mytest-") .run(assertThreadPoolTaskExecutor((taskExecutor) -> { assertThat(taskExecutor).hasFieldOrPropertyWithValue("queueCapacity", 10); assertThat(taskExecutor.getCorePoolSize()).isEqualTo(2); assertThat(taskExecutor.getMaxPoolSize()).isEqualTo(4); assertThat(taskExecutor).hasFieldOrPropertyWithValue("allowCoreThreadTimeOut", true); assertThat(taskExecutor.getKeepAliveSeconds()).isEqualTo(5); + assertThat(taskExecutor).hasFieldOrPropertyWithValue("acceptTasksAfterContextClose", true); assertThat(taskExecutor).hasFieldOrPropertyWithValue("waitForTasksToCompleteOnShutdown", true); assertThat(taskExecutor).hasFieldOrPropertyWithValue("awaitTerminationMillis", 30000L); assertThat(taskExecutor.getThreadNamePrefix()).isEqualTo("mytest-"); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilder.java index 2609245832ad..ee993a1ef3a9 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -40,6 +40,7 @@ * * @author Stephane Nicoll * @author Filip Hrisafov + * @author Yanming Zhou * @since 3.2.0 */ public class ThreadPoolTaskExecutorBuilder { @@ -54,6 +55,8 @@ public class ThreadPoolTaskExecutorBuilder { private final Duration keepAlive; + private final Boolean acceptTasksAfterContextClose; + private final Boolean awaitTermination; private final Duration awaitTerminationPeriod; @@ -70,6 +73,7 @@ public ThreadPoolTaskExecutorBuilder() { this.maxPoolSize = null; this.allowCoreThreadTimeOut = null; this.keepAlive = null; + this.acceptTasksAfterContextClose = null; this.awaitTermination = null; this.awaitTerminationPeriod = null; this.threadNamePrefix = null; @@ -78,14 +82,15 @@ public ThreadPoolTaskExecutorBuilder() { } private ThreadPoolTaskExecutorBuilder(Integer queueCapacity, Integer corePoolSize, Integer maxPoolSize, - Boolean allowCoreThreadTimeOut, Duration keepAlive, Boolean awaitTermination, - Duration awaitTerminationPeriod, String threadNamePrefix, TaskDecorator taskDecorator, - Set customizers) { + Boolean allowCoreThreadTimeOut, Duration keepAlive, Boolean acceptTasksAfterContextClose, + Boolean awaitTermination, Duration awaitTerminationPeriod, String threadNamePrefix, + TaskDecorator taskDecorator, Set customizers) { this.queueCapacity = queueCapacity; this.corePoolSize = corePoolSize; this.maxPoolSize = maxPoolSize; this.allowCoreThreadTimeOut = allowCoreThreadTimeOut; this.keepAlive = keepAlive; + this.acceptTasksAfterContextClose = acceptTasksAfterContextClose; this.awaitTermination = awaitTermination; this.awaitTerminationPeriod = awaitTerminationPeriod; this.threadNamePrefix = threadNamePrefix; @@ -101,8 +106,8 @@ private ThreadPoolTaskExecutorBuilder(Integer queueCapacity, Integer corePoolSiz */ public ThreadPoolTaskExecutorBuilder queueCapacity(int queueCapacity) { return new ThreadPoolTaskExecutorBuilder(queueCapacity, this.corePoolSize, this.maxPoolSize, - this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod, - this.threadNamePrefix, this.taskDecorator, this.customizers); + this.allowCoreThreadTimeOut, this.keepAlive, this.acceptTasksAfterContextClose, this.awaitTermination, + this.awaitTerminationPeriod, this.threadNamePrefix, this.taskDecorator, this.customizers); } /** @@ -116,8 +121,8 @@ public ThreadPoolTaskExecutorBuilder queueCapacity(int queueCapacity) { */ public ThreadPoolTaskExecutorBuilder corePoolSize(int corePoolSize) { return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, corePoolSize, this.maxPoolSize, - this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod, - this.threadNamePrefix, this.taskDecorator, this.customizers); + this.allowCoreThreadTimeOut, this.keepAlive, this.acceptTasksAfterContextClose, this.awaitTermination, + this.awaitTerminationPeriod, this.threadNamePrefix, this.taskDecorator, this.customizers); } /** @@ -131,8 +136,8 @@ public ThreadPoolTaskExecutorBuilder corePoolSize(int corePoolSize) { */ public ThreadPoolTaskExecutorBuilder maxPoolSize(int maxPoolSize) { return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, maxPoolSize, - this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod, - this.threadNamePrefix, this.taskDecorator, this.customizers); + this.allowCoreThreadTimeOut, this.keepAlive, this.acceptTasksAfterContextClose, this.awaitTermination, + this.awaitTerminationPeriod, this.threadNamePrefix, this.taskDecorator, this.customizers); } /** @@ -143,8 +148,8 @@ public ThreadPoolTaskExecutorBuilder maxPoolSize(int maxPoolSize) { */ public ThreadPoolTaskExecutorBuilder allowCoreThreadTimeOut(boolean allowCoreThreadTimeOut) { return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize, - allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod, - this.threadNamePrefix, this.taskDecorator, this.customizers); + allowCoreThreadTimeOut, this.keepAlive, this.acceptTasksAfterContextClose, this.awaitTermination, + this.awaitTerminationPeriod, this.threadNamePrefix, this.taskDecorator, this.customizers); } /** @@ -154,8 +159,21 @@ public ThreadPoolTaskExecutorBuilder allowCoreThreadTimeOut(boolean allowCoreThr */ public ThreadPoolTaskExecutorBuilder keepAlive(Duration keepAlive) { return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize, - this.allowCoreThreadTimeOut, keepAlive, this.awaitTermination, this.awaitTerminationPeriod, - this.threadNamePrefix, this.taskDecorator, this.customizers); + this.allowCoreThreadTimeOut, keepAlive, this.acceptTasksAfterContextClose, this.awaitTermination, + this.awaitTerminationPeriod, this.threadNamePrefix, this.taskDecorator, this.customizers); + } + + /** + * Set whether to accept further tasks after the application context close phase has + * begun. + * @param acceptTasksAfterContextClose to accept further tasks after the application + * context close phase has begun + * @return a new builder instance + */ + public ThreadPoolTaskExecutorBuilder acceptTasksAfterContextClose(boolean acceptTasksAfterContextClose) { + return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize, + this.allowCoreThreadTimeOut, this.keepAlive, acceptTasksAfterContextClose, this.awaitTermination, + this.awaitTerminationPeriod, this.threadNamePrefix, this.taskDecorator, this.customizers); } /** @@ -168,8 +186,8 @@ public ThreadPoolTaskExecutorBuilder keepAlive(Duration keepAlive) { */ public ThreadPoolTaskExecutorBuilder awaitTermination(boolean awaitTermination) { return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize, - this.allowCoreThreadTimeOut, this.keepAlive, awaitTermination, this.awaitTerminationPeriod, - this.threadNamePrefix, this.taskDecorator, this.customizers); + this.allowCoreThreadTimeOut, this.keepAlive, this.acceptTasksAfterContextClose, awaitTermination, + this.awaitTerminationPeriod, this.threadNamePrefix, this.taskDecorator, this.customizers); } /** @@ -183,8 +201,8 @@ public ThreadPoolTaskExecutorBuilder awaitTermination(boolean awaitTermination) */ public ThreadPoolTaskExecutorBuilder awaitTerminationPeriod(Duration awaitTerminationPeriod) { return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize, - this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, awaitTerminationPeriod, - this.threadNamePrefix, this.taskDecorator, this.customizers); + this.allowCoreThreadTimeOut, this.keepAlive, this.acceptTasksAfterContextClose, this.awaitTermination, + awaitTerminationPeriod, this.threadNamePrefix, this.taskDecorator, this.customizers); } /** @@ -194,8 +212,8 @@ public ThreadPoolTaskExecutorBuilder awaitTerminationPeriod(Duration awaitTermin */ public ThreadPoolTaskExecutorBuilder threadNamePrefix(String threadNamePrefix) { return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize, - this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod, - threadNamePrefix, this.taskDecorator, this.customizers); + this.allowCoreThreadTimeOut, this.keepAlive, this.acceptTasksAfterContextClose, this.awaitTermination, + this.awaitTerminationPeriod, threadNamePrefix, this.taskDecorator, this.customizers); } /** @@ -205,8 +223,8 @@ public ThreadPoolTaskExecutorBuilder threadNamePrefix(String threadNamePrefix) { */ public ThreadPoolTaskExecutorBuilder taskDecorator(TaskDecorator taskDecorator) { return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize, - this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod, - this.threadNamePrefix, taskDecorator, this.customizers); + this.allowCoreThreadTimeOut, this.keepAlive, this.acceptTasksAfterContextClose, this.awaitTermination, + this.awaitTerminationPeriod, this.threadNamePrefix, taskDecorator, this.customizers); } /** @@ -235,8 +253,8 @@ public ThreadPoolTaskExecutorBuilder customizers(ThreadPoolTaskExecutorCustomize public ThreadPoolTaskExecutorBuilder customizers(Iterable customizers) { Assert.notNull(customizers, "Customizers must not be null"); return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize, - this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod, - this.threadNamePrefix, this.taskDecorator, append(null, customizers)); + this.allowCoreThreadTimeOut, this.keepAlive, this.acceptTasksAfterContextClose, this.awaitTermination, + this.awaitTerminationPeriod, this.threadNamePrefix, this.taskDecorator, append(null, customizers)); } /** @@ -264,8 +282,9 @@ public ThreadPoolTaskExecutorBuilder additionalCustomizers( Iterable customizers) { Assert.notNull(customizers, "Customizers must not be null"); return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize, - this.allowCoreThreadTimeOut, this.keepAlive, this.awaitTermination, this.awaitTerminationPeriod, - this.threadNamePrefix, this.taskDecorator, append(this.customizers, customizers)); + this.allowCoreThreadTimeOut, this.keepAlive, this.acceptTasksAfterContextClose, this.awaitTermination, + this.awaitTerminationPeriod, this.threadNamePrefix, this.taskDecorator, + append(this.customizers, customizers)); } /** @@ -307,6 +326,7 @@ public T configure(T taskExecutor) { map.from(this.maxPoolSize).to(taskExecutor::setMaxPoolSize); map.from(this.keepAlive).asInt(Duration::getSeconds).to(taskExecutor::setKeepAliveSeconds); map.from(this.allowCoreThreadTimeOut).to(taskExecutor::setAllowCoreThreadTimeOut); + map.from(this.acceptTasksAfterContextClose).to(taskExecutor::setAcceptTasksAfterContextClose); map.from(this.awaitTermination).to(taskExecutor::setWaitForTasksToCompleteOnShutdown); map.from(this.awaitTerminationPeriod).as(Duration::toMillis).to(taskExecutor::setAwaitTerminationMillis); map.from(this.threadNamePrefix).whenHasText().to(taskExecutor::setThreadNamePrefix); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilderTests.java index b57ffc6905a9..8b8dc650e680 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -36,6 +36,7 @@ * * @author Stephane Nicoll * @author Filip Hrisafov + * @author Yanming Zhou */ class ThreadPoolTaskExecutorBuilderTests { @@ -56,6 +57,12 @@ void poolSettingsShouldApply() { assertThat(executor.getKeepAliveSeconds()).isEqualTo(60); } + @Test + void acceptTasksAfterContextCloseShouldApply() { + ThreadPoolTaskExecutor executor = this.builder.acceptTasksAfterContextClose(true).build(); + assertThat(executor).hasFieldOrPropertyWithValue("acceptTasksAfterContextClose", true); + } + @Test void awaitTerminationShouldApply() { ThreadPoolTaskExecutor executor = this.builder.awaitTermination(true).build(); From 1c411c2fc39ec4959dc3e810c0bf8704ee80c0fb Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 10 Jan 2024 10:38:28 +0100 Subject: [PATCH 1013/1215] Polish "Add configuration property "spring.task.execution.pool.shutdown.accept-tasks-after-context-close"" See gh-38968 --- .../boot/task/ThreadPoolTaskExecutorBuilder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilder.java index ee993a1ef3a9..3ed8506474ff 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilder.java @@ -166,8 +166,8 @@ public ThreadPoolTaskExecutorBuilder keepAlive(Duration keepAlive) { /** * Set whether to accept further tasks after the application context close phase has * begun. - * @param acceptTasksAfterContextClose to accept further tasks after the application - * context close phase has begun + * @param acceptTasksAfterContextClose whether to accept further tasks after the + * application context close phase has begun * @return a new builder instance */ public ThreadPoolTaskExecutorBuilder acceptTasksAfterContextClose(boolean acceptTasksAfterContextClose) { From d2a3c8703aa9cd4c628c55163bd1a393324d6a23 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 10 Jan 2024 09:58:23 +0000 Subject: [PATCH 1014/1215] Add missing since javadoc to acceptTasksAfterContextClose See gh-38968 --- .../springframework/boot/task/ThreadPoolTaskExecutorBuilder.java | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilder.java index 3ed8506474ff..fb90c8ad5898 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/task/ThreadPoolTaskExecutorBuilder.java @@ -169,6 +169,7 @@ public ThreadPoolTaskExecutorBuilder keepAlive(Duration keepAlive) { * @param acceptTasksAfterContextClose whether to accept further tasks after the * application context close phase has begun * @return a new builder instance + * @since 3.3.0 */ public ThreadPoolTaskExecutorBuilder acceptTasksAfterContextClose(boolean acceptTasksAfterContextClose) { return new ThreadPoolTaskExecutorBuilder(this.queueCapacity, this.corePoolSize, this.maxPoolSize, From 49c6bacd44ebd075f5fb54d2828510c6ba6acb44 Mon Sep 17 00:00:00 2001 From: John Niang Date: Sun, 10 Dec 2023 00:19:26 +0800 Subject: [PATCH 1015/1215] Support configuring maximum number of sessions for reactive server Signed-off-by: John Niang See gh-38703 --- .../autoconfigure/web/ServerProperties.java | 13 +++++++++ .../reactive/WebFluxAutoConfiguration.java | 5 +++- .../WebFluxAutoConfigurationTests.java | 27 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index ca10a7335352..3e6d21c95c38 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -329,6 +329,11 @@ public static class Session { @DurationUnit(ChronoUnit.SECONDS) private Duration timeout = Duration.ofMinutes(30); + /** + * The maximum number of sessions that can be stored. + */ + private int maxSessions = 10_000; + @NestedConfigurationProperty private final Cookie cookie = new Cookie(); @@ -340,6 +345,14 @@ public void setTimeout(Duration timeout) { this.timeout = timeout; } + public int getMaxSessions() { + return this.maxSessions; + } + + public void setMaxSessions(int maxSessions) { + this.maxSessions = maxSessions; + } + public Cookie getCookie() { return this.cookie; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java index e9ebef362499..09f3f9237a70 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java @@ -332,7 +332,10 @@ public LocaleContextResolver localeContextResolver() { public WebSessionManager webSessionManager(ObjectProvider webSessionIdResolver) { DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager(); Duration timeout = this.serverProperties.getReactive().getSession().getTimeout(); - webSessionManager.setSessionStore(new MaxIdleTimeInMemoryWebSessionStore(timeout)); + int maxSessions = this.serverProperties.getReactive().getSession().getMaxSessions(); + MaxIdleTimeInMemoryWebSessionStore sessionStore = new MaxIdleTimeInMemoryWebSessionStore(timeout); + sessionStore.setMaxSessions(maxSessions); + webSessionManager.setSessionStore(sessionStore); webSessionIdResolver.ifAvailable(webSessionManager::setSessionIdResolver); return webSessionManager; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java index 2340013df8ea..276944fe0de7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java @@ -109,8 +109,11 @@ import org.springframework.web.server.i18n.FixedLocaleContextResolver; import org.springframework.web.server.i18n.LocaleContextResolver; import org.springframework.web.server.session.CookieWebSessionIdResolver; +import org.springframework.web.server.session.DefaultWebSessionManager; +import org.springframework.web.server.session.InMemoryWebSessionStore; import org.springframework.web.server.session.WebSessionIdResolver; import org.springframework.web.server.session.WebSessionManager; +import org.springframework.web.server.session.WebSessionStore; import org.springframework.web.util.pattern.PathPattern; import static org.assertj.core.api.Assertions.assertThat; @@ -622,6 +625,20 @@ void customSessionTimeoutConfigurationShouldBeApplied() { }))); } + @Test + void customSessionMaxSessionsConfigurationShouldBeApplied() { + this.contextRunner.withPropertyValues("server.reactive.session.max-sessions:123") + .run((context) -> assertMaxSessionsWithWebSession(123)); + } + + @Test + void defaultSessionMaxSessionsConfigurationShouldBeInSync() { + this.contextRunner.run((context) -> { + int defaultMaxSessions = new InMemoryWebSessionStore().getMaxSessions(); + assertMaxSessionsWithWebSession(defaultMaxSessions); + }); + } + @Test void customSessionCookieConfigurationShouldBeApplied() { this.contextRunner.withPropertyValues("server.reactive.session.cookie.name:JSESSIONID", @@ -753,6 +770,16 @@ private ContextConsumer assertSessionTimeoutWithW }; } + private ContextConsumer assertMaxSessionsWithWebSession(int maxSessions) { + return (context) -> { + WebSessionManager sessionManager = context.getBean(WebSessionManager.class); + assertThat(sessionManager).isInstanceOf(DefaultWebSessionManager.class); + WebSessionStore sessionStore = ((DefaultWebSessionManager) sessionManager).getSessionStore(); + assertThat(sessionStore).isInstanceOf(InMemoryWebSessionStore.class); + assertThat(((InMemoryWebSessionStore) sessionStore).getMaxSessions()).isEqualTo(maxSessions); + }; + } + private Map getHandlerMap(ApplicationContext context) { HandlerMapping mapping = context.getBean("resourceHandlerMapping", HandlerMapping.class); if (mapping instanceof SimpleUrlHandlerMapping simpleMapping) { From e5b2ad9b8aa0c1dac0498033eea78cac6f816bf6 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 10 Jan 2024 11:08:12 +0100 Subject: [PATCH 1016/1215] Add possibility to configure a custom ExecutionContextSerializer in BatchAutoConfiguration See gh-38328 --- .../batch/BatchAutoConfiguration.java | 15 +++++++- .../batch/BatchAutoConfigurationTests.java | 35 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java index 7f31db1fb7a5..ba4cc5e8adc6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java @@ -24,7 +24,9 @@ import org.springframework.batch.core.configuration.support.DefaultBatchConfiguration; import org.springframework.batch.core.explore.JobExplorer; import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.repository.ExecutionContextSerializer; import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.repository.dao.DefaultExecutionContextSerializer; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.ExitCodeGenerator; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -62,6 +64,7 @@ * @author Eddú Meléndez * @author Kazuki Shimizu * @author Mahmoud Ben Hassine + * @author Lars Uffmann * @since 1.0.0 */ @AutoConfiguration(after = { HibernateJpaAutoConfiguration.class, TransactionAutoConfiguration.class }) @@ -102,13 +105,18 @@ static class SpringBootBatchConfiguration extends DefaultBatchConfiguration { private final List batchConversionServiceCustomizers; + private final ExecutionContextSerializer executionContextSerializer; + SpringBootBatchConfiguration(DataSource dataSource, @BatchDataSource ObjectProvider batchDataSource, PlatformTransactionManager transactionManager, BatchProperties properties, - ObjectProvider batchConversionServiceCustomizers) { + ObjectProvider batchConversionServiceCustomizers, + ObjectProvider executionContextSerializer) { this.dataSource = batchDataSource.getIfAvailable(() -> dataSource); this.transactionManager = transactionManager; this.properties = properties; this.batchConversionServiceCustomizers = batchConversionServiceCustomizers.orderedStream().toList(); + this.executionContextSerializer = executionContextSerializer + .getIfAvailable(DefaultExecutionContextSerializer::new); } @Override @@ -142,6 +150,11 @@ protected ConfigurableConversionService getConversionService() { return conversionService; } + @Override + protected ExecutionContextSerializer getExecutionContextSerializer() { + return this.executionContextSerializer; + } + } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java index 3c2c63d781a0..13da2af2361a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java @@ -43,7 +43,10 @@ import org.springframework.batch.core.job.AbstractJob; import org.springframework.batch.core.launch.JobLauncher; import org.springframework.batch.core.launch.JobOperator; +import org.springframework.batch.core.repository.ExecutionContextSerializer; import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.repository.dao.DefaultExecutionContextSerializer; +import org.springframework.batch.core.repository.dao.Jackson2ExecutionContextStringSerializer; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanPostProcessor; @@ -98,6 +101,7 @@ * @author Vedran Pavic * @author Kazuki Shimizu * @author Mahmoud Ben Hassine + * @author Lars Uffmann */ @ExtendWith(OutputCaptureExtension.class) class BatchAutoConfigurationTests { @@ -462,6 +466,27 @@ void whenTheUserDefinesAJobNameThatDoesNotExistWithRegisteredJobFailsFast() { .withMessage("No job found with name 'three'"); } + @Test + void customExecutionContextSerializerIsUsed() { + this.contextRunner.withUserConfiguration(TestConfiguration.class, EmbeddedDataSourceConfiguration.class) + .withUserConfiguration(CustomExecutionContextConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(Jackson2ExecutionContextStringSerializer.class); + assertThat(context.getBean(SpringBootBatchConfiguration.class).getExecutionContextSerializer()) + .isInstanceOf(Jackson2ExecutionContextStringSerializer.class); + }); + } + + @Test + void defaultExecutionContextSerializerIsUsed() { + this.contextRunner.withUserConfiguration(TestConfiguration.class, EmbeddedDataSourceConfiguration.class) + .run((context) -> { + assertThat(context).doesNotHaveBean(ExecutionContextSerializer.class); + assertThat(context.getBean(SpringBootBatchConfiguration.class).getExecutionContextSerializer()) + .isInstanceOf(DefaultExecutionContextSerializer.class); + }); + } + private JobLauncherApplicationRunner createInstance(String... registeredJobNames) { JobLauncherApplicationRunner runner = new JobLauncherApplicationRunner(mock(JobLauncher.class), mock(JobExplorer.class), mock(JobRepository.class)); @@ -777,4 +802,14 @@ BatchConversionServiceCustomizer anotherBatchConversionServiceCustomizer() { } + @Configuration(proxyBeanMethods = false) + static class CustomExecutionContextConfiguration { + + @Bean + ExecutionContextSerializer executionContextSerializer() { + return new Jackson2ExecutionContextStringSerializer(); + } + + } + } From 470029aff10cd318160ffd37e15d8608e37057d1 Mon Sep 17 00:00:00 2001 From: Lenin Jaganathan Date: Sun, 5 Nov 2023 21:32:14 -0800 Subject: [PATCH 1017/1215] Use unknown_service as default application name for OpenTelemetry See gh-38219 --- .../opentelemetry/OpenTelemetryAutoConfiguration.java | 2 +- .../opentelemetry/OpenTelemetryAutoConfigurationTests.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java index f87bfa6548dd..4c6ec413b3be 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfiguration.java @@ -51,7 +51,7 @@ public class OpenTelemetryAutoConfiguration { /** * Default value for application name if {@code spring.application.name} is not set. */ - private static final String DEFAULT_APPLICATION_NAME = "application"; + private static final String DEFAULT_APPLICATION_NAME = "unknown_service"; private static final AttributeKey ATTRIBUTE_KEY_SERVICE_NAME = AttributeKey.stringKey("service.name"); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java index 167be648bf18..4cd6ef90235b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/opentelemetry/OpenTelemetryAutoConfigurationTests.java @@ -95,7 +95,7 @@ void shouldFallbackToDefaultApplicationNameIfSpringApplicationNameIsNotSet() { this.runner.run((context) -> { Resource resource = context.getBean(Resource.class); assertThat(resource.getAttributes().asMap()) - .contains(entry(ResourceAttributes.SERVICE_NAME, "application")); + .contains(entry(ResourceAttributes.SERVICE_NAME, "unknown_service")); }); } From 3e705e8d56332056d4fb11922c76e3c5ed3f40cc Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 10 Jan 2024 11:36:34 +0100 Subject: [PATCH 1018/1215] Polish "Use unknown_service as default application name for OpenTelemetry" See gh-38219 --- .../metrics/export/otlp/OtlpPropertiesConfigAdapter.java | 2 +- .../metrics/export/otlp/OtlpPropertiesConfigAdapterTests.java | 2 +- .../tracing/OpenTelemetryAutoConfigurationTests.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java index f329d7bf17ae..d3e32e1674fc 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/otlp/OtlpPropertiesConfigAdapter.java @@ -41,7 +41,7 @@ class OtlpPropertiesConfigAdapter extends StepRegistryPropertiesConfigAdapter, Object> expectedAttributes = Resource.getDefault() - .merge(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, "application"))) + .merge(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, "unknown_service"))) .getAttributes() .asMap(); assertThat(spanData.getResource().getAttributes().asMap()).isEqualTo(expectedAttributes); From 4b157ceaf2c98369a806cd8c6804689fd4529c6b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 10 Jan 2024 12:04:33 +0000 Subject: [PATCH 1019/1215] Make web servers' started log messages more consistent Closes gh-36149 --- .../jetty/JettyReactiveWebServerFactory.java | 3 ++- .../web/embedded/jetty/JettyWebServer.java | 10 ++++++-- .../web/embedded/netty/NettyWebServer.java | 5 ++-- .../web/embedded/tomcat/TomcatWebServer.java | 25 ++++++++++++++++--- .../undertow/UndertowServletWebServer.java | 8 +++--- ...AbstractReactiveWebServerFactoryTests.java | 8 +++--- .../AbstractServletWebServerFactoryTests.java | 6 ++--- 7 files changed, 45 insertions(+), 20 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java index 613514927381..aad002cfcd37 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyReactiveWebServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -205,6 +205,7 @@ protected Server createJettyServer(JettyHttpHandlerAdapter servlet) { statisticsHandler.setHandler(server.getHandler()); server.setHandler(statisticsHandler); } + server.setAttribute(org.springframework.boot.web.server.WebServerFactory.class.getName(), getClass()); return server; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java index bbcde54a773c..acbf9f94e450 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyWebServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -35,6 +35,7 @@ import org.springframework.boot.web.server.PortInUseException; import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.server.WebServerException; +import org.springframework.boot.web.server.WebServerFactory; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -179,7 +180,9 @@ public void start() throws WebServerException { } String getStartedLogMessage() { - return "Jetty started on " + getActualPortsDescription() + " with context path '" + getContextPath() + "'"; + String contextPath = getContextPath(); + return "Jetty started on " + getActualPortsDescription() + + ((contextPath != null) ? " with context path '" + contextPath + "'" : ""); } private String getActualPortsDescription() { @@ -205,6 +208,9 @@ private String getProtocols(Connector connector) { } private String getContextPath() { + if (JettyReactiveWebServerFactory.class.equals(this.server.getAttribute(WebServerFactory.class.getName()))) { + return null; + } return this.server.getHandlers() .stream() .map(this::findContextHandler) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java index a9e6e6c2c953..12948e48ac32 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyWebServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -146,7 +146,8 @@ public void start() throws WebServerException { private String getStartedOnMessage(DisposableServer server) { StringBuilder message = new StringBuilder(); - tryAppend(message, "port %s", server::port); + tryAppend(message, "port %s", () -> server.port() + + ((this.httpServer.configuration().sslProvider() != null) ? " (https)" : " (http)")); tryAppend(message, "path %s", server::path); return (!message.isEmpty()) ? "Netty started on " + message : "Netty started"; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java index 6aa6a8cefbed..baeb5cc4a9e7 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatWebServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -32,6 +32,7 @@ import org.apache.catalina.LifecycleException; import org.apache.catalina.LifecycleState; import org.apache.catalina.Service; +import org.apache.catalina.Wrapper; import org.apache.catalina.connector.Connector; import org.apache.catalina.startup.Tomcat; import org.apache.commons.logging.Log; @@ -45,6 +46,7 @@ import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.server.WebServerException; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * {@link WebServer} that can be used to control a Tomcat web server. Usually this class @@ -256,7 +258,9 @@ public void start() throws WebServerException { } String getStartedLogMessage() { - return "Tomcat started on " + getPortsDescription(true) + " with context path '" + getContextPath() + "'"; + String contextPath = getContextPath(); + return "Tomcat started on " + getPortsDescription(true) + + ((contextPath != null) ? " with context path '" + contextPath + "'" : ""); } private void checkThatConnectorsHaveStarted() { @@ -407,11 +411,26 @@ public int getPort() { } private String getContextPath() { - return Arrays.stream(this.tomcat.getHost().findChildren()) + String contextPath = Arrays.stream(this.tomcat.getHost().findChildren()) .filter(TomcatEmbeddedContext.class::isInstance) .map(TomcatEmbeddedContext.class::cast) + .filter(this::imperative) .map(TomcatEmbeddedContext::getPath) + .map((path) -> path.equals("") ? "/" : path) .collect(Collectors.joining(" ")); + return StringUtils.hasText(contextPath) ? contextPath : null; + } + + private boolean imperative(TomcatEmbeddedContext context) { + for (Container container : context.findChildren()) { + if (container instanceof Wrapper wrapper) { + if (wrapper.getServletClass() + .equals("org.springframework.http.server.reactive.TomcatHttpHandlerAdapter")) { + return false; + } + } + } + return true; } /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServer.java index 3a0a644d2459..56ffd9416aed 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -78,12 +78,10 @@ protected HttpHandler createHttpHandler() { @Override protected String getStartLogMessage() { - if (!StringUtils.hasText(this.contextPath)) { - return super.getStartLogMessage(); - } + String contextPath = StringUtils.hasText(this.contextPath) ? this.contextPath : "/"; StringBuilder message = new StringBuilder(super.getStartLogMessage()); message.append(" with context path '"); - message.append(this.contextPath); + message.append(contextPath); message.append("'"); return message.toString(); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java index f0e29376d781..116e21e01f71 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/server/AbstractReactiveWebServerFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -607,8 +607,8 @@ void startedLogMessageWithSinglePort() { AbstractReactiveWebServerFactory factory = getFactory(); this.webServer = factory.getWebServer(new EchoHandler()); this.webServer.start(); - assertThat(startedLogMessage()).matches("(Jetty|Netty|Tomcat|Undertow) started on port " - + this.webServer.getPort() + "( \\(http(/1.1)?\\))?( with context path '(/)?')?"); + assertThat(startedLogMessage()).matches( + "(Jetty|Netty|Tomcat|Undertow) started on port " + this.webServer.getPort() + " \\(http(/1.1)?\\)"); } @Test @@ -618,7 +618,7 @@ protected void startedLogMessageWithMultiplePorts() { this.webServer = factory.getWebServer(new EchoHandler()); this.webServer.start(); assertThat(startedLogMessage()).matches("(Jetty|Tomcat|Undertow) started on ports " + this.webServer.getPort() - + "( \\(http(/1.1)?\\))?, [0-9]+( \\(http(/1.1)?\\))?( with context path '(/)?')?"); + + " \\(http(/1.1)?\\), [0-9]+ \\(http(/1.1)?\\)"); } protected WebClient prepareCompressionTest() { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java index 46798f2720c4..2189e6aa39bb 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/AbstractServletWebServerFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -1308,7 +1308,7 @@ void startedLogMessageWithSinglePort() { this.webServer = factory.getWebServer(); this.webServer.start(); assertThat(startedLogMessage()).matches("(Jetty|Tomcat|Undertow) started on port " + this.webServer.getPort() - + " \\(http(/1.1)?\\)( with context path '(/)?')?"); + + " \\(http(/1.1)?\\) with context path '/'"); } @Test @@ -1328,7 +1328,7 @@ void startedLogMessageWithMultiplePorts() { this.webServer = factory.getWebServer(); this.webServer.start(); assertThat(startedLogMessage()).matches("(Jetty|Tomcat|Undertow) started on ports " + this.webServer.getPort() - + " \\(http(/1.1)?\\), [0-9]+ \\(http(/1.1)?\\)( with context path '(/)?')?"); + + " \\(http(/1.1)?\\), [0-9]+ \\(http(/1.1)?\\) with context path '/'"); } protected Future initiateGetRequest(int port, String path) { From c3e337233640efbec027016c2e35ab23a95241f6 Mon Sep 17 00:00:00 2001 From: Swamy Mavuri Date: Sun, 26 Nov 2023 01:58:39 +0530 Subject: [PATCH 1020/1215] Add support for Pulsar cluster-level failover See gh-38559 --- .../pulsar/PulsarProperties.java | 111 ++++++++++++++++++ .../pulsar/PulsarPropertiesMapper.java | 48 +++++++- .../pulsar/MockAuthentication.java | 63 ++++++++++ .../pulsar/PulsarConfigurationTests.java | 44 +++++++ .../pulsar/PulsarPropertiesMapperTests.java | 34 ++++++ .../pulsar/PulsarPropertiesTests.java | 34 ++++++ 6 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/MockAuthentication.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java index bd87c9f2f6e3..1f9c7983de73 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java @@ -19,10 +19,12 @@ import java.time.Duration; import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.regex.Pattern; +import org.apache.pulsar.client.api.AutoClusterFailoverBuilder.FailoverPolicy; import org.apache.pulsar.client.api.CompressionType; import org.apache.pulsar.client.api.HashingScheme; import org.apache.pulsar.client.api.MessageRoutingMode; @@ -42,6 +44,7 @@ * * @author Chris Bono * @author Phillip Webb + * @author Swamy Mavuri * @since 3.2.0 */ @ConfigurationProperties("spring.pulsar") @@ -128,6 +131,11 @@ public static class Client { */ private final Authentication authentication = new Authentication(); + /** + * Failover settings. + */ + private final Failover failover = new Failover(); + public String getServiceUrl() { return this.serviceUrl; } @@ -164,6 +172,10 @@ public Authentication getAuthentication() { return this.authentication; } + public Failover getFailover() { + return this.failover; + } + } public static class Admin { @@ -887,4 +899,103 @@ public void setParam(Map param) { } + public static class Failover { + + /** + * Cluster Failover Policy. + */ + private FailoverPolicy failoverPolicy = FailoverPolicy.ORDER; + + /** + * Delay before the Pulsar client switches from the primary cluster to the backup + * cluster. + */ + private Duration failOverDelay; + + /** + * Delay before the Pulsar client switches from the backup cluster to the primary + * cluster. + */ + private Duration switchBackDelay; + + /** + * Frequency of performing a probe task. + */ + private Duration checkInterval; + + /** + * List of backupClusters The backup cluster is chosen in the sequence of the + * given list. If all backup clusters are available, the Pulsar client chooses the + * first backup cluster. + */ + private List backupClusters = new LinkedList<>(); + + public FailoverPolicy getFailoverPolicy() { + return this.failoverPolicy; + } + + public void setFailoverPolicy(FailoverPolicy failoverPolicy) { + this.failoverPolicy = failoverPolicy; + } + + public Duration getFailOverDelay() { + return this.failOverDelay; + } + + public void setFailOverDelay(Duration failOverDelay) { + this.failOverDelay = failOverDelay; + } + + public Duration getSwitchBackDelay() { + return this.switchBackDelay; + } + + public void setSwitchBackDelay(Duration switchBackDelay) { + this.switchBackDelay = switchBackDelay; + } + + public Duration getCheckInterval() { + return this.checkInterval; + } + + public void setCheckInterval(Duration checkInterval) { + this.checkInterval = checkInterval; + } + + public List getBackupClusters() { + return this.backupClusters; + } + + public void setBackupClusters(List backupClusters) { + this.backupClusters = backupClusters; + } + + public static class BackupCluster { + + /** + * Pulsar service URL in the format '(pulsar|pulsar+ssl)://host:port'. + */ + private String serviceUrl = "pulsar://localhost:6650"; + + /** + * Authentication settings. + */ + private final Authentication authentication = new Authentication(); + + public String getServiceUrl() { + return this.serviceUrl; + } + + public void setServiceUrl(String serviceUrl) { + this.serviceUrl = serviceUrl; + } + + public Authentication getAuthentication() { + return this.authentication; + } + + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java index ed9411512eb0..d26bdbd62002 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java @@ -18,6 +18,7 @@ import java.time.Duration; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.TimeUnit; @@ -25,11 +26,16 @@ import java.util.function.Consumer; import org.apache.pulsar.client.admin.PulsarAdminBuilder; +import org.apache.pulsar.client.api.Authentication; +import org.apache.pulsar.client.api.AuthenticationFactory; +import org.apache.pulsar.client.api.AutoClusterFailoverBuilder; import org.apache.pulsar.client.api.ClientBuilder; import org.apache.pulsar.client.api.ConsumerBuilder; import org.apache.pulsar.client.api.ProducerBuilder; import org.apache.pulsar.client.api.PulsarClientException.UnsupportedAuthenticationException; import org.apache.pulsar.client.api.ReaderBuilder; +import org.apache.pulsar.client.api.ServiceUrlProvider; +import org.apache.pulsar.client.impl.AutoClusterFailover.AutoClusterFailoverBuilderImpl; import org.apache.pulsar.common.util.ObjectMapperFactory; import org.springframework.boot.context.properties.PropertyMapper; @@ -42,6 +48,7 @@ * * @author Chris Bono * @author Phillip Webb + * @author Swamy Mavuri */ final class PulsarPropertiesMapper { @@ -54,11 +61,50 @@ final class PulsarPropertiesMapper { void customizeClientBuilder(ClientBuilder clientBuilder, PulsarConnectionDetails connectionDetails) { PulsarProperties.Client properties = this.properties.getClient(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(connectionDetails::getBrokerUrl).to(clientBuilder::serviceUrl); map.from(properties::getConnectionTimeout).to(timeoutProperty(clientBuilder::connectionTimeout)); map.from(properties::getOperationTimeout).to(timeoutProperty(clientBuilder::operationTimeout)); map.from(properties::getLookupTimeout).to(timeoutProperty(clientBuilder::lookupTimeout)); customizeAuthentication(clientBuilder::authentication, properties.getAuthentication()); + customizeServiceUrlProviderBuilder(clientBuilder::serviceUrl, clientBuilder::serviceUrlProvider, properties, + connectionDetails); + } + + private void customizeServiceUrlProviderBuilder(Consumer serviceUrlConsumer, + Consumer serviceUrlProviderConsumer, PulsarProperties.Client properties, + PulsarConnectionDetails connectionDetails) { + PulsarProperties.Failover failoverProperties = properties.getFailover(); + if (!failoverProperties.getBackupClusters().isEmpty()) { + Map secondaryAuths = new LinkedHashMap<>(); + failoverProperties.getBackupClusters().forEach((cluster) -> { + PulsarProperties.Authentication authentication = cluster.getAuthentication(); + if (authentication.getPluginClassName() != null) { + customizeAuthentication((authPluginClassName, authParams) -> secondaryAuths + .put(cluster.getServiceUrl(), AuthenticationFactory.create(authPluginClassName, authParams)), + authentication); + } + else { + secondaryAuths.put(cluster.getServiceUrl(), null); + } + }); + + AutoClusterFailoverBuilder autoClusterFailoverBuilder = new AutoClusterFailoverBuilderImpl(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(connectionDetails::getBrokerUrl).to(autoClusterFailoverBuilder::primary); + map.from(new ArrayList<>(secondaryAuths.keySet())).to(autoClusterFailoverBuilder::secondary); + map.from(failoverProperties::getFailoverPolicy).to(autoClusterFailoverBuilder::failoverPolicy); + map.from(failoverProperties::getFailOverDelay) + .to(timeoutProperty(autoClusterFailoverBuilder::failoverDelay)); + map.from(failoverProperties::getSwitchBackDelay) + .to(timeoutProperty(autoClusterFailoverBuilder::switchBackDelay)); + map.from(failoverProperties::getCheckInterval) + .to(timeoutProperty(autoClusterFailoverBuilder::checkInterval)); + map.from(secondaryAuths).to(autoClusterFailoverBuilder::secondaryAuthentication); + + serviceUrlProviderConsumer.accept(autoClusterFailoverBuilder.build()); + } + else { + serviceUrlConsumer.accept(connectionDetails.getBrokerUrl()); + } } void customizeAdminBuilder(PulsarAdminBuilder adminBuilder, PulsarConnectionDetails connectionDetails) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/MockAuthentication.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/MockAuthentication.java new file mode 100644 index 000000000000..cdf8f5b7025b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/MockAuthentication.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2023 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.pulsar; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.apache.pulsar.client.api.Authentication; +import org.apache.pulsar.client.api.AuthenticationDataProvider; +import org.apache.pulsar.client.api.PulsarClientException; + +/** + * Test plugin-class-name for Authentication + * + * @author Swamy Mavuri + */ + +public class MockAuthentication implements Authentication { + + public Map authParamsMap = new HashMap<>(); + + @Override + public String getAuthMethodName() { + return null; + } + + @Override + public AuthenticationDataProvider getAuthData() { + return null; + } + + @Override + @Deprecated + public void configure(Map authParams) { + this.authParamsMap = authParams; + } + + @Override + public void start() throws PulsarClientException { + + } + + @Override + public void close() throws IOException { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfigurationTests.java index a1136b11ba29..999b0d225aa9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfigurationTests.java @@ -16,21 +16,26 @@ package org.springframework.boot.autoconfigure.pulsar; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.function.BiConsumer; import java.util.function.Consumer; import org.apache.pulsar.client.admin.PulsarAdminBuilder; import org.apache.pulsar.client.api.ClientBuilder; import org.apache.pulsar.client.api.PulsarClient; import org.apache.pulsar.client.api.Schema; +import org.apache.pulsar.client.impl.AutoClusterFailover; import org.apache.pulsar.common.schema.KeyValueEncodingType; import org.assertj.core.api.InstanceOfAssertFactories; import org.assertj.core.api.InstanceOfAssertFactory; import org.assertj.core.api.MapAssert; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.mockito.InOrder; +import org.mockito.Mockito; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.TestConfiguration; @@ -48,10 +53,12 @@ import org.springframework.pulsar.core.SchemaResolver.SchemaResolverCustomizer; import org.springframework.pulsar.core.TopicResolver; import org.springframework.pulsar.function.PulsarFunctionAdministration; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; /** @@ -61,6 +68,7 @@ * @author Alexander Preuß * @author Soby Chacko * @author Phillip Webb + * @author Swamy Mavuri */ class PulsarConfigurationTests { @@ -113,6 +121,42 @@ void whenHasUserDefinedCustomizersAppliesInCorrectOrder() { }); } + @Test + void whenHasUserDefinedFailoverPropertiesAddsToClient() { + PulsarConnectionDetails connectionDetails = mock(PulsarConnectionDetails.class); + given(connectionDetails.getBrokerUrl()).willReturn("connectiondetails"); + + PulsarConfigurationTests.this.contextRunner.withBean(PulsarConnectionDetails.class, () -> connectionDetails) + .withPropertyValues("spring.pulsar.client.service-url=properties", + "spring.pulsar.client.failover.backup-clusters[0].service-url=backup-cluster-1", + "spring.pulsar.client.failover.failover-delay=15s", + "spring.pulsar.client.failover.switch-back-delay=30s", + "spring.pulsar.client.failover.check-interval=5s", + "spring.pulsar.client.failover.backup-clusters[1].service-url=backup-cluster-2", + "spring.pulsar.client.failover.backup-clusters[1].authentication.plugin-class-name=org.springframework.boot.autoconfigure.pulsar.MockAuthentication", + "spring.pulsar.client.failover.backup-clusters[1].authentication.param.token=1234") + .run((context) -> { + DefaultPulsarClientFactory clientFactory = context.getBean(DefaultPulsarClientFactory.class); + PulsarProperties pulsarProperties = context.getBean(PulsarProperties.class); + + ClientBuilder target = mock(ClientBuilder.class); + BiConsumer customizeAction = PulsarClientBuilderCustomizer::customize; + PulsarClientBuilderCustomizer pulsarClientBuilderCustomizer = (PulsarClientBuilderCustomizer) ReflectionTestUtils + .getField(clientFactory, "customizer"); + customizeAction.accept(pulsarClientBuilderCustomizer, target); + InOrder ordered = inOrder(target); + ordered.verify(target).serviceUrlProvider(Mockito.any(AutoClusterFailover.class)); + + assertThat(pulsarProperties.getClient().getFailover().getFailOverDelay()) + .isEqualTo(Duration.ofSeconds(15)); + assertThat(pulsarProperties.getClient().getFailover().getSwitchBackDelay()) + .isEqualTo(Duration.ofSeconds(30)); + assertThat(pulsarProperties.getClient().getFailover().getCheckInterval()) + .isEqualTo(Duration.ofSeconds(5)); + assertThat(pulsarProperties.getClient().getFailover().getBackupClusters().size()).isEqualTo(2); + }); + } + @TestConfiguration(proxyBeanMethods = false) static class PulsarClientBuilderCustomizersConfig { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java index 458b3abf480b..55a393a54207 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java @@ -23,6 +23,7 @@ import java.util.regex.Pattern; import org.apache.pulsar.client.admin.PulsarAdminBuilder; +import org.apache.pulsar.client.api.AutoClusterFailoverBuilder.FailoverPolicy; import org.apache.pulsar.client.api.ClientBuilder; import org.apache.pulsar.client.api.CompressionType; import org.apache.pulsar.client.api.ConsumerBuilder; @@ -34,10 +35,13 @@ import org.apache.pulsar.client.api.PulsarClientException.UnsupportedAuthenticationException; import org.apache.pulsar.client.api.ReaderBuilder; import org.apache.pulsar.client.api.SubscriptionType; +import org.apache.pulsar.client.impl.AutoClusterFailover; import org.apache.pulsar.common.schema.SchemaType; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.boot.autoconfigure.pulsar.PulsarProperties.Consumer; +import org.springframework.boot.autoconfigure.pulsar.PulsarProperties.Failover.BackupCluster; import org.springframework.pulsar.listener.PulsarContainerProperties; import static org.assertj.core.api.Assertions.assertThat; @@ -50,6 +54,7 @@ * * @author Chris Bono * @author Phillip Webb + * @author Swamy Mavuri */ class PulsarPropertiesMapperTests { @@ -93,6 +98,35 @@ void customizeClientBuilderWhenHasConnectionDetails() { then(builder).should().serviceUrl("https://used.example.com"); } + @Test + void customizeClientBuilderWhenHasFailover() { + BackupCluster backupCluster1 = new BackupCluster(); + backupCluster1.setServiceUrl("backup-cluster-1"); + Map params = Map.of("param", "name"); + backupCluster1.getAuthentication() + .setPluginClassName("org.springframework.boot.autoconfigure.pulsar.MockAuthentication"); + backupCluster1.getAuthentication().setParam(params); + + BackupCluster backupCluster2 = new BackupCluster(); + backupCluster2.setServiceUrl("backup-cluster-2"); + + PulsarProperties properties = new PulsarProperties(); + properties.getClient().setServiceUrl("https://used.example.com"); + properties.getClient().getFailover().setFailoverPolicy(FailoverPolicy.ORDER); + properties.getClient().getFailover().setCheckInterval(Duration.ofSeconds(5)); + properties.getClient().getFailover().setFailOverDelay(Duration.ofSeconds(30)); + properties.getClient().getFailover().setSwitchBackDelay(Duration.ofSeconds(30)); + properties.getClient().getFailover().setBackupClusters(List.of(backupCluster1, backupCluster2)); + + PulsarConnectionDetails connectionDetails = mock(PulsarConnectionDetails.class); + given(connectionDetails.getBrokerUrl()).willReturn("https://used.example.com"); + + ClientBuilder builder = mock(ClientBuilder.class); + new PulsarPropertiesMapper(properties).customizeClientBuilder(builder, + new PropertiesPulsarConnectionDetails(properties)); + then(builder).should().serviceUrlProvider(Mockito.any(AutoClusterFailover.class)); + } + @Test void customizeAdminBuilderWhenHasNoAuthentication() { PulsarProperties properties = new PulsarProperties(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesTests.java index 48c17247a4f9..fe104453c691 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesTests.java @@ -18,6 +18,7 @@ import java.time.Duration; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.apache.pulsar.client.api.CompressionType; @@ -34,6 +35,8 @@ import org.springframework.boot.autoconfigure.pulsar.PulsarProperties.Defaults.SchemaInfo; import org.springframework.boot.autoconfigure.pulsar.PulsarProperties.Defaults.TypeMapping; +import org.springframework.boot.autoconfigure.pulsar.PulsarProperties.Failover; +import org.springframework.boot.autoconfigure.pulsar.PulsarProperties.Failover.BackupCluster; import org.springframework.boot.context.properties.bind.BindException; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; @@ -48,6 +51,7 @@ * @author Christophe Bornet * @author Soby Chacko * @author Phillip Webb + * @author Swamy Mavuri */ class PulsarPropertiesTests { @@ -82,6 +86,36 @@ void bindAuthentication() { assertThat(properties.getAuthentication().getParam()).containsEntry("token", "1234"); } + @Test + void bindFailover() { + Map map = new HashMap<>(); + map.put("spring.pulsar.client.service-url", "my-service-url"); + map.put("spring.pulsar.client.failover.failover-delay", "30s"); + map.put("spring.pulsar.client.failover.switch-back-delay", "15s"); + map.put("spring.pulsar.client.failover.check-interval", "1s"); + map.put("spring.pulsar.client.failover.backup-clusters[0].service-url", "backup-service-url-1"); + map.put("spring.pulsar.client.failover.backup-clusters[0].authentication.plugin-class-name", + "com.example.MyAuth1"); + map.put("spring.pulsar.client.failover.backup-clusters[0].authentication.param.token", "1234"); + map.put("spring.pulsar.client.failover.backup-clusters[1].service-url", "backup-service-url-2"); + map.put("spring.pulsar.client.failover.backup-clusters[1].authentication.plugin-class-name", + "com.example.MyAuth2"); + map.put("spring.pulsar.client.failover.backup-clusters[1].authentication.param.token", "5678"); + PulsarProperties.Client properties = bindPropeties(map).getClient(); + Failover failoverProperties = properties.getFailover(); + List backupClusters = properties.getFailover().getBackupClusters(); + assertThat(properties.getServiceUrl()).isEqualTo("my-service-url"); + assertThat(failoverProperties.getFailOverDelay()).isEqualTo(Duration.ofMillis(30000)); + assertThat(failoverProperties.getSwitchBackDelay()).isEqualTo(Duration.ofMillis(15000)); + assertThat(failoverProperties.getCheckInterval()).isEqualTo(Duration.ofMillis(1000)); + assertThat(backupClusters.get(0).getServiceUrl()).isEqualTo("backup-service-url-1"); + assertThat(backupClusters.get(0).getAuthentication().getPluginClassName()).isEqualTo("com.example.MyAuth1"); + assertThat(backupClusters.get(0).getAuthentication().getParam()).containsEntry("token", "1234"); + assertThat(backupClusters.get(1).getServiceUrl()).isEqualTo("backup-service-url-2"); + assertThat(backupClusters.get(1).getAuthentication().getPluginClassName()).isEqualTo("com.example.MyAuth2"); + assertThat(backupClusters.get(1).getAuthentication().getParam()).containsEntry("token", "5678"); + } + } @Nested From f696190d8399156c480dbddd244b472d6dd5af1c Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 10 Jan 2024 13:13:41 +0100 Subject: [PATCH 1021/1215] Polish "Add support for Pulsar cluster-level failover" See gh-38559 --- .../pulsar/PulsarProperties.java | 3 +- .../pulsar/PulsarPropertiesMapper.java | 54 +++++++++---------- .../pulsar/MockAuthentication.java | 3 +- .../pulsar/PulsarConfigurationTests.java | 3 -- .../pulsar/PulsarPropertiesMapperTests.java | 4 -- 5 files changed, 26 insertions(+), 41 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java index 1f9c7983de73..37fe9a3b4a1f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java @@ -19,7 +19,6 @@ import java.time.Duration; import java.util.ArrayList; import java.util.LinkedHashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.regex.Pattern; @@ -928,7 +927,7 @@ public static class Failover { * given list. If all backup clusters are available, the Pulsar client chooses the * first backup cluster. */ - private List backupClusters = new LinkedList<>(); + private List backupClusters = new ArrayList<>(); public FailoverPolicy getFailoverPolicy() { return this.failoverPolicy; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java index d26bdbd62002..31755501b5ad 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java @@ -73,38 +73,32 @@ private void customizeServiceUrlProviderBuilder(Consumer serviceUrlConsu Consumer serviceUrlProviderConsumer, PulsarProperties.Client properties, PulsarConnectionDetails connectionDetails) { PulsarProperties.Failover failoverProperties = properties.getFailover(); - if (!failoverProperties.getBackupClusters().isEmpty()) { - Map secondaryAuths = new LinkedHashMap<>(); - failoverProperties.getBackupClusters().forEach((cluster) -> { - PulsarProperties.Authentication authentication = cluster.getAuthentication(); - if (authentication.getPluginClassName() != null) { - customizeAuthentication((authPluginClassName, authParams) -> secondaryAuths - .put(cluster.getServiceUrl(), AuthenticationFactory.create(authPluginClassName, authParams)), - authentication); - } - else { - secondaryAuths.put(cluster.getServiceUrl(), null); - } - }); - - AutoClusterFailoverBuilder autoClusterFailoverBuilder = new AutoClusterFailoverBuilderImpl(); - PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - map.from(connectionDetails::getBrokerUrl).to(autoClusterFailoverBuilder::primary); - map.from(new ArrayList<>(secondaryAuths.keySet())).to(autoClusterFailoverBuilder::secondary); - map.from(failoverProperties::getFailoverPolicy).to(autoClusterFailoverBuilder::failoverPolicy); - map.from(failoverProperties::getFailOverDelay) - .to(timeoutProperty(autoClusterFailoverBuilder::failoverDelay)); - map.from(failoverProperties::getSwitchBackDelay) - .to(timeoutProperty(autoClusterFailoverBuilder::switchBackDelay)); - map.from(failoverProperties::getCheckInterval) - .to(timeoutProperty(autoClusterFailoverBuilder::checkInterval)); - map.from(secondaryAuths).to(autoClusterFailoverBuilder::secondaryAuthentication); - - serviceUrlProviderConsumer.accept(autoClusterFailoverBuilder.build()); - } - else { + if (failoverProperties.getBackupClusters().isEmpty()) { serviceUrlConsumer.accept(connectionDetails.getBrokerUrl()); + return; } + Map secondaryAuths = new LinkedHashMap<>(); + failoverProperties.getBackupClusters().forEach((cluster) -> { + PulsarProperties.Authentication authentication = cluster.getAuthentication(); + if (authentication.getPluginClassName() == null) { + secondaryAuths.put(cluster.getServiceUrl(), null); + } + else { + customizeAuthentication((authPluginClassName, authParams) -> secondaryAuths.put(cluster.getServiceUrl(), + AuthenticationFactory.create(authPluginClassName, authParams)), authentication); + } + }); + AutoClusterFailoverBuilder autoClusterFailoverBuilder = new AutoClusterFailoverBuilderImpl(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(connectionDetails::getBrokerUrl).to(autoClusterFailoverBuilder::primary); + map.from(new ArrayList<>(secondaryAuths.keySet())).to(autoClusterFailoverBuilder::secondary); + map.from(failoverProperties::getFailoverPolicy).to(autoClusterFailoverBuilder::failoverPolicy); + map.from(failoverProperties::getFailOverDelay).to(timeoutProperty(autoClusterFailoverBuilder::failoverDelay)); + map.from(failoverProperties::getSwitchBackDelay) + .to(timeoutProperty(autoClusterFailoverBuilder::switchBackDelay)); + map.from(failoverProperties::getCheckInterval).to(timeoutProperty(autoClusterFailoverBuilder::checkInterval)); + map.from(secondaryAuths).to(autoClusterFailoverBuilder::secondaryAuthentication); + serviceUrlProviderConsumer.accept(autoClusterFailoverBuilder.build()); } void customizeAdminBuilder(PulsarAdminBuilder adminBuilder, PulsarConnectionDetails connectionDetails) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/MockAuthentication.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/MockAuthentication.java index cdf8f5b7025b..d398d4349931 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/MockAuthentication.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/MockAuthentication.java @@ -29,7 +29,7 @@ * * @author Swamy Mavuri */ - +@SuppressWarnings("deprecation") public class MockAuthentication implements Authentication { public Map authParamsMap = new HashMap<>(); @@ -45,7 +45,6 @@ public AuthenticationDataProvider getAuthData() { } @Override - @Deprecated public void configure(Map authParams) { this.authParamsMap = authParams; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfigurationTests.java index 999b0d225aa9..bb3ff549ec6b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarConfigurationTests.java @@ -125,7 +125,6 @@ void whenHasUserDefinedCustomizersAppliesInCorrectOrder() { void whenHasUserDefinedFailoverPropertiesAddsToClient() { PulsarConnectionDetails connectionDetails = mock(PulsarConnectionDetails.class); given(connectionDetails.getBrokerUrl()).willReturn("connectiondetails"); - PulsarConfigurationTests.this.contextRunner.withBean(PulsarConnectionDetails.class, () -> connectionDetails) .withPropertyValues("spring.pulsar.client.service-url=properties", "spring.pulsar.client.failover.backup-clusters[0].service-url=backup-cluster-1", @@ -138,7 +137,6 @@ void whenHasUserDefinedFailoverPropertiesAddsToClient() { .run((context) -> { DefaultPulsarClientFactory clientFactory = context.getBean(DefaultPulsarClientFactory.class); PulsarProperties pulsarProperties = context.getBean(PulsarProperties.class); - ClientBuilder target = mock(ClientBuilder.class); BiConsumer customizeAction = PulsarClientBuilderCustomizer::customize; PulsarClientBuilderCustomizer pulsarClientBuilderCustomizer = (PulsarClientBuilderCustomizer) ReflectionTestUtils @@ -146,7 +144,6 @@ void whenHasUserDefinedFailoverPropertiesAddsToClient() { customizeAction.accept(pulsarClientBuilderCustomizer, target); InOrder ordered = inOrder(target); ordered.verify(target).serviceUrlProvider(Mockito.any(AutoClusterFailover.class)); - assertThat(pulsarProperties.getClient().getFailover().getFailOverDelay()) .isEqualTo(Duration.ofSeconds(15)); assertThat(pulsarProperties.getClient().getFailover().getSwitchBackDelay()) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java index 55a393a54207..06b20589fe5d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java @@ -106,10 +106,8 @@ void customizeClientBuilderWhenHasFailover() { backupCluster1.getAuthentication() .setPluginClassName("org.springframework.boot.autoconfigure.pulsar.MockAuthentication"); backupCluster1.getAuthentication().setParam(params); - BackupCluster backupCluster2 = new BackupCluster(); backupCluster2.setServiceUrl("backup-cluster-2"); - PulsarProperties properties = new PulsarProperties(); properties.getClient().setServiceUrl("https://used.example.com"); properties.getClient().getFailover().setFailoverPolicy(FailoverPolicy.ORDER); @@ -117,10 +115,8 @@ void customizeClientBuilderWhenHasFailover() { properties.getClient().getFailover().setFailOverDelay(Duration.ofSeconds(30)); properties.getClient().getFailover().setSwitchBackDelay(Duration.ofSeconds(30)); properties.getClient().getFailover().setBackupClusters(List.of(backupCluster1, backupCluster2)); - PulsarConnectionDetails connectionDetails = mock(PulsarConnectionDetails.class); given(connectionDetails.getBrokerUrl()).willReturn("https://used.example.com"); - ClientBuilder builder = mock(ClientBuilder.class); new PulsarPropertiesMapper(properties).customizeClientBuilder(builder, new PropertiesPulsarConnectionDetails(properties)); From e9bce315ae9372798862810d6c776e5bd9b14603 Mon Sep 17 00:00:00 2001 From: Yan Kardziyaka Date: Mon, 30 Oct 2023 03:26:59 +0300 Subject: [PATCH 1022/1215] Auto-configure a JwtAuthenticationConverter See gh-38105 --- .../OAuth2ResourceServerProperties.java | 56 ++++++++++ ...tiveOAuth2ResourceServerConfiguration.java | 1 + ...eOAuth2ResourceServerJwkConfiguration.java | 33 ++++++ .../OAuth2ResourceServerJwtConfiguration.java | 31 ++++++ .../Oauth2ResourceServerConfiguration.java | 3 +- ...verterCustomizationsArgumentsProvider.java | 104 ++++++++++++++++++ ...2ResourceServerAutoConfigurationTests.java | 61 ++++++++++ ...2ResourceServerAutoConfigurationTests.java | 60 ++++++++++ 8 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/JwtConverterCustomizationsArgumentsProvider.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java index 61c478793220..15f93db7ad04 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java @@ -26,6 +26,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; import org.springframework.core.io.Resource; +import org.springframework.security.core.GrantedAuthority; import org.springframework.util.Assert; import org.springframework.util.StreamUtils; @@ -35,6 +36,7 @@ * @author Madhura Bhave * @author Artsiom Yudovin * @author Mushtaq Ahmed + * @author Yan Kardziyaka * @since 2.1.0 */ @ConfigurationProperties(prefix = "spring.security.oauth2.resourceserver") @@ -80,6 +82,28 @@ public static class Jwt { */ private List audiences = new ArrayList<>(); + /** + * Prefix to use for {@link GrantedAuthority authorities} mapped from JWT. + */ + private String authorityPrefix; + + /** + * Regex to use for splitting the value of the authorities claim into + * {@link GrantedAuthority authorities}. + */ + private String authoritiesClaimDelimiter; + + /** + * Name of token claim to use for mapping {@link GrantedAuthority authorities} + * from JWT. + */ + private String authoritiesClaimName; + + /** + * JWT principal claim name. + */ + private String principalClaimName; + public String getJwkSetUri() { return this.jwkSetUri; } @@ -120,6 +144,38 @@ public void setAudiences(List audiences) { this.audiences = audiences; } + public String getAuthorityPrefix() { + return this.authorityPrefix; + } + + public void setAuthorityPrefix(String authorityPrefix) { + this.authorityPrefix = authorityPrefix; + } + + public String getAuthoritiesClaimDelimiter() { + return this.authoritiesClaimDelimiter; + } + + public void setAuthoritiesClaimDelimiter(String authoritiesClaimDelimiter) { + this.authoritiesClaimDelimiter = authoritiesClaimDelimiter; + } + + public String getAuthoritiesClaimName() { + return this.authoritiesClaimName; + } + + public void setAuthoritiesClaimName(String authoritiesClaimName) { + this.authoritiesClaimName = authoritiesClaimName; + } + + public String getPrincipalClaimName() { + return this.principalClaimName; + } + + public void setPrincipalClaimName(String principalClaimName) { + this.principalClaimName = principalClaimName; + } + public String readPublicKey() throws IOException { String key = "spring.security.oauth2.resourceserver.public-key-location"; Assert.notNull(this.publicKeyLocation, "PublicKeyLocation must not be null"); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerConfiguration.java index d4f5388f041d..31ae7f901323 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerConfiguration.java @@ -34,6 +34,7 @@ class ReactiveOAuth2ResourceServerConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ BearerTokenAuthenticationToken.class, ReactiveJwtDecoder.class }) @Import({ ReactiveOAuth2ResourceServerJwkConfiguration.JwtConfiguration.class, + ReactiveOAuth2ResourceServerJwkConfiguration.JwtConverterConfiguration.class, ReactiveOAuth2ResourceServerJwkConfiguration.WebSecurityConfiguration.class }) static class JwtConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java index 5f5cba160eaa..b00fce2b734d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java @@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.security.oauth2.resource.IssuerUriCondition; import org.springframework.boot.autoconfigure.security.oauth2.resource.KeyValueCondition; import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; +import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -48,6 +49,9 @@ import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.JwkSetUriReactiveJwtDecoderBuilder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtGrantedAuthoritiesConverterAdapter; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.util.CollectionUtils; @@ -62,6 +66,7 @@ * @author Anastasiia Losieva * @author Mushtaq Ahmed * @author Roman Golovin + * @author Yan Kardziyaka */ @Configuration(proxyBeanMethods = false) class ReactiveOAuth2ResourceServerJwkConfiguration { @@ -161,6 +166,34 @@ SupplierReactiveJwtDecoder jwtDecoderByIssuerUri( } + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(ReactiveJwtAuthenticationConverter.class) + static class JwtConverterConfiguration { + + private final OAuth2ResourceServerProperties.Jwt properties; + + JwtConverterConfiguration(OAuth2ResourceServerProperties properties) { + this.properties = properties.getJwt(); + } + + @Bean + ReactiveJwtAuthenticationConverter reactiveJwtAuthenticationConverter() { + JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(this.properties.getAuthorityPrefix()).to(grantedAuthoritiesConverter::setAuthorityPrefix); + map.from(this.properties.getAuthoritiesClaimDelimiter()) + .to(grantedAuthoritiesConverter::setAuthoritiesClaimDelimiter); + map.from(this.properties.getAuthoritiesClaimName()) + .to(grantedAuthoritiesConverter::setAuthoritiesClaimName); + ReactiveJwtAuthenticationConverter jwtAuthenticationConverter = new ReactiveJwtAuthenticationConverter(); + map.from(this.properties.getPrincipalClaimName()).to(jwtAuthenticationConverter::setPrincipalClaimName); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter( + new ReactiveJwtGrantedAuthoritiesConverterAdapter(grantedAuthoritiesConverter)); + return jwtAuthenticationConverter; + } + + } + @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(SecurityWebFilterChain.class) static class WebSecurityConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java index 84bafab99db0..b55e238a2f53 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java @@ -33,6 +33,7 @@ import org.springframework.boot.autoconfigure.security.oauth2.resource.IssuerUriCondition; import org.springframework.boot.autoconfigure.security.oauth2.resource.KeyValueCondition; import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; +import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -48,6 +49,8 @@ import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder; import org.springframework.security.oauth2.jwt.SupplierJwtDecoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.security.web.SecurityFilterChain; import org.springframework.util.CollectionUtils; @@ -63,6 +66,7 @@ * @author HaiTao Zhang * @author Mushtaq Ahmed * @author Roman Golovin + * @author Yan Kardziyaka */ @Configuration(proxyBeanMethods = false) class OAuth2ResourceServerJwtConfiguration { @@ -173,4 +177,31 @@ SecurityFilterChain jwtSecurityFilterChain(HttpSecurity http) throws Exception { } + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(JwtAuthenticationConverter.class) + static class JwtConverterConfiguration { + + private final OAuth2ResourceServerProperties.Jwt properties; + + JwtConverterConfiguration(OAuth2ResourceServerProperties properties) { + this.properties = properties.getJwt(); + } + + @Bean + JwtAuthenticationConverter getJwtAuthenticationConverter() { + JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(this.properties.getAuthorityPrefix()).to(grantedAuthoritiesConverter::setAuthorityPrefix); + map.from(this.properties.getAuthoritiesClaimDelimiter()) + .to(grantedAuthoritiesConverter::setAuthoritiesClaimDelimiter); + map.from(this.properties.getAuthoritiesClaimName()) + .to(grantedAuthoritiesConverter::setAuthoritiesClaimName); + JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); + map.from(this.properties.getPrincipalClaimName()).to(jwtAuthenticationConverter::setPrincipalClaimName); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); + return jwtAuthenticationConverter; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/Oauth2ResourceServerConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/Oauth2ResourceServerConfiguration.java index 36c522e39a7b..a6726ed28f59 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/Oauth2ResourceServerConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/Oauth2ResourceServerConfiguration.java @@ -32,7 +32,8 @@ class Oauth2ResourceServerConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnClass(JwtDecoder.class) @Import({ OAuth2ResourceServerJwtConfiguration.JwtDecoderConfiguration.class, - OAuth2ResourceServerJwtConfiguration.OAuth2SecurityFilterChainConfiguration.class }) + OAuth2ResourceServerJwtConfiguration.OAuth2SecurityFilterChainConfiguration.class, + OAuth2ResourceServerJwtConfiguration.JwtConverterConfiguration.class }) static class JwtConfiguration { } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/JwtConverterCustomizationsArgumentsProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/JwtConverterCustomizationsArgumentsProvider.java new file mode 100644 index 000000000000..45f445173546 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/JwtConverterCustomizationsArgumentsProvider.java @@ -0,0 +1,104 @@ +/* + * Copyright 2012-2023 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.security.oauth2.resource; + +import java.time.Instant; +import java.util.UUID; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; + +import org.springframework.security.oauth2.jwt.Jwt; + +/** + * {@link ArgumentsProvider Arguments provider} supplying different Spring Boot properties + * to customize JWT converter behavior, JWT token for conversion, expected principal name + * and expected authorities. + * + * @author Yan Kardziyaka + */ +public final class JwtConverterCustomizationsArgumentsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext extensionContext) { + String customPrefix = "CUSTOM_AUTHORITY_PREFIX_"; + String customDelimiter = "[~,#:]"; + String customAuthoritiesClaim = "custom_authorities"; + String customPrincipalClaim = "custom_principal"; + + String jwkSetUriProperty = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com"; + String authorityPrefixProperty = "spring.security.oauth2.resourceserver.jwt.authority-prefix=" + customPrefix; + String authoritiesDelimiterProperty = "spring.security.oauth2.resourceserver.jwt.authorities-claim-delimiter=" + + customDelimiter; + String authoritiesClaimProperty = "spring.security.oauth2.resourceserver.jwt.authorities-claim-name=" + + customAuthoritiesClaim; + String principalClaimProperty = "spring.security.oauth2.resourceserver.jwt.principal-claim-name=" + + customPrincipalClaim; + + String[] noJwtConverterProps = { jwkSetUriProperty }; + String[] customPrefixProps = { jwkSetUriProperty, authorityPrefixProperty }; + String[] customDelimiterProps = { jwkSetUriProperty, authoritiesDelimiterProperty }; + String[] customAuthoritiesClaimProps = { jwkSetUriProperty, authoritiesClaimProperty }; + String[] customPrincipalClaimProps = { jwkSetUriProperty, principalClaimProperty }; + String[] allJwtConverterProps = { jwkSetUriProperty, authorityPrefixProperty, authoritiesDelimiterProperty, + authoritiesClaimProperty, principalClaimProperty }; + + String[] jwtScopes = { "custom_scope0", "custom_scope1" }; + String subjectValue = UUID.randomUUID().toString(); + String customPrincipalValue = UUID.randomUUID().toString(); + + Jwt.Builder jwtBuilder = Jwt.withTokenValue("token") + .header("alg", "none") + .expiresAt(Instant.MAX) + .issuedAt(Instant.MIN) + .issuer("https://issuer.example.org") + .jti("jti") + .notBefore(Instant.MIN) + .subject(subjectValue) + .claim(customPrincipalClaim, customPrincipalValue); + + Jwt noAuthoritiesCustomizationsJwt = jwtBuilder.claim("scp", jwtScopes[0] + " " + jwtScopes[1]).build(); + Jwt customAuthoritiesDelimiterJwt = jwtBuilder.claim("scp", jwtScopes[0] + "~" + jwtScopes[1]).build(); + Jwt customAuthoritiesClaimJwt = jwtBuilder.claim("scp", null) + .claim(customAuthoritiesClaim, jwtScopes[0] + " " + jwtScopes[1]) + .build(); + Jwt customAuthoritiesClaimAndDelimiterJwt = jwtBuilder.claim("scp", null) + .claim(customAuthoritiesClaim, jwtScopes[0] + "~" + jwtScopes[1]) + .build(); + + String[] customPrefixAuthorities = { customPrefix + jwtScopes[0], customPrefix + jwtScopes[1] }; + String[] defaultPrefixAuthorities = { "SCOPE_" + jwtScopes[0], "SCOPE_" + jwtScopes[1] }; + + return Stream.of( + Arguments.of(Named.named("No JWT converter customizations", noJwtConverterProps), + noAuthoritiesCustomizationsJwt, subjectValue, defaultPrefixAuthorities), + Arguments.of(Named.named("Custom prefix for GrantedAuthority", customPrefixProps), + noAuthoritiesCustomizationsJwt, subjectValue, customPrefixAuthorities), + Arguments.of(Named.named("Custom delimiter for JWT scopes", customDelimiterProps), + customAuthoritiesDelimiterJwt, subjectValue, defaultPrefixAuthorities), + Arguments.of(Named.named("Custom JWT authority claim name", customAuthoritiesClaimProps), + customAuthoritiesClaimJwt, subjectValue, defaultPrefixAuthorities), + Arguments.of(Named.named("Custom JWT principal claim name", customPrincipalClaimProps), + noAuthoritiesCustomizationsJwt, customPrincipalValue, defaultPrefixAuthorities), + Arguments.of(Named.named("All JWT converter customizations", allJwtConverterProps), + customAuthoritiesClaimAndDelimiterJwt, customPrincipalValue, customPrefixAuthorities)); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java index e8165ee18916..5c4e359ce424 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java @@ -39,10 +39,13 @@ import org.assertj.core.api.ThrowingConsumer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; import org.mockito.InOrder; import reactor.core.publisher.Mono; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.security.oauth2.resource.JwtConverterCustomizationsArgumentsProvider; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; @@ -52,10 +55,12 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver; import org.springframework.security.config.BeanIds; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidator; @@ -70,6 +75,7 @@ import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken; import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager; import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenReactiveAuthenticationManager; +import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector; import org.springframework.security.web.server.MatcherSecurityWebFilterChain; import org.springframework.security.web.server.SecurityWebFilterChain; @@ -92,6 +98,7 @@ * @author Anastasiia Losieva * @author Mushtaq Ahmed * @author Roman Golovin + * @author Yan Kardziyaka */ class ReactiveOAuth2ResourceServerAutoConfigurationTests { @@ -626,6 +633,46 @@ void customValidatorWhenInvalid() throws Exception { }); } + @ParameterizedTest(name = "{0}") + @ArgumentsSource(JwtConverterCustomizationsArgumentsProvider.class) + void autoConfigurationShouldConfigureResourceServerWithJwtConverterCustomizations(String[] properties, Jwt jwt, + String expectedPrincipal, String[] expectedAuthorities) { + this.contextRunner.withPropertyValues(properties).run((context) -> { + ReactiveJwtAuthenticationConverter converter = context.getBean(ReactiveJwtAuthenticationConverter.class); + AbstractAuthenticationToken token = converter.convert(jwt).block(); + assertThat(token).isNotNull().extracting(AbstractAuthenticationToken::getName).isEqualTo(expectedPrincipal); + assertThat(token.getAuthorities()).extracting(GrantedAuthority::getAuthority) + .containsExactlyInAnyOrder(expectedAuthorities); + assertThat(context).hasSingleBean(NimbusReactiveJwtDecoder.class); + assertFilterConfiguredWithJwtAuthenticationManager(context); + }); + } + + @Test + void jwtAuthenticationConverterByJwtConfigIsConditionalOnMissingBean() { + String propertiesPrincipalClaim = "principal_from_properties"; + String propertiesPrincipalValue = "from_props"; + String userConfigPrincipalValue = "from_user_config"; + this.contextRunner + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com", + "spring.security.oauth2.resourceserver.jwt.principal-claim-name=" + propertiesPrincipalClaim) + .withUserConfiguration(CustomJwtConverterConfig.class) + .run((context) -> { + ReactiveJwtAuthenticationConverter converter = context + .getBean(ReactiveJwtAuthenticationConverter.class); + Jwt jwt = jwt().claim(propertiesPrincipalClaim, propertiesPrincipalValue) + .claim(CustomJwtConverterConfig.PRINCIPAL_CLAIM, userConfigPrincipalValue) + .build(); + AbstractAuthenticationToken token = converter.convert(jwt).block(); + assertThat(token).isNotNull() + .extracting(AbstractAuthenticationToken::getName) + .isEqualTo(userConfigPrincipalValue) + .isNotEqualTo(propertiesPrincipalValue); + assertThat(context).hasSingleBean(NimbusReactiveJwtDecoder.class); + assertFilterConfiguredWithJwtAuthenticationManager(context); + }); + } + private void assertFilterConfiguredWithJwtAuthenticationManager(AssertableReactiveWebApplicationContext context) { MatcherSecurityWebFilterChain filterChain = (MatcherSecurityWebFilterChain) context .getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN); @@ -807,4 +854,18 @@ JwtClaimValidator customJwtClaimValidator() { } + @Configuration(proxyBeanMethods = false) + static class CustomJwtConverterConfig { + + static String PRINCIPAL_CLAIM = "principal_from_user_configuration"; + + @Bean + ReactiveJwtAuthenticationConverter customReactiveJwtAuthenticationConverter() { + ReactiveJwtAuthenticationConverter converter = new ReactiveJwtAuthenticationConverter(); + converter.setPrincipalClaimName(PRINCIPAL_CLAIM); + return converter; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java index af394c05585d..758ac7b6aa02 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java @@ -38,9 +38,12 @@ import org.assertj.core.api.ThrowingConsumer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ArgumentsSource; import org.mockito.InOrder; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.security.oauth2.resource.JwtConverterCustomizationsArgumentsProvider; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext; @@ -51,9 +54,11 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.config.BeanIds; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.jwt.Jwt; @@ -64,6 +69,7 @@ import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.jwt.SupplierJwtDecoder; import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter; @@ -84,6 +90,7 @@ * @author HaiTao Zhang * @author Mushtaq Ahmed * @author Roman Golovin + * @author Yan Kardziyaka */ class OAuth2ResourceServerAutoConfigurationTests { @@ -640,6 +647,45 @@ void opaqueTokenSecurityConfigurerBacksOffWhenSecurityFilterChainBeanIsPresent() .run((context) -> assertThat(context).hasSingleBean(SecurityFilterChain.class)); } + @ParameterizedTest(name = "{0}") + @ArgumentsSource(JwtConverterCustomizationsArgumentsProvider.class) + void autoConfigurationShouldConfigureResourceServerWithJwtConverterCustomizations(String[] properties, Jwt jwt, + String expectedPrincipal, String[] expectedAuthorities) { + this.contextRunner.withPropertyValues(properties).run((context) -> { + JwtAuthenticationConverter converter = context.getBean(JwtAuthenticationConverter.class); + AbstractAuthenticationToken token = converter.convert(jwt); + assertThat(token).isNotNull().extracting(AbstractAuthenticationToken::getName).isEqualTo(expectedPrincipal); + assertThat(token.getAuthorities()).extracting(GrantedAuthority::getAuthority) + .containsExactlyInAnyOrder(expectedAuthorities); + assertThat(context).hasSingleBean(JwtDecoder.class); + assertThat(getBearerTokenFilter(context)).isNotNull(); + }); + } + + @Test + void jwtAuthenticationConverterByJwtConfigIsConditionalOnMissingBean() { + String propertiesPrincipalClaim = "principal_from_properties"; + String propertiesPrincipalValue = "from_props"; + String userConfigPrincipalValue = "from_user_config"; + this.contextRunner + .withPropertyValues("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com", + "spring.security.oauth2.resourceserver.jwt.principal-claim-name=" + propertiesPrincipalClaim) + .withUserConfiguration(CustomJwtConverterConfig.class) + .run((context) -> { + JwtAuthenticationConverter converter = context.getBean(JwtAuthenticationConverter.class); + Jwt jwt = jwt().claim(propertiesPrincipalClaim, propertiesPrincipalValue) + .claim(CustomJwtConverterConfig.PRINCIPAL_CLAIM, userConfigPrincipalValue) + .build(); + AbstractAuthenticationToken token = converter.convert(jwt); + assertThat(token).isNotNull() + .extracting(AbstractAuthenticationToken::getName) + .isEqualTo(userConfigPrincipalValue) + .isNotEqualTo(propertiesPrincipalValue); + assertThat(context).hasSingleBean(JwtDecoder.class); + assertThat(getBearerTokenFilter(context)).isNotNull(); + }); + } + private Filter getBearerTokenFilter(AssertableWebApplicationContext context) { FilterChainProxy filterChain = (FilterChainProxy) context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN); List filterChains = filterChain.getFilterChains(); @@ -795,4 +841,18 @@ JwtClaimValidator customJwtClaimValidator() { } + @Configuration(proxyBeanMethods = false) + static class CustomJwtConverterConfig { + + static String PRINCIPAL_CLAIM = "principal_from_user_configuration"; + + @Bean + JwtAuthenticationConverter customJwtAuthenticationConverter() { + JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); + converter.setPrincipalClaimName(PRINCIPAL_CLAIM); + return converter; + } + + } + } From baf52214a2fcad69644d566650cc0c66e24ea10f Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 10 Jan 2024 14:45:40 +0100 Subject: [PATCH 1023/1215] Polish "Auto-configure a JwtAuthenticationConverter" The JwtConverter bean is only supplied, if one of the following properties is there: * spring.security.oauth2.resourceserver.jwt.authority-prefix * spring.security.oauth2.resourceserver.jwt.principal-claim-name * spring.security.oauth2.resourceserver.jwt.authorities-claim-name See gh-38105 --- ...eOAuth2ResourceServerJwkConfiguration.java | 25 +++++++++++++++++++ .../OAuth2ResourceServerJwtConfiguration.java | 25 +++++++++++++++++++ ...verterCustomizationsArgumentsProvider.java | 14 ++--------- ...2ResourceServerAutoConfigurationTests.java | 24 ++++++++++++++++++ ...2ResourceServerAutoConfigurationTests.java | 23 +++++++++++++++++ 5 files changed, 99 insertions(+), 12 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java index b00fce2b734d..ffeb7bf977ef 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java @@ -26,6 +26,7 @@ import java.util.Set; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -168,6 +169,7 @@ SupplierReactiveJwtDecoder jwtDecoderByIssuerUri( @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(ReactiveJwtAuthenticationConverter.class) + @Conditional(JwtConverterPropertiesCondition.class) static class JwtConverterConfiguration { private final OAuth2ResourceServerProperties.Jwt properties; @@ -212,4 +214,27 @@ private void customDecoder(OAuth2ResourceServerSpec server, ReactiveJwtDecoder d } + private static class JwtConverterPropertiesCondition extends AnyNestedCondition { + + JwtConverterPropertiesCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "authority-prefix") + static class OnAuthorityPrefix { + + } + + @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "principal-claim-name") + static class OnPrincipalClaimName { + + } + + @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "authorities-claim-name") + static class OnAuthoritiesClaimName { + + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java index b55e238a2f53..7ba584815dbb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java @@ -26,6 +26,7 @@ import java.util.Set; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -179,6 +180,7 @@ SecurityFilterChain jwtSecurityFilterChain(HttpSecurity http) throws Exception { @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(JwtAuthenticationConverter.class) + @Conditional(JwtConverterPropertiesCondition.class) static class JwtConverterConfiguration { private final OAuth2ResourceServerProperties.Jwt properties; @@ -204,4 +206,27 @@ JwtAuthenticationConverter getJwtAuthenticationConverter() { } + private static class JwtConverterPropertiesCondition extends AnyNestedCondition { + + JwtConverterPropertiesCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "authority-prefix") + static class OnAuthorityPrefix { + + } + + @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "principal-claim-name") + static class OnPrincipalClaimName { + + } + + @ConditionalOnProperty(prefix = "spring.security.oauth2.resourceserver.jwt", name = "authorities-claim-name") + static class OnAuthoritiesClaimName { + + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/JwtConverterCustomizationsArgumentsProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/JwtConverterCustomizationsArgumentsProvider.java index 45f445173546..cf40b50c443d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/JwtConverterCustomizationsArgumentsProvider.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/JwtConverterCustomizationsArgumentsProvider.java @@ -42,7 +42,6 @@ public Stream provideArguments(ExtensionContext extensionCo String customDelimiter = "[~,#:]"; String customAuthoritiesClaim = "custom_authorities"; String customPrincipalClaim = "custom_principal"; - String jwkSetUriProperty = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://jwk-set-uri.com"; String authorityPrefixProperty = "spring.security.oauth2.resourceserver.jwt.authority-prefix=" + customPrefix; String authoritiesDelimiterProperty = "spring.security.oauth2.resourceserver.jwt.authorities-claim-delimiter=" @@ -51,19 +50,15 @@ public Stream provideArguments(ExtensionContext extensionCo + customAuthoritiesClaim; String principalClaimProperty = "spring.security.oauth2.resourceserver.jwt.principal-claim-name=" + customPrincipalClaim; - - String[] noJwtConverterProps = { jwkSetUriProperty }; String[] customPrefixProps = { jwkSetUriProperty, authorityPrefixProperty }; - String[] customDelimiterProps = { jwkSetUriProperty, authoritiesDelimiterProperty }; + String[] customDelimiterProps = { jwkSetUriProperty, authorityPrefixProperty, authoritiesDelimiterProperty }; String[] customAuthoritiesClaimProps = { jwkSetUriProperty, authoritiesClaimProperty }; String[] customPrincipalClaimProps = { jwkSetUriProperty, principalClaimProperty }; String[] allJwtConverterProps = { jwkSetUriProperty, authorityPrefixProperty, authoritiesDelimiterProperty, authoritiesClaimProperty, principalClaimProperty }; - String[] jwtScopes = { "custom_scope0", "custom_scope1" }; String subjectValue = UUID.randomUUID().toString(); String customPrincipalValue = UUID.randomUUID().toString(); - Jwt.Builder jwtBuilder = Jwt.withTokenValue("token") .header("alg", "none") .expiresAt(Instant.MAX) @@ -73,7 +68,6 @@ public Stream provideArguments(ExtensionContext extensionCo .notBefore(Instant.MIN) .subject(subjectValue) .claim(customPrincipalClaim, customPrincipalValue); - Jwt noAuthoritiesCustomizationsJwt = jwtBuilder.claim("scp", jwtScopes[0] + " " + jwtScopes[1]).build(); Jwt customAuthoritiesDelimiterJwt = jwtBuilder.claim("scp", jwtScopes[0] + "~" + jwtScopes[1]).build(); Jwt customAuthoritiesClaimJwt = jwtBuilder.claim("scp", null) @@ -82,17 +76,13 @@ public Stream provideArguments(ExtensionContext extensionCo Jwt customAuthoritiesClaimAndDelimiterJwt = jwtBuilder.claim("scp", null) .claim(customAuthoritiesClaim, jwtScopes[0] + "~" + jwtScopes[1]) .build(); - String[] customPrefixAuthorities = { customPrefix + jwtScopes[0], customPrefix + jwtScopes[1] }; String[] defaultPrefixAuthorities = { "SCOPE_" + jwtScopes[0], "SCOPE_" + jwtScopes[1] }; - return Stream.of( - Arguments.of(Named.named("No JWT converter customizations", noJwtConverterProps), - noAuthoritiesCustomizationsJwt, subjectValue, defaultPrefixAuthorities), Arguments.of(Named.named("Custom prefix for GrantedAuthority", customPrefixProps), noAuthoritiesCustomizationsJwt, subjectValue, customPrefixAuthorities), Arguments.of(Named.named("Custom delimiter for JWT scopes", customDelimiterProps), - customAuthoritiesDelimiterJwt, subjectValue, defaultPrefixAuthorities), + customAuthoritiesDelimiterJwt, subjectValue, customPrefixAuthorities), Arguments.of(Named.named("Custom JWT authority claim name", customAuthoritiesClaimProps), customAuthoritiesClaimJwt, subjectValue, defaultPrefixAuthorities), Arguments.of(Named.named("Custom JWT principal claim name", customPrincipalClaimProps), diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java index 5c4e359ce424..5f67fe9f17a4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java @@ -633,6 +633,30 @@ void customValidatorWhenInvalid() throws Exception { }); } + @Test + void shouldNotConfigureJwtConverterIfNoPropertiesAreSet() { + this.contextRunner + .run((context) -> assertThat(context).doesNotHaveBean(ReactiveJwtAuthenticationConverter.class)); + } + + @Test + void shouldConfigureJwtConverterIfPrincipalClaimNameIsSet() { + this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.principal-claim-name=dummy") + .run((context) -> assertThat(context).hasSingleBean(ReactiveJwtAuthenticationConverter.class)); + } + + @Test + void shouldConfigureJwtConverterIfAuthorityPrefixIsSet() { + this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.authority-prefix=dummy") + .run((context) -> assertThat(context).hasSingleBean(ReactiveJwtAuthenticationConverter.class)); + } + + @Test + void shouldConfigureJwtConverterIfAuthorityClaimsNameIsSet() { + this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-claim-name=dummy") + .run((context) -> assertThat(context).hasSingleBean(ReactiveJwtAuthenticationConverter.class)); + } + @ParameterizedTest(name = "{0}") @ArgumentsSource(JwtConverterCustomizationsArgumentsProvider.class) void autoConfigurationShouldConfigureResourceServerWithJwtConverterCustomizations(String[] properties, Jwt jwt, diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java index 758ac7b6aa02..3c71539b81d6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java @@ -662,6 +662,29 @@ void autoConfigurationShouldConfigureResourceServerWithJwtConverterCustomization }); } + @Test + void shouldNotConfigureJwtConverterIfNoPropertiesAreSet() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(JwtAuthenticationConverter.class)); + } + + @Test + void shouldConfigureJwtConverterIfPrincipalClaimNameIsSet() { + this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.principal-claim-name=dummy") + .run((context) -> assertThat(context).hasSingleBean(JwtAuthenticationConverter.class)); + } + + @Test + void shouldConfigureJwtConverterIfAuthorityPrefixIsSet() { + this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.authority-prefix=dummy") + .run((context) -> assertThat(context).hasSingleBean(JwtAuthenticationConverter.class)); + } + + @Test + void shouldConfigureJwtConverterIfAuthorityClaimsNameIsSet() { + this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.authorities-claim-name=dummy") + .run((context) -> assertThat(context).hasSingleBean(JwtAuthenticationConverter.class)); + } + @Test void jwtAuthenticationConverterByJwtConfigIsConditionalOnMissingBean() { String propertiesPrincipalClaim = "principal_from_properties"; From ed039fcf7dff62fb7e620baf33ae76a53dc9058a Mon Sep 17 00:00:00 2001 From: Kai Zander Date: Wed, 10 Jan 2024 20:56:39 +0100 Subject: [PATCH 1024/1215] Fix context runner assertions not being executed See gh-39087 --- .../web/reactive/WebFluxAutoConfigurationTests.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java index 276944fe0de7..b6a15e1ae4d6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java @@ -628,15 +628,13 @@ void customSessionTimeoutConfigurationShouldBeApplied() { @Test void customSessionMaxSessionsConfigurationShouldBeApplied() { this.contextRunner.withPropertyValues("server.reactive.session.max-sessions:123") - .run((context) -> assertMaxSessionsWithWebSession(123)); + .run(assertMaxSessionsWithWebSession(123)); } @Test void defaultSessionMaxSessionsConfigurationShouldBeInSync() { - this.contextRunner.run((context) -> { - int defaultMaxSessions = new InMemoryWebSessionStore().getMaxSessions(); - assertMaxSessionsWithWebSession(defaultMaxSessions); - }); + int defaultMaxSessions = new InMemoryWebSessionStore().getMaxSessions(); + this.contextRunner.run(assertMaxSessionsWithWebSession(defaultMaxSessions)); } @Test From 5047048c10ad22ce65d93b7b03a8de42e427c286 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 11 Jan 2024 13:23:04 +0000 Subject: [PATCH 1025/1215] Upgrade to Reactor Bom 2023.0.2 Closes gh-38980 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 1ad6750decf7..d7c622af3515 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1327,7 +1327,7 @@ bom { ] } } - library("Reactor Bom", "2023.0.2-SNAPSHOT") { + library("Reactor Bom", "2023.0.2") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From b14c4ef6432aa7d9001563a6665218182650e26e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 11 Jan 2024 13:23:05 +0000 Subject: [PATCH 1026/1215] Upgrade to Spring Framework 6.1.3 Closes gh-38982 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index cb25a7eb5641..39b223c7fb52 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ junitJupiterVersion=5.10.1 kotlinVersion=1.9.21 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.28 -springFrameworkVersion=6.1.3-SNAPSHOT +springFrameworkVersion=6.1.3 tomcatVersion=10.1.17 kotlin.stdlib.default.dependency=false From 3acaba15fc5acc04beee6915e7f247322cf7b38d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 11 Jan 2024 13:37:05 +0000 Subject: [PATCH 1027/1215] Upgrade to Micrometer 1.12.2 Closes gh-39097 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e1708ab8cd83..70c9450cd8f2 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1000,7 +1000,7 @@ bom { ] } } - library("Micrometer", "1.12.1") { + library("Micrometer", "1.12.2") { considerSnapshots() group("io.micrometer") { modules = [ From f5be3529b4964a8e1fb9f6f87b954713557222f9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 11 Jan 2024 13:37:10 +0000 Subject: [PATCH 1028/1215] Upgrade to Micrometer Tracing 1.2.2 Closes gh-39098 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 70c9450cd8f2..12401ee9c0fe 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1013,7 +1013,7 @@ bom { ] } } - library("Micrometer Tracing", "1.2.1") { + library("Micrometer Tracing", "1.2.2") { considerSnapshots() calendarName = "Tracing" group("io.micrometer") { From 339422434f8d288f6e41643ada2bfd390dd145bb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 11 Jan 2024 13:37:11 +0000 Subject: [PATCH 1029/1215] Upgrade to Reactor Bom 2023.0.2 Closes gh-38986 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 12401ee9c0fe..0ac7be077ef5 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1328,7 +1328,7 @@ bom { ] } } - library("Reactor Bom", "2023.0.2-SNAPSHOT") { + library("Reactor Bom", "2023.0.2") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From 946b73bcaff27641bf48a5ae45a85616880afc05 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 11 Jan 2024 13:37:11 +0000 Subject: [PATCH 1030/1215] Upgrade to Spring Framework 6.1.3 Closes gh-38989 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 16eccbb3f6f8..b50c786ea445 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ junitJupiterVersion=5.10.1 kotlinVersion=1.9.22 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.28 -springFrameworkVersion=6.1.3-SNAPSHOT +springFrameworkVersion=6.1.3 tomcatVersion=10.1.17 kotlin.stdlib.default.dependency=false From 3928fac5ba1ed7441e5a88f42baa7a48a95e3111 Mon Sep 17 00:00:00 2001 From: BenchmarkingBuffalo <46448799+BenchmarkingBuffalo@users.noreply.github.com> Date: Thu, 14 Dec 2023 18:18:31 +0100 Subject: [PATCH 1031/1215] Add clientId and subscriptionDurable to JmsProperties See gh-38817 --- ...JmsListenerContainerFactoryConfigurer.java | 3 +++ .../boot/autoconfigure/jms/JmsProperties.java | 27 +++++++++++++++++++ .../jms/JmsAutoConfigurationTests.java | 6 ++++- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java index 26e511dbb9f0..100761cf3c73 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java @@ -36,6 +36,7 @@ * @author Stephane Nicoll * @author Eddú Meléndez * @author Vedran Pavic + * @author Lasse Wulff * @since 1.3.3 */ public final class DefaultJmsListenerContainerFactoryConfigurer { @@ -116,6 +117,8 @@ public void configure(DefaultJmsListenerContainerFactory factory, ConnectionFact Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); factory.setConnectionFactory(connectionFactory); factory.setPubSubDomain(this.jmsProperties.isPubSubDomain()); + factory.setSubscriptionDurable(this.jmsProperties.isSubscriptionDurable()); + factory.setClientId(this.jmsProperties.getClientId()); JmsProperties.Listener listenerProperties = this.jmsProperties.getListener(); Session sessionProperties = listenerProperties.getSession(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java index 08b7a6d5ee5e..ab5e2f37eead 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/JmsProperties.java @@ -27,6 +27,7 @@ * @author Greg Turnquist * @author Phillip Webb * @author Stephane Nicoll + * @author Lasse Wulff * @author Vedran Pavic * @since 1.0.0 */ @@ -44,6 +45,16 @@ public class JmsProperties { */ private String jndiName; + /** + * Whether the subscription is durable. + */ + private boolean subscriptionDurable = false; + + /** + * Client id of the connection. + */ + private String clientId; + private final Cache cache = new Cache(); private final Listener listener = new Listener(); @@ -58,6 +69,22 @@ public void setPubSubDomain(boolean pubSubDomain) { this.pubSubDomain = pubSubDomain; } + public boolean isSubscriptionDurable() { + return this.subscriptionDurable; + } + + public void setSubscriptionDurable(boolean subscriptionDurable) { + this.subscriptionDurable = subscriptionDurable; + } + + public String getClientId() { + return this.clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + public String getJndiName() { return this.jndiName; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java index 7ea7a0446e16..14a4b37c0cbf 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/JmsAutoConfigurationTests.java @@ -63,6 +63,7 @@ * @author Aurélien Leboulanger * @author Eddú Meléndez * @author Vedran Pavic + * @author Lasse Wulff */ class JmsAutoConfigurationTests { @@ -151,7 +152,8 @@ void testJmsListenerContainerFactoryWithCustomSettings() { .withPropertyValues("spring.jms.listener.autoStartup=false", "spring.jms.listener.session.acknowledgeMode=client", "spring.jms.listener.session.transacted=false", "spring.jms.listener.minConcurrency=2", - "spring.jms.listener.receiveTimeout=2s", "spring.jms.listener.maxConcurrency=10") + "spring.jms.listener.receiveTimeout=2s", "spring.jms.listener.maxConcurrency=10", + "spring.jms.subscription-durable=true", "spring.jms.client-id=exampleId") .run(this::testJmsListenerContainerFactoryWithCustomSettings); } @@ -163,6 +165,8 @@ private void testJmsListenerContainerFactoryWithCustomSettings(AssertableApplica assertThat(container.getConcurrentConsumers()).isEqualTo(2); assertThat(container.getMaxConcurrentConsumers()).isEqualTo(10); assertThat(container).hasFieldOrPropertyWithValue("receiveTimeout", 2000L); + assertThat(container.isSubscriptionDurable()).isTrue(); + assertThat(container.getClientId()).isEqualTo("exampleId"); } @Test From 697b2529572030eff638484b9c37f622e9b46d70 Mon Sep 17 00:00:00 2001 From: "Zhiyang.Wang1" Date: Sat, 11 Nov 2023 02:21:25 +0800 Subject: [PATCH 1032/1215] Remove deprecated support for FailureAnalyzer setter injection See gh-38322 --- .../boot/diagnostics/FailureAnalyzers.java | 38 +-------------- .../diagnostics/FailureAnalyzersTests.java | 48 +------------------ 2 files changed, 4 insertions(+), 82 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java index 7a905a8fcbe8..cf9bb3f199c6 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java @@ -22,16 +22,13 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.boot.SpringBootExceptionReporter; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.EnvironmentAware; import org.springframework.core.env.Environment; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.core.io.support.SpringFactoriesLoader.ArgumentResolver; import org.springframework.core.io.support.SpringFactoriesLoader.FailureHandler; import org.springframework.core.log.LogMessage; -import org.springframework.util.StringUtils; /** * Utility to trigger {@link FailureAnalyzer} and {@link FailureAnalysisReporter} @@ -61,39 +58,8 @@ public FailureAnalyzers(ConfigurableApplicationContext context) { FailureAnalyzers(ConfigurableApplicationContext context, SpringFactoriesLoader springFactoriesLoader) { this.springFactoriesLoader = springFactoriesLoader; - this.analyzers = loadFailureAnalyzers(context, this.springFactoriesLoader); - } - - private static List loadFailureAnalyzers(ConfigurableApplicationContext context, - SpringFactoriesLoader springFactoriesLoader) { - List analyzers = springFactoriesLoader.load(FailureAnalyzer.class, - getArgumentResolver(context), FailureHandler.logging(logger)); - List awareAnalyzers = analyzers.stream() - .filter((analyzer) -> analyzer instanceof BeanFactoryAware || analyzer instanceof EnvironmentAware) - .toList(); - if (!awareAnalyzers.isEmpty()) { - String awareAnalyzerNames = StringUtils.collectionToCommaDelimitedString( - awareAnalyzers.stream().map((analyzer) -> analyzer.getClass().getName()).toList()); - logger.warn(LogMessage.format( - "FailureAnalyzers [%s] implement BeanFactoryAware or EnvironmentAware. " - + "Support for these interfaces on FailureAnalyzers is deprecated, " - + "and will be removed in a future release. " - + "Instead provide a constructor that accepts BeanFactory or Environment parameters.", - awareAnalyzerNames)); - if (context == null) { - logger.trace(LogMessage.format("Skipping [%s] due to missing context", awareAnalyzerNames)); - return analyzers.stream().filter((analyzer) -> !awareAnalyzers.contains(analyzer)).toList(); - } - awareAnalyzers.forEach((analyzer) -> { - if (analyzer instanceof BeanFactoryAware beanFactoryAware) { - beanFactoryAware.setBeanFactory(context.getBeanFactory()); - } - if (analyzer instanceof EnvironmentAware environmentAware) { - environmentAware.setEnvironment(context.getEnvironment()); - } - }); - } - return analyzers; + this.analyzers = springFactoriesLoader.load(FailureAnalyzer.class, getArgumentResolver(context), + FailureHandler.logging(logger)); } private static ArgumentResolver getArgumentResolver(ConfigurableApplicationContext context) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/FailureAnalyzersTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/FailureAnalyzersTests.java index d1fdcd7747ed..42637ed5974b 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/FailureAnalyzersTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/FailureAnalyzersTests.java @@ -21,16 +21,13 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.testsupport.system.OutputCaptureExtension; -import org.springframework.context.EnvironmentAware; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.core.env.Environment; import org.springframework.core.test.io.support.MockSpringFactoriesLoader; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.same; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -45,20 +42,13 @@ @ExtendWith(OutputCaptureExtension.class) class FailureAnalyzersTests { - private static AwareFailureAnalyzer failureAnalyzer; + private static FailureAnalyzer failureAnalyzer; private final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); @BeforeEach void configureMock() { - failureAnalyzer = mock(AwareFailureAnalyzer.class); - } - - @Test - void analyzersAreLoadedAndCalled() { - RuntimeException failure = new RuntimeException(); - analyzeAndReport(failure, BasicFailureAnalyzer.class, StandardAwareFailureAnalyzer.class); - then(failureAnalyzer).should(times(2)).analyze(failure); + failureAnalyzer = mock(FailureAnalyzer.class); } @Test @@ -77,22 +67,6 @@ void analyzerIsConstructedWithEnvironment(CapturedOutput output) { assertThat(output).doesNotContain("implement BeanFactoryAware or EnvironmentAware"); } - @Test - void beanFactoryIsInjectedIntoBeanFactoryAwareFailureAnalyzers(CapturedOutput output) { - RuntimeException failure = new RuntimeException(); - analyzeAndReport(failure, BasicFailureAnalyzer.class, StandardAwareFailureAnalyzer.class); - then(failureAnalyzer).should().setBeanFactory(same(this.context.getBeanFactory())); - assertThat(output).contains("FailureAnalyzers [" + StandardAwareFailureAnalyzer.class.getName() - + "] implement BeanFactoryAware or EnvironmentAware."); - } - - @Test - void environmentIsInjectedIntoEnvironmentAwareFailureAnalyzers() { - RuntimeException failure = new RuntimeException(); - analyzeAndReport(failure, BasicFailureAnalyzer.class, StandardAwareFailureAnalyzer.class); - then(failureAnalyzer).should().setEnvironment(same(this.context.getEnvironment())); - } - @Test void analyzerThatFailsDuringInitializationDoesNotPreventOtherAnalyzersFromBeingCalled() { RuntimeException failure = new RuntimeException(); @@ -170,22 +144,4 @@ static class EnvironmentConstructorFailureAnalyzer extends BasicFailureAnalyzer } - interface AwareFailureAnalyzer extends BeanFactoryAware, EnvironmentAware, FailureAnalyzer { - - } - - static class StandardAwareFailureAnalyzer extends BasicFailureAnalyzer implements AwareFailureAnalyzer { - - @Override - public void setEnvironment(Environment environment) { - failureAnalyzer.setEnvironment(environment); - } - - @Override - public void setBeanFactory(BeanFactory beanFactory) { - failureAnalyzer.setBeanFactory(beanFactory); - } - - } - } From 9b8c45c35d62ddaf6334d4120c55722ce69550a0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 11 Jan 2024 14:08:03 +0000 Subject: [PATCH 1033/1215] Polish "Remove deprecated support for FailureAnalyzer setter injection" See gh-38322 --- .../boot/diagnostics/FailureAnalyzers.java | 9 +++++++-- .../boot/diagnostics/FailureAnalyzersTests.java | 16 ++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java index cf9bb3f199c6..a0e1aa7eda07 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -58,7 +58,12 @@ public FailureAnalyzers(ConfigurableApplicationContext context) { FailureAnalyzers(ConfigurableApplicationContext context, SpringFactoriesLoader springFactoriesLoader) { this.springFactoriesLoader = springFactoriesLoader; - this.analyzers = springFactoriesLoader.load(FailureAnalyzer.class, getArgumentResolver(context), + this.analyzers = loadFailureAnalyzers(context, this.springFactoriesLoader); + } + + private static List loadFailureAnalyzers(ConfigurableApplicationContext context, + SpringFactoriesLoader springFactoriesLoader) { + return springFactoriesLoader.load(FailureAnalyzer.class, getArgumentResolver(context), FailureHandler.logging(logger)); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/FailureAnalyzersTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/FailureAnalyzersTests.java index 42637ed5974b..0ed9267e6ee0 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/FailureAnalyzersTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/FailureAnalyzersTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -21,7 +21,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.BeanFactory; -import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.testsupport.system.OutputCaptureExtension; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.core.env.Environment; @@ -52,19 +51,24 @@ void configureMock() { } @Test - void analyzerIsConstructedWithBeanFactory(CapturedOutput output) { + void analyzersAreLoadedAndCalled() { + RuntimeException failure = new RuntimeException(); + analyzeAndReport(failure, BasicFailureAnalyzer.class, BasicFailureAnalyzer.class); + then(failureAnalyzer).should(times(2)).analyze(failure); + } + + @Test + void analyzerIsConstructedWithBeanFactory() { RuntimeException failure = new RuntimeException(); analyzeAndReport(failure, BasicFailureAnalyzer.class, BeanFactoryConstructorFailureAnalyzer.class); then(failureAnalyzer).should(times(2)).analyze(failure); - assertThat(output).doesNotContain("implement BeanFactoryAware or EnvironmentAware"); } @Test - void analyzerIsConstructedWithEnvironment(CapturedOutput output) { + void analyzerIsConstructedWithEnvironment() { RuntimeException failure = new RuntimeException(); analyzeAndReport(failure, BasicFailureAnalyzer.class, EnvironmentConstructorFailureAnalyzer.class); then(failureAnalyzer).should(times(2)).analyze(failure); - assertThat(output).doesNotContain("implement BeanFactoryAware or EnvironmentAware"); } @Test From c6c7fbc15fcff40aa1b9fd93e9f62d9bc9d74889 Mon Sep 17 00:00:00 2001 From: teacmity Date: Thu, 17 Aug 2023 12:52:15 +0000 Subject: [PATCH 1034/1215] Change log messages to use singular or plural instead of "noun(s)" See gh-37017 --- .../boot/build/bom/bomr/UpgradeDependencies.java | 3 ++- .../actuate/endpoint/web/EndpointLinksResolver.java | 4 +++- .../freemarker/FreeMarkerAutoConfiguration.java | 5 +++-- .../boot/maven/RunIntegrationTests.java | 2 +- .../springframework/boot/maven/AbstractRunMojo.java | 12 ++++++++---- .../boot/web/embedded/jetty/GracefulShutdown.java | 3 ++- 6 files changed, 19 insertions(+), 10 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeDependencies.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeDependencies.java index 81511606b710..264d4902d563 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeDependencies.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeDependencies.java @@ -169,8 +169,9 @@ private List verifyLabels(GitHubRepository repository) { if (!availableLabels.containsAll(issueLabels)) { List unknownLabels = new ArrayList<>(issueLabels); unknownLabels.removeAll(availableLabels); + String suffix = (unknownLabels.size() == 1) ? "" : "s"; throw new InvalidUserDataException( - "Unknown label(s): " + StringUtils.collectionToCommaDelimitedString(unknownLabels)); + "Unknown label" + suffix + ": " + StringUtils.collectionToCommaDelimitedString(unknownLabels)); } return issueLabels; } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointLinksResolver.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointLinksResolver.java index cba80cde53d6..05dcd99c6163 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointLinksResolver.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/EndpointLinksResolver.java @@ -55,7 +55,9 @@ public EndpointLinksResolver(Collection> endpoint public EndpointLinksResolver(Collection> endpoints, String basePath) { this.endpoints = endpoints; if (logger.isInfoEnabled()) { - logger.info("Exposing " + endpoints.size() + " endpoint(s) beneath base path '" + basePath + "'"); + String suffix = (endpoints.size() == 1) ? "" : "s"; + logger + .info("Exposing " + endpoints.size() + " endpoint" + suffix + " beneath base path '" + basePath + "'"); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerAutoConfiguration.java index 254d8d7d4a2b..673acb1f5c8d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerAutoConfiguration.java @@ -62,8 +62,9 @@ public void checkTemplateLocationExists() { if (logger.isWarnEnabled() && this.properties.isCheckTemplateLocation()) { List locations = getLocations(); if (locations.stream().noneMatch(this::locationExists)) { - logger.warn("Cannot find template location(s): " + locations + " (please add some templates, " - + "check your FreeMarker configuration, or set " + String suffix = (locations.size() == 1) ? "" : "s"; + logger.warn("Cannot find template location" + suffix + ": " + locations + + " (please add some templates, " + "check your FreeMarker configuration, or set " + "spring.freemarker.check-template-location=false)"); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java index 12382d58ef45..65a8cc680153 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java @@ -38,7 +38,7 @@ class RunIntegrationTests { @TestTemplate void whenTheRunGoalIsExecutedTheApplicationIsForkedWithOptimizedJvmArguments(MavenBuild mavenBuild) { mavenBuild.project("run").goals("spring-boot:run", "-X").execute((project) -> { - String jvmArguments = "JVM argument(s): -XX:TieredStopAtLevel=1"; + String jvmArguments = "JVM argument: -XX:TieredStopAtLevel=1"; assertThat(buildLog(project)).contains("I haz been run").contains(jvmArguments); }); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java index c4dfd77162af..440825206126 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java @@ -277,12 +277,14 @@ protected EnvVariables resolveEnvVariables() { private void addArgs(List args) { RunArguments applicationArguments = resolveApplicationArguments(); Collections.addAll(args, applicationArguments.asArray()); - logArguments("Application argument(s): ", applicationArguments.asArray()); + String suffix = (applicationArguments.asArray().length == 1) ? "" : "s"; + logArguments("Application argument" + suffix + ": ", applicationArguments.asArray()); } private Map determineEnvironmentVariables() { EnvVariables envVariables = resolveEnvVariables(); - logArguments("Environment variable(s): ", envVariables.asArray()); + String suffix = (envVariables.asArray().length == 1) ? "" : "s"; + logArguments("Environment variable" + suffix + ": ", envVariables.asArray()); return envVariables.asMap(); } @@ -307,7 +309,8 @@ protected RunArguments resolveJvmArguments() { private void addJvmArgs(List args) { RunArguments jvmArguments = resolveJvmArguments(); Collections.addAll(args, jvmArguments.asArray()); - logArguments("JVM argument(s): ", jvmArguments.asArray()); + String suffix = (jvmArguments.asArray().length == 1) ? "" : "s"; + logArguments("JVM argument" + suffix + ": ", jvmArguments.asArray()); } private void addAgents(List args) { @@ -334,7 +337,8 @@ private void addActiveProfileArgument(RunArguments arguments) { } } arguments.getArgs().addFirst(arg.toString()); - logArguments("Active profile(s): ", this.profiles); + String suffix = (this.profiles.length == 1) ? "" : "s"; + logArguments("Active profile" + suffix + ": ", this.profiles); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/GracefulShutdown.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/GracefulShutdown.java index 8ccd5d26e56c..c0f1306c16e2 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/GracefulShutdown.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/GracefulShutdown.java @@ -107,7 +107,8 @@ private void awaitShutdown(GracefulShutdownCallback callback) { callback.shutdownComplete(GracefulShutdownResult.IDLE); } else { - logger.info(LogMessage.format("Graceful shutdown aborted with %d request(s) still active", activeRequests)); + logger.info(LogMessage.format("Graceful shutdown aborted with %d request%s still active", activeRequests, + (activeRequests == 1) ? "" : "s")); callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE); } } From 6f4a8cc0c3c0354ddf5a0806fc46d6d945def0f0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 11 Jan 2024 14:44:07 +0000 Subject: [PATCH 1035/1215] Polish "Change log messages to use singular or plural instead of "noun(s)"" See gh-37017 --- .../boot/maven/AbstractRunMojo.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java index 440825206126..9ed48bcbe517 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java @@ -277,14 +277,12 @@ protected EnvVariables resolveEnvVariables() { private void addArgs(List args) { RunArguments applicationArguments = resolveApplicationArguments(); Collections.addAll(args, applicationArguments.asArray()); - String suffix = (applicationArguments.asArray().length == 1) ? "" : "s"; - logArguments("Application argument" + suffix + ": ", applicationArguments.asArray()); + logArguments("Application argument", applicationArguments.asArray()); } private Map determineEnvironmentVariables() { EnvVariables envVariables = resolveEnvVariables(); - String suffix = (envVariables.asArray().length == 1) ? "" : "s"; - logArguments("Environment variable" + suffix + ": ", envVariables.asArray()); + logArguments("Environment variable", envVariables.asArray()); return envVariables.asMap(); } @@ -309,8 +307,7 @@ protected RunArguments resolveJvmArguments() { private void addJvmArgs(List args) { RunArguments jvmArguments = resolveJvmArguments(); Collections.addAll(args, jvmArguments.asArray()); - String suffix = (jvmArguments.asArray().length == 1) ? "" : "s"; - logArguments("JVM argument" + suffix + ": ", jvmArguments.asArray()); + logArguments("JVM argument", jvmArguments.asArray()); } private void addAgents(List args) { @@ -337,8 +334,7 @@ private void addActiveProfileArgument(RunArguments arguments) { } } arguments.getArgs().addFirst(arg.toString()); - String suffix = (this.profiles.length == 1) ? "" : "s"; - logArguments("Active profile" + suffix + ": ", this.profiles); + logArguments("Active profile", this.profiles); } } @@ -417,8 +413,9 @@ private void addDependencies(List urls) throws MalformedURLException, MojoE } } - private void logArguments(String message, String[] args) { + private void logArguments(String name, String[] args) { if (getLog().isDebugEnabled()) { + String message = (args.length == 1) ? name + ": " : name + "s: "; getLog().debug(Arrays.stream(args).collect(Collectors.joining(" ", message, ""))); } } From cb3745ff9212e2e693c8c28ededa69b9a281c5a7 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 11 Jan 2024 16:15:42 +0100 Subject: [PATCH 1036/1215] Restore configuration property for http requests names when using WebFlux Closes gh-39083 --- .../WebFluxObservationAutoConfiguration.java | 24 +++++++++++++--- ...FluxObservationAutoConfigurationTests.java | 28 ++++++++++++++++++- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java index 0ccb94d55b5c..94d125f63f2a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -30,10 +30,13 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.core.annotation.Order; +import org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention; +import org.springframework.http.server.reactive.observation.ServerRequestObservationConvention; /** * {@link EnableAutoConfiguration Auto-configuration} for instrumentation of Spring @@ -42,6 +45,7 @@ * @author Brian Clozel * @author Jon Schneider * @author Dmytro Nosan + * @author Moritz Halbritter * @since 3.0.0 */ @AutoConfiguration(after = { SimpleMetricsExportAutoConfiguration.class, ObservationAutoConfiguration.class }) @@ -51,15 +55,27 @@ @EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class }) public class WebFluxObservationAutoConfiguration { + private final ObservationProperties observationProperties; + + WebFluxObservationAutoConfiguration(ObservationProperties observationProperties) { + this.observationProperties = observationProperties; + } + @Bean @Order(0) - MeterFilter metricsHttpServerUriTagFilter(MetricsProperties metricsProperties, - ObservationProperties observationProperties) { - String name = observationProperties.getHttp().getServer().getRequests().getName(); + MeterFilter metricsHttpServerUriTagFilter(MetricsProperties metricsProperties) { + String name = this.observationProperties.getHttp().getServer().getRequests().getName(); MeterFilter filter = new OnlyOnceLoggingDenyMeterFilter( () -> "Reached the maximum number of URI tags for '%s'.".formatted(name)); return MeterFilter.maximumAllowableTags(name, "uri", metricsProperties.getWeb().getServer().getMaxUriTags(), filter); } + @Bean + @ConditionalOnMissingBean(ServerRequestObservationConvention.class) + DefaultServerRequestObservationConvention defaultServerRequestObservationConvention() { + return new DefaultServerRequestObservationConvention( + this.observationProperties.getHttp().getServer().getRequests().getName()); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java index b5e05e99ea0a..384be8d4ae30 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -33,8 +33,11 @@ import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention; +import org.springframework.http.server.reactive.observation.ServerRequestObservationConvention; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * Tests for {@link WebFluxObservationAutoConfiguration} @@ -42,6 +45,7 @@ * @author Brian Clozel * @author Dmytro Nosan * @author Madhura Bhave + * @author Moritz Halbritter */ @ExtendWith(OutputCaptureExtension.class) class WebFluxObservationAutoConfigurationTests { @@ -91,6 +95,28 @@ void shouldNotDenyNorLogIfMaxUrisIsNotReached(CapturedOutput output) { }); } + @Test + void shouldSupplyDefaultServerRequestObservationConvention() { + this.contextRunner.withPropertyValues("management.observations.http.server.requests.name=some-other-name") + .run((context) -> { + assertThat(context).hasSingleBean(DefaultServerRequestObservationConvention.class); + DefaultServerRequestObservationConvention bean = context + .getBean(DefaultServerRequestObservationConvention.class); + assertThat(bean.getName()).isEqualTo("some-other-name"); + }); + } + + @Test + void shouldBackOffOnCustomServerRequestObservationConvention() { + this.contextRunner + .withBean("customServerRequestObservationConvention", ServerRequestObservationConvention.class, + () -> mock(ServerRequestObservationConvention.class)) + .run((context) -> { + assertThat(context).hasBean("customServerRequestObservationConvention"); + assertThat(context).hasSingleBean(ServerRequestObservationConvention.class); + }); + } + private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicationContext context) { return getInitializedMeterRegistry(context, "http.server.requests"); } From a424ba2055d7026e7066f272b8ddd1b4b4175cdb Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 11 Jan 2024 16:15:42 +0100 Subject: [PATCH 1037/1215] Restore configuration property for http requests names when using WebFlux Closes gh-39083 --- .../WebFluxObservationAutoConfiguration.java | 24 +++++++++++++--- ...FluxObservationAutoConfigurationTests.java | 28 ++++++++++++++++++- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java index 0ccb94d55b5c..94d125f63f2a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -30,10 +30,13 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.core.annotation.Order; +import org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention; +import org.springframework.http.server.reactive.observation.ServerRequestObservationConvention; /** * {@link EnableAutoConfiguration Auto-configuration} for instrumentation of Spring @@ -42,6 +45,7 @@ * @author Brian Clozel * @author Jon Schneider * @author Dmytro Nosan + * @author Moritz Halbritter * @since 3.0.0 */ @AutoConfiguration(after = { SimpleMetricsExportAutoConfiguration.class, ObservationAutoConfiguration.class }) @@ -51,15 +55,27 @@ @EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class }) public class WebFluxObservationAutoConfiguration { + private final ObservationProperties observationProperties; + + WebFluxObservationAutoConfiguration(ObservationProperties observationProperties) { + this.observationProperties = observationProperties; + } + @Bean @Order(0) - MeterFilter metricsHttpServerUriTagFilter(MetricsProperties metricsProperties, - ObservationProperties observationProperties) { - String name = observationProperties.getHttp().getServer().getRequests().getName(); + MeterFilter metricsHttpServerUriTagFilter(MetricsProperties metricsProperties) { + String name = this.observationProperties.getHttp().getServer().getRequests().getName(); MeterFilter filter = new OnlyOnceLoggingDenyMeterFilter( () -> "Reached the maximum number of URI tags for '%s'.".formatted(name)); return MeterFilter.maximumAllowableTags(name, "uri", metricsProperties.getWeb().getServer().getMaxUriTags(), filter); } + @Bean + @ConditionalOnMissingBean(ServerRequestObservationConvention.class) + DefaultServerRequestObservationConvention defaultServerRequestObservationConvention() { + return new DefaultServerRequestObservationConvention( + this.observationProperties.getHttp().getServer().getRequests().getName()); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java index b5e05e99ea0a..384be8d4ae30 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -33,8 +33,11 @@ import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention; +import org.springframework.http.server.reactive.observation.ServerRequestObservationConvention; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * Tests for {@link WebFluxObservationAutoConfiguration} @@ -42,6 +45,7 @@ * @author Brian Clozel * @author Dmytro Nosan * @author Madhura Bhave + * @author Moritz Halbritter */ @ExtendWith(OutputCaptureExtension.class) class WebFluxObservationAutoConfigurationTests { @@ -91,6 +95,28 @@ void shouldNotDenyNorLogIfMaxUrisIsNotReached(CapturedOutput output) { }); } + @Test + void shouldSupplyDefaultServerRequestObservationConvention() { + this.contextRunner.withPropertyValues("management.observations.http.server.requests.name=some-other-name") + .run((context) -> { + assertThat(context).hasSingleBean(DefaultServerRequestObservationConvention.class); + DefaultServerRequestObservationConvention bean = context + .getBean(DefaultServerRequestObservationConvention.class); + assertThat(bean.getName()).isEqualTo("some-other-name"); + }); + } + + @Test + void shouldBackOffOnCustomServerRequestObservationConvention() { + this.contextRunner + .withBean("customServerRequestObservationConvention", ServerRequestObservationConvention.class, + () -> mock(ServerRequestObservationConvention.class)) + .run((context) -> { + assertThat(context).hasBean("customServerRequestObservationConvention"); + assertThat(context).hasSingleBean(ServerRequestObservationConvention.class); + }); + } + private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicationContext context) { return getInitializedMeterRegistry(context, "http.server.requests"); } From 653474fc4628caeee10270c0f2b1baa323e6cb10 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 11 Jan 2024 21:40:18 -0800 Subject: [PATCH 1038/1215] Polish --- ...crometerTracingAutoConfigurationTests.java | 2 +- ...JmsListenerContainerFactoryConfigurer.java | 8 ++-- .../pulsar/PulsarPropertiesMapper.java | 44 +++++++++++-------- .../autoconfigure/web/ServerProperties.java | 2 +- 4 files changed, 31 insertions(+), 25 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java index 727b91174df8..324683ee78ab 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java @@ -233,7 +233,7 @@ private static final class SpanTagAnnotationHandlerConfiguration { @Bean SpanTagAnnotationHandler spanTagAnnotationHandler() { - return new SpanTagAnnotationHandler((aClass) -> null, (aClass) -> null); + return new SpanTagAnnotationHandler((valueResolverClass) -> null, (valueExpressionResolverClass) -> null); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java index 100761cf3c73..43fbf4dc7a44 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/DefaultJmsListenerContainerFactoryConfigurer.java @@ -115,13 +115,13 @@ public void setObservationRegistry(ObservationRegistry observationRegistry) { public void configure(DefaultJmsListenerContainerFactory factory, ConnectionFactory connectionFactory) { Assert.notNull(factory, "Factory must not be null"); Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); - factory.setConnectionFactory(connectionFactory); - factory.setPubSubDomain(this.jmsProperties.isPubSubDomain()); - factory.setSubscriptionDurable(this.jmsProperties.isSubscriptionDurable()); - factory.setClientId(this.jmsProperties.getClientId()); JmsProperties.Listener listenerProperties = this.jmsProperties.getListener(); Session sessionProperties = listenerProperties.getSession(); + factory.setConnectionFactory(connectionFactory); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(this.jmsProperties::isPubSubDomain).to(factory::setPubSubDomain); + map.from(this.jmsProperties::isSubscriptionDurable).to(factory::setSubscriptionDurable); + map.from(this.jmsProperties::getClientId).to(factory::setClientId); map.from(this.transactionManager).to(factory::setTransactionManager); map.from(this.destinationResolver).to(factory::setDestinationResolver); map.from(this.messageConverter).to(factory::setMessageConverter); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java index 31755501b5ad..ade74e51b1bc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java @@ -64,7 +64,7 @@ void customizeClientBuilder(ClientBuilder clientBuilder, PulsarConnectionDetails map.from(properties::getConnectionTimeout).to(timeoutProperty(clientBuilder::connectionTimeout)); map.from(properties::getOperationTimeout).to(timeoutProperty(clientBuilder::operationTimeout)); map.from(properties::getLookupTimeout).to(timeoutProperty(clientBuilder::lookupTimeout)); - customizeAuthentication(clientBuilder::authentication, properties.getAuthentication()); + customizeAuthentication(properties.getAuthentication(), clientBuilder::authentication); customizeServiceUrlProviderBuilder(clientBuilder::serviceUrl, clientBuilder::serviceUrlProvider, properties, connectionDetails); } @@ -77,21 +77,11 @@ private void customizeServiceUrlProviderBuilder(Consumer serviceUrlConsu serviceUrlConsumer.accept(connectionDetails.getBrokerUrl()); return; } - Map secondaryAuths = new LinkedHashMap<>(); - failoverProperties.getBackupClusters().forEach((cluster) -> { - PulsarProperties.Authentication authentication = cluster.getAuthentication(); - if (authentication.getPluginClassName() == null) { - secondaryAuths.put(cluster.getServiceUrl(), null); - } - else { - customizeAuthentication((authPluginClassName, authParams) -> secondaryAuths.put(cluster.getServiceUrl(), - AuthenticationFactory.create(authPluginClassName, authParams)), authentication); - } - }); + Map secondaryAuths = getSecondaryAuths(failoverProperties); AutoClusterFailoverBuilder autoClusterFailoverBuilder = new AutoClusterFailoverBuilderImpl(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(connectionDetails::getBrokerUrl).to(autoClusterFailoverBuilder::primary); - map.from(new ArrayList<>(secondaryAuths.keySet())).to(autoClusterFailoverBuilder::secondary); + map.from(secondaryAuths::keySet).as(ArrayList::new).to(autoClusterFailoverBuilder::secondary); map.from(failoverProperties::getFailoverPolicy).to(autoClusterFailoverBuilder::failoverPolicy); map.from(failoverProperties::getFailOverDelay).to(timeoutProperty(autoClusterFailoverBuilder::failoverDelay)); map.from(failoverProperties::getSwitchBackDelay) @@ -101,6 +91,23 @@ private void customizeServiceUrlProviderBuilder(Consumer serviceUrlConsu serviceUrlProviderConsumer.accept(autoClusterFailoverBuilder.build()); } + private Map getSecondaryAuths(PulsarProperties.Failover properties) { + Map secondaryAuths = new LinkedHashMap<>(); + properties.getBackupClusters().forEach((backupCluster) -> { + PulsarProperties.Authentication authenticationProperties = backupCluster.getAuthentication(); + if (authenticationProperties.getPluginClassName() == null) { + secondaryAuths.put(backupCluster.getServiceUrl(), null); + } + else { + customizeAuthentication(authenticationProperties, (authPluginClassName, authParams) -> { + Authentication authentication = AuthenticationFactory.create(authPluginClassName, authParams); + secondaryAuths.put(backupCluster.getServiceUrl(), authentication); + }); + } + }); + return secondaryAuths; + } + void customizeAdminBuilder(PulsarAdminBuilder adminBuilder, PulsarConnectionDetails connectionDetails) { PulsarProperties.Admin properties = this.properties.getAdmin(); PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); @@ -108,15 +115,14 @@ void customizeAdminBuilder(PulsarAdminBuilder adminBuilder, PulsarConnectionDeta map.from(properties::getConnectionTimeout).to(timeoutProperty(adminBuilder::connectionTimeout)); map.from(properties::getReadTimeout).to(timeoutProperty(adminBuilder::readTimeout)); map.from(properties::getRequestTimeout).to(timeoutProperty(adminBuilder::requestTimeout)); - customizeAuthentication(adminBuilder::authentication, properties.getAuthentication()); + customizeAuthentication(properties.getAuthentication(), adminBuilder::authentication); } - private void customizeAuthentication(AuthenticationConsumer authentication, - PulsarProperties.Authentication properties) { - if (StringUtils.hasText(properties.getPluginClassName())) { + private void customizeAuthentication(PulsarProperties.Authentication properties, AuthenticationConsumer action) { + String pluginClassName = properties.getPluginClassName(); + if (StringUtils.hasText(pluginClassName)) { try { - authentication.accept(properties.getPluginClassName(), - getAuthenticationParamsJson(properties.getParam())); + action.accept(pluginClassName, getAuthenticationParamsJson(properties.getParam())); } catch (UnsupportedAuthenticationException ex) { throw new IllegalStateException("Unable to configure Pulsar authentication", ex); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index 3e6d21c95c38..d0be8c72f65d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -332,7 +332,7 @@ public static class Session { /** * The maximum number of sessions that can be stored. */ - private int maxSessions = 10_000; + private int maxSessions = 10000; @NestedConfigurationProperty private final Cookie cookie = new Cookie(); From cff1b33f8e30492aea5a14ab959752e96b7c3e16 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 12 Jan 2024 09:01:23 +0100 Subject: [PATCH 1039/1215] Configure virtual threads on Undertow if enabled Closes gh-38819 --- ...verFactoryCustomizerAutoConfiguration.java | 11 +++- .../UndertowWebServerFactoryCustomizer.java | 6 +- ...erFactoryCustomizerConfigurationTests.java | 57 +++++++++++++++++++ 3 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizerConfigurationTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java index 371ab5034526..aef7f8036163 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/EmbeddedWebServerFactoryCustomizerAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -34,15 +34,18 @@ import org.springframework.boot.autoconfigure.thread.Threading; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.embedded.undertow.UndertowDeploymentInfoCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; +import org.springframework.core.task.VirtualThreadTaskExecutor; /** * {@link EnableAutoConfiguration Auto-configuration} for embedded servlet and reactive * web servers customizations. * * @author Phillip Webb + * @author Moritz Halbritter * @since 2.0.0 */ @AutoConfiguration @@ -107,6 +110,12 @@ public UndertowWebServerFactoryCustomizer undertowWebServerFactoryCustomizer(Env return new UndertowWebServerFactoryCustomizer(environment, serverProperties); } + @Bean + @ConditionalOnThreading(Threading.VIRTUAL) + UndertowDeploymentInfoCustomizer virtualThreadsUndertowDeploymentInfoCustomizer() { + return (deploymentInfo) -> deploymentInfo.setExecutor(new VirtualThreadTaskExecutor("undertow-")); + } + } /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizer.java index d5d861aad7c6..c71f67929f51 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -77,8 +77,7 @@ public int getOrder() { public void customize(ConfigurableUndertowWebServerFactory factory) { PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); ServerOptions options = new ServerOptions(factory); - ServerProperties properties = this.serverProperties; - map.from(properties::getMaxHttpRequestHeaderSize) + map.from(this.serverProperties::getMaxHttpRequestHeaderSize) .asInt(DataSize::toBytes) .when(this::isPositive) .to(options.option(UndertowOptions.MAX_HEADER_SIZE)); @@ -164,6 +163,7 @@ private abstract static class AbstractOptions { lookup.put(getCanonicalName(field.getName()), option); } catch (IllegalAccessException ex) { + // Ignore } } }); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizerConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizerConfigurationTests.java new file mode 100644 index 000000000000..927462ecfb75 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/UndertowWebServerFactoryCustomizerConfigurationTests.java @@ -0,0 +1,57 @@ +/* + * 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.web.embedded; + +import io.undertow.servlet.api.DeploymentInfo; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration.UndertowWebServerFactoryCustomizerConfiguration; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.web.embedded.undertow.UndertowDeploymentInfoCustomizer; +import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebApplicationContext; +import org.springframework.core.task.VirtualThreadTaskExecutor; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link UndertowWebServerFactoryCustomizerConfiguration}. + * + * @author Moritz Halbritter + */ +class UndertowWebServerFactoryCustomizerConfigurationTests { + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner( + AnnotationConfigServletWebApplicationContext::new) + .withConfiguration(AutoConfigurations.of(EmbeddedWebServerFactoryCustomizerAutoConfiguration.class)); + + @EnabledForJreRange(min = JRE.JAVA_21) + @Test + void shouldUseVirtualThreadsIfEnabled() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true").run((context) -> { + assertThat(context).hasSingleBean(UndertowDeploymentInfoCustomizer.class); + assertThat(context).hasBean("virtualThreadsUndertowDeploymentInfoCustomizer"); + UndertowDeploymentInfoCustomizer customizer = context.getBean(UndertowDeploymentInfoCustomizer.class); + DeploymentInfo deploymentInfo = new DeploymentInfo(); + customizer.customize(deploymentInfo); + assertThat(deploymentInfo.getExecutor()).isInstanceOf(VirtualThreadTaskExecutor.class); + }); + } + +} From a48e2d35398b587028c2341876a36ecdf37c1703 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 12 Jan 2024 11:09:29 +0000 Subject: [PATCH 1040/1215] Fix configuration property conversion for CharSequence inputs Closes gh-39051 --- ...opertiesCharSequenceToObjectConverter.java | 106 ++++++++++++++++ .../properties/ConversionServiceDeducer.java | 4 +- ...iesCharSequenceToObjectConverterTests.java | 116 ++++++++++++++++++ .../ConfigurationPropertiesTests.java | 38 +++++- .../ConversionServiceDeducerTests.java | 16 +++ .../convert/ConversionServiceArguments.java | 12 +- .../boot/convert/ConversionServiceTest.java | 4 +- 7 files changed, 286 insertions(+), 10 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesCharSequenceToObjectConverter.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesCharSequenceToObjectConverterTests.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesCharSequenceToObjectConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesCharSequenceToObjectConverter.java new file mode 100644 index 000000000000..c97e882102dd --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesCharSequenceToObjectConverter.java @@ -0,0 +1,106 @@ +/* + * 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.context.properties; + +import java.util.Collections; +import java.util.Set; + +import org.springframework.boot.convert.ApplicationConversionService; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; + +/** + * Copy of package-private + * {@code org.springframework.boot.convert.CharSequenceToObjectConverter}, renamed for + * differentiation. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +final class ConfigurationPropertiesCharSequenceToObjectConverter implements ConditionalGenericConverter { + + private static final TypeDescriptor STRING = TypeDescriptor.valueOf(String.class); + + private static final TypeDescriptor BYTE_ARRAY = TypeDescriptor.valueOf(byte[].class); + + private static final Set TYPES; + + private final ThreadLocal disable = new ThreadLocal<>(); + + static { + TYPES = Collections.singleton(new ConvertiblePair(CharSequence.class, Object.class)); + } + + private final ConversionService conversionService; + + ConfigurationPropertiesCharSequenceToObjectConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Override + public Set getConvertibleTypes() { + return TYPES; + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + if (sourceType.getType() == String.class || this.disable.get() == Boolean.TRUE) { + return false; + } + this.disable.set(Boolean.TRUE); + try { + boolean canDirectlyConvertCharSequence = this.conversionService.canConvert(sourceType, targetType); + if (canDirectlyConvertCharSequence && !isStringConversionBetter(sourceType, targetType)) { + return false; + } + return this.conversionService.canConvert(STRING, targetType); + } + finally { + this.disable.remove(); + } + } + + /** + * Return if String based conversion is better based on the target type. This is + * required when ObjectTo... conversion produces incorrect results. + * @param sourceType the source type to test + * @param targetType the target type to test + * @return if string conversion is better + */ + private boolean isStringConversionBetter(TypeDescriptor sourceType, TypeDescriptor targetType) { + if (this.conversionService instanceof ApplicationConversionService applicationConversionService) { + if (applicationConversionService.isConvertViaObjectSourceType(sourceType, targetType)) { + // If an ObjectTo... converter is being used then there might be a + // better StringTo... version + return true; + } + } + if ((targetType.isArray() || targetType.isCollection()) && !targetType.equals(BYTE_ARRAY)) { + // StringToArrayConverter / StringToCollectionConverter are better than + // ObjectToArrayConverter / ObjectToCollectionConverter + return true; + } + return false; + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return this.conversionService.convert(source.toString(), STRING, targetType); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java index a80535000f6f..3f29066f03ce 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -65,6 +65,8 @@ private List getConversionServices(ConfigurableApplicationCon if (!converterBeans.isEmpty()) { FormattingConversionService beansConverterService = new FormattingConversionService(); DefaultConversionService.addCollectionConverters(beansConverterService); + beansConverterService + .addConverter(new ConfigurationPropertiesCharSequenceToObjectConverter(beansConverterService)); converterBeans.addTo(beansConverterService); conversionServices.add(beansConverterService); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesCharSequenceToObjectConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesCharSequenceToObjectConverterTests.java new file mode 100644 index 000000000000..3a81830df698 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesCharSequenceToObjectConverterTests.java @@ -0,0 +1,116 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.context.properties; + +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.provider.Arguments; + +import org.springframework.boot.convert.ApplicationConversionService; +import org.springframework.boot.convert.ConversionServiceArguments; +import org.springframework.boot.convert.ConversionServiceTest; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.format.support.FormattingConversionService; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConfigurationPropertiesCharSequenceToObjectConverter} + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +class ConfigurationPropertiesCharSequenceToObjectConverterTests { + + @ConversionServiceTest + void convertWhenCanConvertViaToString(ConversionService conversionService) { + assertThat(conversionService.convert(new StringBuilder("1"), Integer.class)).isOne(); + } + + @ConversionServiceTest + void convertWhenCanConvertDirectlySkipsStringConversion(ConversionService conversionService) { + assertThat(conversionService.convert(new String("1"), Long.class)).isOne(); + if (!ConversionServiceArguments.isApplicationConversionService(conversionService)) { + assertThat(conversionService.convert(new StringBuilder("1"), Long.class)).isEqualTo(2); + } + } + + @Test + @SuppressWarnings("unchecked") + void convertWhenTargetIsList() { + ConversionService conversionService = new ApplicationConversionService(); + StringBuilder source = new StringBuilder("1,2,3"); + TypeDescriptor sourceType = TypeDescriptor.valueOf(StringBuilder.class); + TypeDescriptor targetType = TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)); + List converted = (List) conversionService.convert(source, sourceType, targetType); + assertThat(converted).containsExactly("1", "2", "3"); + } + + @Test + @SuppressWarnings("unchecked") + void convertWhenTargetIsListAndNotUsingApplicationConversionService() { + FormattingConversionService conversionService = new DefaultFormattingConversionService(); + conversionService.addConverter(new ConfigurationPropertiesCharSequenceToObjectConverter(conversionService)); + StringBuilder source = new StringBuilder("1,2,3"); + TypeDescriptor sourceType = TypeDescriptor.valueOf(StringBuilder.class); + TypeDescriptor targetType = TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)); + List converted = (List) conversionService.convert(source, sourceType, targetType); + assertThat(converted).containsExactly("1", "2", "3"); + } + + static Stream conversionServices() { + return ConversionServiceArguments.with((conversionService) -> { + conversionService.addConverter(new StringToIntegerConverter()); + conversionService.addConverter(new StringToLongConverter()); + conversionService.addConverter(new CharSequenceToLongConverter()); + conversionService.addConverter(new ConfigurationPropertiesCharSequenceToObjectConverter(conversionService)); + }); + } + + static class StringToIntegerConverter implements Converter { + + @Override + public Integer convert(String source) { + return Integer.valueOf(source); + } + + } + + static class StringToLongConverter implements Converter { + + @Override + public Long convert(String source) { + return Long.valueOf(source); + } + + } + + static class CharSequenceToLongConverter implements Converter { + + @Override + public Long convert(CharSequence source) { + return Long.valueOf(source.toString()) + 1; + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java index 15488c4fdb69..bebaa9ae3703 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -83,6 +83,7 @@ import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertySource; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.env.SystemEnvironmentPropertySource; import org.springframework.core.io.ClassPathResource; @@ -653,6 +654,41 @@ void loadShouldUseConverterBean() { assertThat(person.lastName).isEqualTo("Smith"); } + @Test + void loadShouldUseStringConverterBeanWhenValueIsCharSequence() { + this.context.register(PersonConverterConfiguration.class, PersonProperties.class); + PropertySource testProperties = new MapPropertySource("test", Map.of("test.person", new CharSequence() { + + private final String value = "John Smith"; + + @Override + public int length() { + return this.value.length(); + } + + @Override + public char charAt(int index) { + return this.value.charAt(index); + } + + @Override + public CharSequence subSequence(int start, int end) { + return this.value.subSequence(start, end); + } + + @Override + public String toString() { + return this.value; + } + + })); + this.context.getEnvironment().getPropertySources().addLast(testProperties); + this.context.refresh(); + Person person = this.context.getBean(PersonProperties.class).getPerson(); + assertThat(person.firstName).isEqualTo("John"); + assertThat(person.lastName).isEqualTo("Smith"); + } + @Test void loadWhenBeanFactoryConversionServiceAndConverterBeanCanUseBeanFactoryConverter() { DefaultConversionService conversionService = new DefaultConversionService(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConversionServiceDeducerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConversionServiceDeducerTests.java index eed7608024a4..0027cb1d7cfe 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConversionServiceDeducerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConversionServiceDeducerTests.java @@ -78,6 +78,7 @@ void getConversionServiceWhenHasQualifiedConverterBeansContainsCustomizedFormatt assertThat(conversionServices).hasSize(2); assertThat(conversionServices.get(0)).isExactlyInstanceOf(FormattingConversionService.class); assertThat(conversionServices.get(0).canConvert(InputStream.class, OutputStream.class)).isTrue(); + assertThat(conversionServices.get(0).canConvert(CharSequence.class, InputStream.class)).isTrue(); assertThat(conversionServices.get(1)).isSameAs(ApplicationConversionService.getSharedInstance()); } @@ -105,6 +106,12 @@ TestConverter testConverter() { return new TestConverter(); } + @Bean + @ConfigurationPropertiesBinding + StringConverter stringConverter() { + return new StringConverter(); + } + } private static final class TestApplicationConversionService extends ApplicationConversionService { @@ -120,4 +127,13 @@ public OutputStream convert(InputStream source) { } + private static final class StringConverter implements Converter { + + @Override + public InputStream convert(String source) { + throw new UnsupportedOperationException(); + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ConversionServiceArguments.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ConversionServiceArguments.java index 90c29e18a019..5afe71d440d3 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ConversionServiceArguments.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ConversionServiceArguments.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -35,20 +35,20 @@ * @author Phillip Webb * @author Andy Wilkinson */ -final class ConversionServiceArguments { +public final class ConversionServiceArguments { private ConversionServiceArguments() { } - static Stream with(Formatter formatter) { + public static Stream with(Formatter formatter) { return with((conversionService) -> conversionService.addFormatter(formatter)); } - static Stream with(GenericConverter converter) { + public static Stream with(GenericConverter converter) { return with((conversionService) -> conversionService.addConverter(converter)); } - static Stream with(Consumer initializer) { + public static Stream with(Consumer initializer) { FormattingConversionService withoutDefaults = new FormattingConversionService(); initializer.accept(withoutDefaults); return Stream.of( @@ -57,7 +57,7 @@ static Stream with(Consumer in "Application conversion service"))); } - static boolean isApplicationConversionService(ConversionService conversionService) { + public static boolean isApplicationConversionService(ConversionService conversionService) { if (conversionService instanceof NamedConversionService namedConversionService) { return isApplicationConversionService(namedConversionService.delegate); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ConversionServiceTest.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ConversionServiceTest.java index 37ba16740fd7..522a010813d8 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ConversionServiceTest.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ConversionServiceTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * 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. @@ -39,6 +39,6 @@ @MethodSource("conversionServices") @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) -@interface ConversionServiceTest { +public @interface ConversionServiceTest { } From 6ec56da91910fb76a5e10a0b48809de1712f181e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 12 Jan 2024 12:10:58 +0000 Subject: [PATCH 1041/1215] Ensure that reactive actuator security has an auth manager This is a follow-on from afad358 and ensures that the auto-configured security for Actuator in a WebFlux app has an authentication manager to back its use of HTTP basic and form login. Fixes gh-39069 --- ...anagementWebSecurityAutoConfiguration.java | 15 ++++++++- ...mentWebSecurityAutoConfigurationTests.java | 32 ++++++++++++++----- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfiguration.java index 7ceff5128753..a1c4a600fe91 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfiguration.java @@ -16,6 +16,8 @@ package org.springframework.boot.actuate.autoconfigure.security.reactive; +import reactor.core.publisher.Mono; + import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration; @@ -28,10 +30,14 @@ import org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration; import org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration; import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.SecurityWebFiltersOrder; import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.WebFilterChainProxy; import org.springframework.web.cors.reactive.PreFlightRequestHandler; @@ -50,7 +56,8 @@ @AutoConfiguration(before = ReactiveSecurityAutoConfiguration.class, after = { HealthEndpointAutoConfiguration.class, InfoEndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, ReactiveOAuth2ClientAutoConfiguration.class, - ReactiveOAuth2ResourceServerAutoConfiguration.class }) + ReactiveOAuth2ResourceServerAutoConfiguration.class, + ReactiveUserDetailsServiceAutoConfiguration.class }) @ConditionalOnClass({ EnableWebFluxSecurity.class, WebFilterChainProxy.class }) @ConditionalOnMissingBean({ SecurityWebFilterChain.class, WebFilterChainProxy.class }) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) @@ -69,4 +76,10 @@ public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, return http.build(); } + @Bean + @ConditionalOnMissingBean({ ReactiveAuthenticationManager.class, ReactiveUserDetailsService.class }) + ReactiveAuthenticationManager denyAllAuthenticationManager() { + return (authentication) -> Mono.error(new UsernameNotFoundException(authentication.getName())); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfigurationTests.java index e41082b893a4..66b39ded5a73 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/security/reactive/ReactiveManagementWebSecurityAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -71,17 +71,26 @@ class ReactiveManagementWebSecurityAutoConfigurationTests { HealthEndpointAutoConfiguration.class, InfoEndpointAutoConfiguration.class, WebFluxAutoConfiguration.class, EnvironmentEndpointAutoConfiguration.class, EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, - ReactiveSecurityAutoConfiguration.class, ReactiveManagementWebSecurityAutoConfiguration.class)) - .withUserConfiguration(UserDetailsServiceConfiguration.class); + ReactiveSecurityAutoConfiguration.class, ReactiveManagementWebSecurityAutoConfiguration.class)); @Test void permitAllForHealth() { - this.contextRunner.run((context) -> assertThat(getAuthenticateHeader(context, "/actuator/health")).isNull()); + this.contextRunner.withUserConfiguration(UserDetailsServiceConfiguration.class) + .run((context) -> assertThat(getAuthenticateHeader(context, "/actuator/health")).isNull()); } @Test void securesEverythingElse() { + this.contextRunner.withUserConfiguration(UserDetailsServiceConfiguration.class).run((context) -> { + assertThat(getAuthenticateHeader(context, "/actuator").get(0)).contains("Basic realm="); + assertThat(getAuthenticateHeader(context, "/foo").toString()).contains("Basic realm="); + }); + } + + @Test + void noExistingAuthenticationManagerOrUserDetailsService() { this.contextRunner.run((context) -> { + assertThat(getAuthenticateHeader(context, "/actuator/health")).isNull(); assertThat(getAuthenticateHeader(context, "/actuator").get(0)).contains("Basic realm="); assertThat(getAuthenticateHeader(context, "/foo").toString()).contains("Basic realm="); }); @@ -89,10 +98,12 @@ void securesEverythingElse() { @Test void usesMatchersBasedOffConfiguredActuatorBasePath() { - this.contextRunner.withPropertyValues("management.endpoints.web.base-path=/").run((context) -> { - assertThat(getAuthenticateHeader(context, "/health")).isNull(); - assertThat(getAuthenticateHeader(context, "/foo").get(0)).contains("Basic realm="); - }); + this.contextRunner.withUserConfiguration(UserDetailsServiceConfiguration.class) + .withPropertyValues("management.endpoints.web.base-path=/") + .run((context) -> { + assertThat(getAuthenticateHeader(context, "/health")).isNull(); + assertThat(getAuthenticateHeader(context, "/foo").get(0)).contains("Basic realm="); + }); } @Test @@ -180,6 +191,11 @@ SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { return http.build(); } + @Bean + ReactiveAuthenticationManager authenticationManager() { + return mock(ReactiveAuthenticationManager.class); + } + } @Configuration(proxyBeanMethods = false) From 6bfac1f8604b2a3fce6e16c7ef18070a06147aee Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 12 Jan 2024 15:13:58 +0000 Subject: [PATCH 1042/1215] Fix handling of nested: UNC paths on Windows Closes gh-38956 --- .../net/protocol/nested/NestedLocation.java | 11 ++++++++--- .../net/protocol/nested/NestedLocationTests.java | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java index 589ef5972831..94e513b64036 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -48,6 +48,7 @@ * @param path the path to the zip that contains the nested entry * @param nestedEntryName the nested entry name * @author Phillip Webb + * @author Andy Wilkinson * @since 3.2.0 */ public record NestedLocation(Path path, String nestedEntryName) { @@ -72,7 +73,7 @@ public static NestedLocation fromUrl(URL url) { if (url == null || !"nested".equalsIgnoreCase(url.getProtocol())) { throw new IllegalArgumentException("'url' must not be null and must use 'nested' protocol"); } - return parse(UrlDecoder.decode(url.getPath())); + return parse(UrlDecoder.decode(url.toString().substring(7))); } /** @@ -98,7 +99,7 @@ static NestedLocation parse(String path) { private static NestedLocation create(int index, String location) { String locationPath = (index != -1) ? location.substring(0, index) : location; - if (isWindows()) { + if (isWindows() && !isUncPath(location)) { while (locationPath.startsWith("/")) { locationPath = locationPath.substring(1, locationPath.length()); } @@ -111,6 +112,10 @@ private static boolean isWindows() { return File.separatorChar == '\\'; } + private static boolean isUncPath(String input) { + return !input.contains(":"); + } + static void clearCache() { cache.clear(); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java index c1a13b21b0cf..40449813b837 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -17,6 +17,7 @@ package org.springframework.boot.loader.net.protocol.nested; import java.io.File; +import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.nio.file.Path; @@ -24,6 +25,8 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.io.TempDir; import org.springframework.boot.loader.net.protocol.Handlers; @@ -35,6 +38,7 @@ * Tests for {@link NestedLocation}. * * @author Phillip Webb + * @author Andy Wilkinson */ class NestedLocationTests { @@ -130,4 +134,13 @@ void fromUriReturnsNestedLocation() throws Exception { assertThat(location.nestedEntryName()).isEqualTo("lib/nested.jar"); } + @Test + @EnabledOnOs(OS.WINDOWS) + void windowsUncPathIsHandledCorrectly() throws MalformedURLException { + NestedLocation location = NestedLocation.fromUrl( + new URL("nested://localhost/c$/dev/temp/demo/build/libs/demo-0.0.1-SNAPSHOT.jar/!BOOT-INF/classes/")); + assertThat(location.path()).asString() + .isEqualTo("\\\\localhost\\c$\\dev\\temp\\demo\\build\\libs\\demo-0.0.1-SNAPSHOT.jar"); + } + } From 7851c2362eb53c989cbdda706576c729a016bc49 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Fri, 12 Jan 2024 16:08:44 -0600 Subject: [PATCH 1043/1215] Remove APIs that were deprecated for removal in 3.3.0 See gh-39039 --- .../boot/web/server/WebServerSslBundle.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java index f5e6f448b743..115dba9afac5 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java @@ -141,14 +141,6 @@ private static SslStoreBundle createStoreBundle(Ssl ssl) { throw new IllegalStateException("SSL is enabled but no trust material is configured"); } - static SslBundle createCertificateFileSslStoreProviderDelegate(Ssl ssl) { - if (!hasCertificateProperties(ssl)) { - return null; - } - SslStoreBundle stores = createPemStoreBundle(ssl); - return new WebServerSslBundle(stores, ssl.getKeyPassword(), ssl); - } - private static boolean hasCertificateProperties(Ssl ssl) { return Ssl.isEnabled(ssl) && ssl.getCertificate() != null && ssl.getCertificatePrivateKey() != null; } From 84bb0603122bfde6bab30cbbfd5d7ba75f8e04e5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Jan 2024 10:14:50 +0000 Subject: [PATCH 1044/1215] Upgrade to Spring Data Bom 2023.1.2 Closes gh-38981 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d7c622af3515..2fe96a675b95 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1519,7 +1519,7 @@ bom { ] } } - library("Spring Data Bom", "2023.1.2-SNAPSHOT") { + library("Spring Data Bom", "2023.1.2") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From 8bdaae37b00bc1c860bea4ba0d5f95f4c6fef9e4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Jan 2024 10:14:54 +0000 Subject: [PATCH 1045/1215] Upgrade to Spring WS 4.0.10 Closes gh-39130 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 2fe96a675b95..f23aeb7ccda7 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1626,7 +1626,7 @@ bom { ] } } - library("Spring WS", "4.0.9") { + library("Spring WS", "4.0.10") { considerSnapshots() group("org.springframework.ws") { imports = [ From bd14cf6a8581809253c7d32ae343c9faaf64b461 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Jan 2024 10:20:19 +0000 Subject: [PATCH 1046/1215] Upgrade to Spring Data Bom 2023.1.2 Closes gh-38988 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 0ac7be077ef5..6dff521e8f30 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1520,7 +1520,7 @@ bom { ] } } - library("Spring Data Bom", "2023.1.2-SNAPSHOT") { + library("Spring Data Bom", "2023.1.2") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From fdd34a56394b7dbab4a77ce30190956b22b391b0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Jan 2024 10:20:23 +0000 Subject: [PATCH 1047/1215] Upgrade to Spring WS 4.0.10 Closes gh-39131 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 6dff521e8f30..1dc88b12d8e7 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1627,7 +1627,7 @@ bom { ] } } - library("Spring WS", "4.0.9") { + library("Spring WS", "4.0.10") { considerSnapshots() group("org.springframework.ws") { imports = [ From 46b7bd2f23adc85e795a96d680893a10e4f1f3b3 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 15 Jan 2024 13:51:15 +0100 Subject: [PATCH 1048/1215] Add configuration property to enable micrometer annotations Prior to this commit, the Micrometer annotations support (`@Timed`, `@Counted`...) was guarded by the presence of both Micrometer and AspectJ on the classpath. This signal is too weak, considering the startup performance impact and the fact that the AspectJ dependency can be brought transitively in many cases. This commit adds a new `micrometer.observations.annotations.enabled` property that is set to `false` by default to only process the annotations support when this property is enabled. Fixes gh-39128 --- .../DocumentConfigurationProperties.java | 1 + .../MetricsAspectsAutoConfiguration.java | 4 +++- .../MicrometerTracingAutoConfiguration.java | 4 +++- ...itional-spring-configuration-metadata.json | 6 +++++ .../MetricsAspectsAutoConfigurationTests.java | 24 ++++++++++++++----- ...crometerTracingAutoConfigurationTests.java | 14 +++++++++++ .../src/docs/asciidoc/actuator/metrics.adoc | 3 ++- .../docs/asciidoc/actuator/observability.adoc | 8 +++++++ .../src/docs/asciidoc/attributes.adoc | 1 + 9 files changed, 56 insertions(+), 9 deletions(-) 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 6245d5b9da5e..e3bce2d4f41a 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 @@ -215,6 +215,7 @@ private void rsocketPrefixes(Config prefix) { private void actuatorPrefixes(Config prefix) { prefix.accept("management"); + prefix.accept("micrometer"); } private void dockerComposePrefixes(Config prefix) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAspectsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAspectsAutoConfiguration.java index dbeeb8b27d6e..1541778479a9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAspectsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAspectsAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -28,6 +28,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; /** @@ -39,6 +40,7 @@ */ @AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class }) @ConditionalOnClass({ MeterRegistry.class, Advice.class }) +@ConditionalOnProperty(prefix = "micrometer.observations.annotations", name = "enabled", havingValue = "true") @ConditionalOnBean(MeterRegistry.class) public class MetricsAspectsAutoConfiguration { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java index 93d8acaa0e3d..1ef058236f8a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -35,6 +35,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; @@ -96,6 +97,7 @@ public PropagatingReceiverTracingObservationHandler propagatingReceiverTracin @Configuration(proxyBeanMethods = false) @ConditionalOnClass(Advice.class) + @ConditionalOnProperty(prefix = "micrometer.observations.annotations", name = "enabled", havingValue = "true") static class SpanAspectConfiguration { @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index bfdc19a9445c..90eef215d6cf 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -2229,6 +2229,12 @@ "defaultValue": [ "W3C" ] + }, + { + "name": "micrometer.observations.annotations.enabled", + "type": "java.lang.Boolean", + "description": "Whether auto-configuration of Micrometer annotations is enabled.", + "defaultValue": false } ], "hints": [ diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAspectsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAspectsAutoConfigurationTests.java index 215eba7b481b..d2ecaf6f06d2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAspectsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAspectsAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -41,8 +41,19 @@ class MetricsAspectsAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple()) + .withPropertyValues("micrometer.observations.annotations.enabled=true") .withConfiguration(AutoConfigurations.of(MetricsAspectsAutoConfiguration.class)); + @Test + void shouldNotConfigureAspectsByDefault() { + new ApplicationContextRunner().with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(MetricsAspectsAutoConfiguration.class)) + .run((context) -> { + assertThat(context).doesNotHaveBean(CountedAspect.class); + assertThat(context).doesNotHaveBean(TimedAspect.class); + }); + } + @Test void shouldConfigureAspects() { this.contextRunner.run((context) -> { @@ -78,11 +89,12 @@ void shouldNotConfigureAspectsIfAspectjIsMissing() { @Test void shouldNotConfigureAspectsIfMeterRegistryBeanIsMissing() { - new ApplicationContextRunner().run((context) -> { - assertThat(context).doesNotHaveBean(MeterRegistry.class); - assertThat(context).doesNotHaveBean(CountedAspect.class); - assertThat(context).doesNotHaveBean(TimedAspect.class); - }); + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(MetricsAspectsAutoConfiguration.class)) + .run((context) -> { + assertThat(context).doesNotHaveBean(MeterRegistry.class); + assertThat(context).doesNotHaveBean(CountedAspect.class); + assertThat(context).doesNotHaveBean(TimedAspect.class); + }); } @Test diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java index 0d2ce0396f8f..f671d494159e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/MicrometerTracingAutoConfigurationTests.java @@ -47,15 +47,18 @@ * * @author Moritz Halbritter * @author Jonatan Ivanov + * @author Brian Clozel */ class MicrometerTracingAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("micrometer.observations.annotations.enabled=true") .withConfiguration(AutoConfigurations.of(MicrometerTracingAutoConfiguration.class)); @Test void shouldSupplyBeans() { this.contextRunner.withUserConfiguration(TracerConfiguration.class, PropagatorConfiguration.class) + .withPropertyValues("micrometer.observations.annotations.enabled=true") .run((context) -> { assertThat(context).hasSingleBean(DefaultTracingObservationHandler.class); assertThat(context).hasSingleBean(PropagatingReceiverTracingObservationHandler.class); @@ -127,6 +130,17 @@ void shouldNotSupplyBeansIfTracerIsMissing() { }); } + @Test + void shouldNotSupplyAspectBeansIfPropertyIsDisabled() { + this.contextRunner.withUserConfiguration(TracerConfiguration.class, PropagatorConfiguration.class) + .withPropertyValues("micrometer.observations.annotations.enabled=false") + .run((context) -> { + assertThat(context).doesNotHaveBean(DefaultNewSpanParser.class); + assertThat(context).doesNotHaveBean(ImperativeMethodInvocationProcessor.class); + assertThat(context).doesNotHaveBean(SpanAspect.class); + }); + } + @Test void shouldNotSupplyBeansIfAspectjIsMissing() { this.contextRunner.withUserConfiguration(TracerConfiguration.class) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc index f31a807d8298..f0e4f9c984e6 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc @@ -1067,7 +1067,8 @@ Metrics for Jetty's `Connector` instances are bound by using Micrometer's `Jetty [[actuator.metrics.supported.timed-annotation]] ==== @Timed Annotation Support -To use `@Timed` where it is not directly supported by Spring Boot, refer to the {micrometer-concepts-docs}#_the_timed_annotation[Micrometer documentation]. +To enable scanning of `@Timed` annotations, you will need to set the configprop:micrometer.observations.annotations.enabled[] property to `true`. +Please refer to the {micrometer-concepts-docs}#_the_timed_annotation[Micrometer documentation]. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc index dbf2e3259768..68187d94296e 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc @@ -85,3 +85,11 @@ NOTE: Spring Boot does not provide auto-configuration for OpenTelemetry metrics OpenTelemetry tracing is only auto-configured when used together with <>. The next sections will provide more details about logging, metrics and traces. + + + +[[actuator.observability.annotations]] +=== Micrometer Observation Annotations support + +To enable scanning of metrics and tracing annotations like `@Timed`, `@Counted`, `@MeterTag` and `@NewSpan` annotations, you will need to set the configprop:micrometer.observations.annotations.enabled[] property to `true`. +This feature is supported Micrometer directly, please refer to the {micrometer-concepts-docs}#_the_timed_annotation[Micrometer] and {micrometer-tracing-docs}/api.html#_aspect_oriented_programming[Micrometer Tracing] reference docs. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc index ed3f880e0cbc..f3457ddb0975 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc @@ -109,6 +109,7 @@ :micrometer-docs: https://micrometer.io/docs :micrometer-concepts-docs: {micrometer-docs}/concepts :micrometer-registry-docs: {micrometer-docs}/registry +:micrometer-tracing-docs: https://docs.micrometer.io/tracing/reference :tomcat-docs: https://tomcat.apache.org/tomcat-{tomcat-version}-doc :graal-version: 22.3 :graal-native-image-docs: https://www.graalvm.org/{graal-version}/reference-manual/native-image From 0f23feffafd7f153e0e3f083ee55a3a8307ff9d0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Jan 2024 15:26:16 +0000 Subject: [PATCH 1049/1215] Upgrade to Neo4j Java Driver 5.15.0 Closes gh-39136 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index bbb2fafd34fa..a24f25aa9366 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1074,7 +1074,7 @@ bom { ] } } - library("Neo4j Java Driver", "5.13.0") { + library("Neo4j Java Driver", "5.15.0") { alignWithVersion { from "org.springframework.data:spring-data-neo4j" managedBy "Spring Data Bom" From 12d390d564dec2f2f30c10b1c53538a37ad9385e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Jan 2024 17:04:13 +0000 Subject: [PATCH 1050/1215] Upgrade to Spring Security 6.3.0-M1 Closes gh-38990 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f22ac3e40c4b..d0c4440e9fd1 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1611,7 +1611,7 @@ bom { ] } } - library("Spring Security", "6.3.0-SNAPSHOT") { + library("Spring Security", "6.3.0-M1") { considerSnapshots() group("org.springframework.security") { imports = [ From e58f65366c9212424d0945a37cdf87f57124b338 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 15 Jan 2024 18:22:37 +0100 Subject: [PATCH 1051/1215] Auto-configure TypeDefinitionConfigurer beans Prior to this commit, the GraphQL auto-configuration would consider many bean types like `DataFetcherExceptionResolver` and `SubscriptionExceptionResolver` to configure the `GraphQlSource`. It would also configure a default `ConnectionTypeDefinitionConfigurer`. This commit will detect all `TypeDefinitionConfigurer` beans defined in the application and configure them in addition to the `ConnectionTypeDefinitionConfigurer`. Closes gh-39118 --- .../graphql/GraphQlAutoConfiguration.java | 7 +++-- .../GraphQlAutoConfigurationTests.java | 31 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfiguration.java index 886e597a5220..85d5c5e9ddb0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -66,6 +66,7 @@ import org.springframework.graphql.execution.GraphQlSource; import org.springframework.graphql.execution.RuntimeWiringConfigurer; import org.springframework.graphql.execution.SubscriptionExceptionResolver; +import org.springframework.graphql.execution.TypeDefinitionConfigurer; /** * {@link EnableAutoConfiguration Auto-configuration} for creating a Spring GraphQL base @@ -95,7 +96,8 @@ public GraphQlSource graphQlSource(ResourcePatternResolver resourcePatternResolv ObjectProvider exceptionResolvers, ObjectProvider subscriptionExceptionResolvers, ObjectProvider instrumentations, ObjectProvider wiringConfigurers, - ObjectProvider sourceCustomizers) { + ObjectProvider sourceCustomizers, + ObjectProvider typeDefinitionConfigurers) { String[] schemaLocations = properties.getSchema().getLocations(); Resource[] schemaResources = resolveSchemaResources(resourcePatternResolver, schemaLocations, properties.getSchema().getFileExtensions()); @@ -110,6 +112,7 @@ public GraphQlSource graphQlSource(ResourcePatternResolver resourcePatternResolv if (!properties.getSchema().getIntrospection().isEnabled()) { builder.configureRuntimeWiring(this::enableIntrospection); } + typeDefinitionConfigurers.forEach(builder::configureTypeDefinitions); builder.configureTypeDefinitions(new ConnectionTypeDefinitionConfigurer()); wiringConfigurers.orderedStream().forEach(builder::configureRuntimeWiring); sourceCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java index dd0f97c7a4a2..8b121538210d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/GraphQlAutoConfigurationTests.java @@ -26,6 +26,7 @@ import graphql.schema.GraphQLOutputType; import graphql.schema.GraphQLSchema; import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.TypeDefinitionRegistry; import graphql.schema.visibility.DefaultGraphqlFieldVisibility; import graphql.schema.visibility.NoIntrospectionGraphqlFieldVisibility; import org.assertj.core.api.InstanceOfAssertFactories; @@ -52,6 +53,7 @@ import org.springframework.graphql.execution.DataLoaderRegistrar; import org.springframework.graphql.execution.GraphQlSource; import org.springframework.graphql.execution.RuntimeWiringConfigurer; +import org.springframework.graphql.execution.TypeDefinitionConfigurer; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -222,6 +224,14 @@ void shouldContributeConnectionTypeDefinitionConfigurer() { }); } + @Test + void shouldUseCustomTypeDefinitionConfigurerWhenDefined() { + this.contextRunner.withUserConfiguration(CustomTypeDefinitionConfigurer.class).run((context) -> { + TestTypeDefinitionConfigurer configurer = context.getBean(TestTypeDefinitionConfigurer.class); + assertThat(configurer.applied).isTrue(); + }); + } + @Test void whenApplicationTaskExecutorIsDefinedThenAnnotatedControllerConfigurerShouldUseIt() { this.contextRunner.withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class)) @@ -337,4 +347,25 @@ Executor customExecutor() { } + @Configuration(proxyBeanMethods = false) + static class CustomTypeDefinitionConfigurer { + + @Bean + TestTypeDefinitionConfigurer testTypeDefinitionConfigurer() { + return new TestTypeDefinitionConfigurer(); + } + + } + + static class TestTypeDefinitionConfigurer implements TypeDefinitionConfigurer { + + boolean applied = false; + + @Override + public void configure(TypeDefinitionRegistry registry) { + this.applied = true; + } + + } + } From a57580707893662d4bd967a86fbe9136dd1f22f2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 16 Jan 2024 13:02:46 +0000 Subject: [PATCH 1052/1215] Upgrade to Spring Pulsar 1.0.2 Closes gh-38994 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index a24f25aa9366..0a797117d124 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1586,7 +1586,7 @@ bom { ] } } - library("Spring Pulsar", "1.0.2-SNAPSHOT") { + library("Spring Pulsar", "1.0.2") { considerSnapshots() group("org.springframework.pulsar") { imports = [ From c48ff13ceeae232c9f3986f59600c558111c790d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 16 Jan 2024 13:03:25 +0000 Subject: [PATCH 1053/1215] Upgrade to Spring Pulsar 1.0.2 Closes gh-38995 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d0c4440e9fd1..1f8aef8c1371 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1587,7 +1587,7 @@ bom { ] } } - library("Spring Pulsar", "1.0.2-SNAPSHOT") { + library("Spring Pulsar", "1.0.2") { considerSnapshots() group("org.springframework.pulsar") { imports = [ From 18c083d619bb595c2dbdb06df42b2939174e24cd Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 16 Jan 2024 13:44:15 +0000 Subject: [PATCH 1054/1215] Upgrade to Spring Session 3.3.0-M1 Closes gh-38991 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 1f8aef8c1371..901b6dabf0a8 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1619,7 +1619,7 @@ bom { ] } } - library("Spring Session", "3.3.0-SNAPSHOT") { + library("Spring Session", "3.3.0-M1") { considerSnapshots() prohibit { startsWith(["Apple-", "Bean-", "Corn-", "Dragonfruit-"]) From de2aee9816a6b642daab49b759332f87337cc120 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 16 Jan 2024 16:31:19 +0100 Subject: [PATCH 1055/1215] Upgrade to MariaDB 3.3.2 Closes gh-38901 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 0a797117d124..ca14bc4de98f 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -866,7 +866,7 @@ bom { ] } } - library("MariaDB", "3.2.0") { + library("MariaDB", "3.3.2") { group("org.mariadb.jdbc") { modules = [ "mariadb-java-client" From 00f69c4ee87d9630b277f4c76d6bda1d4b15c7d4 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 16 Jan 2024 16:33:29 +0100 Subject: [PATCH 1056/1215] Upgrade to MySQL 8.3.0 Closes gh-39081 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index ca14bc4de98f..54afb5db2aae 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1051,7 +1051,7 @@ bom { ] } } - library("MySQL", "8.1.0") { + library("MySQL", "8.3.0") { group("com.mysql") { modules = [ "mysql-connector-j" { From b54567f5f318de5de990868a835ceb73d18020fe Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 16 Jan 2024 15:40:28 +0000 Subject: [PATCH 1057/1215] Upgrade to Spring Authorization Server 1.3.0-M1 Closes gh-38987 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 901b6dabf0a8..43aca785ec79 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1508,7 +1508,7 @@ bom { ] } } - library("Spring Authorization Server", "1.3.0-SNAPSHOT") { + library("Spring Authorization Server", "1.3.0-M1") { considerSnapshots() group("org.springframework.security") { modules = [ From 50c89ff80385f91e1001f21cda8c41dbcb12929f Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 16 Jan 2024 16:55:17 +0100 Subject: [PATCH 1058/1215] Upgrade to MySQL 8.3.0 Closes gh-39147 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 43aca785ec79..7b4aabe872c1 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1052,7 +1052,7 @@ bom { ] } } - library("MySQL", "8.2.0") { + library("MySQL", "8.3.0") { group("com.mysql") { modules = [ "mysql-connector-j" { From 89874d351a05ef09cc65c91b645da4af2193cd3c Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 16 Jan 2024 10:26:34 -0800 Subject: [PATCH 1059/1215] Ensure containers are started before binding datasource properties Update `TestcontainersLifecycleBeanPostProcessor` so that containers are now initialized either on the first `postProcessAfterInitialization` call with a frozen configuration or just before a test container property is supplied. Prior to this commit, it was assumed that the first post-process call after the configuration was frozen was suitably early to initialize the containers. This turns out to not be no always the case. Specifically, in the `finishBeanFactoryInitialization` method of `AbstractApplicationContext` we see that `LoadTimeWeaverAware` beans are obtained before the configuration is frozen. One such bean is `DefaultPersistenceUnitManager` which is likely to need datasource properties that will require a started container. To fix the problem, the `TestcontainersPropertySource` now publishes a `BeforeTestcontainersPropertySuppliedEvent` to the ApplicationContext just before any value is supplied. By listening for this event, we can ensure that containers are initialized and started before any dynamic property is read. Fixes gh-38913 --- .../DynamicPropertySourceMethodsImporter.java | 10 +- .../ImportTestcontainersRegistrar.java | 4 +- ...ifecycleApplicationContextInitializer.java | 7 +- ...tcontainersLifecycleBeanPostProcessor.java | 34 +++--- ...reTestcontainersPropertySuppliedEvent.java | 47 ++++++++ .../TestcontainersPropertySource.java | 87 +++++++++++++-- ...ainersPropertySourceAutoConfiguration.java | 13 ++- ...adTimeWeaverAwareBeanIntegrationTests.java | 102 ++++++++++++++++++ ...sPropertySourceAutoConfigurationTests.java | 21 ++-- .../TestcontainersPropertySourceTests.java | 23 +++- 10 files changed, 311 insertions(+), 37 deletions(-) create mode 100644 spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/BeforeTestcontainersPropertySuppliedEvent.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersImportWithPropertiesInjectedIntoLoadTimeWeaverAwareBeanIntegrationTests.java diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/DynamicPropertySourceMethodsImporter.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/DynamicPropertySourceMethodsImporter.java index 35d72d7c7206..d680f7504c81 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/DynamicPropertySourceMethodsImporter.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/DynamicPropertySourceMethodsImporter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -20,6 +20,7 @@ import java.lang.reflect.Modifier; import java.util.Set; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.boot.testcontainers.properties.TestcontainersPropertySource; import org.springframework.core.MethodIntrospector; import org.springframework.core.annotation.MergedAnnotations; @@ -43,16 +44,17 @@ class DynamicPropertySourceMethodsImporter { this.environment = environment; } - void registerDynamicPropertySources(Class definitionClass) { + void registerDynamicPropertySources(BeanDefinitionRegistry beanDefinitionRegistry, Class definitionClass) { Set methods = MethodIntrospector.selectMethods(definitionClass, this::isAnnotated); if (methods.isEmpty()) { return; } - DynamicPropertyRegistry registry = TestcontainersPropertySource.attach(this.environment); + DynamicPropertyRegistry dynamicPropertyRegistry = TestcontainersPropertySource.attach(this.environment, + beanDefinitionRegistry); methods.forEach((method) -> { assertValid(method); ReflectionUtils.makeAccessible(method); - ReflectionUtils.invokeMethod(method, null, registry); + ReflectionUtils.invokeMethod(method, null, dynamicPropertyRegistry); }); } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ImportTestcontainersRegistrar.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ImportTestcontainersRegistrar.java index 9e4dc4c97dfb..1c2cf49725c3 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ImportTestcontainersRegistrar.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/context/ImportTestcontainersRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -62,7 +62,7 @@ private void registerBeanDefinitions(BeanDefinitionRegistry registry, Class[] for (Class definitionClass : definitionClasses) { this.containerFieldsImporter.registerBeanDefinitions(registry, definitionClass); if (this.dynamicPropertySourceMethodsImporter != null) { - this.dynamicPropertySourceMethodsImporter.registerDynamicPropertySources(definitionClass); + this.dynamicPropertySourceMethodsImporter.registerDynamicPropertySources(registry, definitionClass); } } } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleApplicationContextInitializer.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleApplicationContextInitializer.java index 41420bbb6f42..662be02a7f47 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleApplicationContextInitializer.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleApplicationContextInitializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -48,7 +48,10 @@ public void initialize(ConfigurableApplicationContext applicationContext) { ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory(); applicationContext.addBeanFactoryPostProcessor(new TestcontainersLifecycleBeanFactoryPostProcessor()); TestcontainersStartup startup = TestcontainersStartup.get(applicationContext.getEnvironment()); - beanFactory.addBeanPostProcessor(new TestcontainersLifecycleBeanPostProcessor(beanFactory, startup)); + TestcontainersLifecycleBeanPostProcessor beanPostProcessor = new TestcontainersLifecycleBeanPostProcessor( + beanFactory, startup); + beanFactory.addBeanPostProcessor(beanPostProcessor); + applicationContext.addApplicationListener(beanPostProcessor); } } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java index 3ec39b713a5e..519d5de0d6a2 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersLifecycleBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -38,6 +38,8 @@ import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; +import org.springframework.boot.testcontainers.properties.BeforeTestcontainersPropertySuppliedEvent; +import org.springframework.context.ApplicationListener; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.core.log.LogMessage; @@ -56,7 +58,8 @@ * @see TestcontainersLifecycleApplicationContextInitializer */ @Order(Ordered.LOWEST_PRECEDENCE) -class TestcontainersLifecycleBeanPostProcessor implements DestructionAwareBeanPostProcessor { +class TestcontainersLifecycleBeanPostProcessor + implements DestructionAwareBeanPostProcessor, ApplicationListener { private static final Log logger = LogFactory.getLog(TestcontainersLifecycleBeanPostProcessor.class); @@ -74,9 +77,14 @@ class TestcontainersLifecycleBeanPostProcessor implements DestructionAwareBeanPo this.startup = startup; } + @Override + public void onApplicationEvent(BeforeTestcontainersPropertySuppliedEvent event) { + initializeContainers(); + } + @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (this.beanFactory.isConfigurationFrozen() && this.containersInitialized.compareAndSet(false, true)) { + if (this.beanFactory.isConfigurationFrozen()) { initializeContainers(); } if (bean instanceof Startable startableBean) { @@ -121,15 +129,17 @@ private void start(List beans) { } private void initializeContainers() { - logger.trace("Initializing containers"); - List beanNames = List.of(this.beanFactory.getBeanNamesForType(ContainerState.class, false, false)); - List beans = getBeans(beanNames); - if (beans != null) { - logger.trace(LogMessage.format("Initialized containers %s", beanNames)); - } - else { - logger.trace(LogMessage.format("Failed to initialize containers %s", beanNames)); - this.containersInitialized.set(false); + if (this.containersInitialized.compareAndSet(false, true)) { + logger.trace("Initializing containers"); + List beanNames = List.of(this.beanFactory.getBeanNamesForType(ContainerState.class, false, false)); + List beans = getBeans(beanNames); + if (beans != null) { + logger.trace(LogMessage.format("Initialized containers %s", beanNames)); + } + else { + logger.trace(LogMessage.format("Failed to initialize containers %s", beanNames)); + this.containersInitialized.set(false); + } } } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/BeforeTestcontainersPropertySuppliedEvent.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/BeforeTestcontainersPropertySuppliedEvent.java new file mode 100644 index 000000000000..4efe59902974 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/BeforeTestcontainersPropertySuppliedEvent.java @@ -0,0 +1,47 @@ +/* + * 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.testcontainers.properties; + +import java.util.function.Supplier; + +import org.springframework.context.ApplicationEvent; + +/** + * Event published just before the {@link Supplier value supplier} of a + * {@link TestcontainersPropertySource} property is called. + * + * @author Phillip Webb + * @since 3.2.2 + */ +public class BeforeTestcontainersPropertySuppliedEvent extends ApplicationEvent { + + private final String propertyName; + + BeforeTestcontainersPropertySuppliedEvent(TestcontainersPropertySource source, String propertyName) { + super(source); + this.propertyName = propertyName; + } + + /** + * Return the name of the property about to be supplied. + * @return the propertyName the property name + */ + public String getPropertyName() { + return this.propertyName; + } + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java index 03cc0932292a..1994303645ef 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -19,10 +19,20 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; import java.util.function.Supplier; import org.testcontainers.containers.Container; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.Environment; @@ -44,6 +54,8 @@ public class TestcontainersPropertySource extends EnumerablePropertySource eventPublishers = new CopyOnWriteArraySet<>(); + TestcontainersPropertySource() { this(Collections.synchronizedMap(new LinkedHashMap<>())); } @@ -57,10 +69,20 @@ private TestcontainersPropertySource(Map> valueSupplier }; } + private void addEventPublisher(ApplicationEventPublisher eventPublisher) { + this.eventPublishers.add(eventPublisher); + } + @Override public Object getProperty(String name) { Supplier valueSupplier = this.source.get(name); - return (valueSupplier != null) ? valueSupplier.get() : null; + return (valueSupplier != null) ? getProperty(name, valueSupplier) : null; + } + + private Object getProperty(String name, Supplier valueSupplier) { + BeforeTestcontainersPropertySuppliedEvent event = new BeforeTestcontainersPropertySuppliedEvent(this, name); + this.eventPublishers.forEach((eventPublisher) -> eventPublisher.publishEvent(event)); + return valueSupplier.get(); } @Override @@ -74,20 +96,73 @@ public String[] getPropertyNames() { } public static DynamicPropertyRegistry attach(Environment environment) { + return attach(environment, null); + } + + static DynamicPropertyRegistry attach(ConfigurableApplicationContext applicationContext) { + return attach(applicationContext.getEnvironment(), applicationContext, null); + } + + public static DynamicPropertyRegistry attach(Environment environment, BeanDefinitionRegistry registry) { + return attach(environment, null, registry); + } + + private static DynamicPropertyRegistry attach(Environment environment, ApplicationEventPublisher eventPublisher, + BeanDefinitionRegistry registry) { Assert.state(environment instanceof ConfigurableEnvironment, "TestcontainersPropertySource can only be attached to a ConfigurableEnvironment"); - return attach((ConfigurableEnvironment) environment); + TestcontainersPropertySource propertySource = getOrAdd((ConfigurableEnvironment) environment); + if (eventPublisher != null) { + propertySource.addEventPublisher(eventPublisher); + } + else if (registry != null) { + registry.registerBeanDefinition(EventPublisherRegistrar.NAME, new RootBeanDefinition( + EventPublisherRegistrar.class, () -> new EventPublisherRegistrar(environment))); + } + return propertySource.registry; } - private static DynamicPropertyRegistry attach(ConfigurableEnvironment environment) { + static TestcontainersPropertySource getOrAdd(ConfigurableEnvironment environment) { PropertySource propertySource = environment.getPropertySources().get(NAME); if (propertySource == null) { environment.getPropertySources().addFirst(new TestcontainersPropertySource()); - return attach(environment); + return getOrAdd(environment); } Assert.state(propertySource instanceof TestcontainersPropertySource, "Incorrect DynamicValuesPropertySource type registered"); - return ((TestcontainersPropertySource) propertySource).registry; + return ((TestcontainersPropertySource) propertySource); + } + + /** + * {@link BeanFactoryPostProcessor} to register the {@link ApplicationEventPublisher} + * to the {@link TestcontainersPropertySource}. This class is a + * {@link BeanFactoryPostProcessor} so that it is initialized as early as possible. + */ + private static class EventPublisherRegistrar implements BeanFactoryPostProcessor, ApplicationEventPublisherAware { + + static final String NAME = EventPublisherRegistrar.class.getName(); + + private final Environment environment; + + private ApplicationEventPublisher eventPublisher; + + EventPublisherRegistrar(Environment environment) { + this.environment = environment; + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + if (this.eventPublisher != null) { + TestcontainersPropertySource.getOrAdd((ConfigurableEnvironment) this.environment) + .addEventPublisher(this.eventPublisher); + } + } + } } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfiguration.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfiguration.java index 5c016c964e5f..e6219f2d2d4e 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -16,9 +16,12 @@ package org.springframework.boot.testcontainers.properties; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; -import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.test.context.DynamicPropertyRegistry; /** @@ -28,6 +31,8 @@ * @author Phillip Webb * @since 3.1.0 */ +@AutoConfiguration +@Order(Ordered.HIGHEST_PRECEDENCE) @ConditionalOnClass(DynamicPropertyRegistry.class) public class TestcontainersPropertySourceAutoConfiguration { @@ -35,8 +40,8 @@ public class TestcontainersPropertySourceAutoConfiguration { } @Bean - DynamicPropertyRegistry dynamicPropertyRegistry(ConfigurableEnvironment environment) { - return TestcontainersPropertySource.attach(environment); + static DynamicPropertyRegistry dynamicPropertyRegistry(ConfigurableApplicationContext applicationContext) { + return TestcontainersPropertySource.attach(applicationContext); } } diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersImportWithPropertiesInjectedIntoLoadTimeWeaverAwareBeanIntegrationTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersImportWithPropertiesInjectedIntoLoadTimeWeaverAwareBeanIntegrationTests.java new file mode 100644 index 000000000000..cab98edd1509 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/TestcontainersImportWithPropertiesInjectedIntoLoadTimeWeaverAwareBeanIntegrationTests.java @@ -0,0 +1,102 @@ +/* + * 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.testcontainers.lifecycle; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.testcontainers.context.ImportTestcontainers; +import org.springframework.boot.testcontainers.lifecycle.TestcontainersImportWithPropertiesInjectedIntoLoadTimeWeaverAwareBeanIntegrationTests.Containers; +import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.context.annotation.Bean; +import org.springframework.context.weaving.LoadTimeWeaverAware; +import org.springframework.instrument.classloading.LoadTimeWeaver; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * @author Phillip Webb + */ +@ExtendWith(SpringExtension.class) +@DirtiesContext +@DisabledIfDockerUnavailable +@ImportTestcontainers(Containers.class) +class TestcontainersImportWithPropertiesInjectedIntoLoadTimeWeaverAwareBeanIntegrationTests { + + // gh-38913 + + @Test + void starts() { + } + + @TestConfiguration + @EnableConfigurationProperties(MockDataSourceProperties.class) + static class Config { + + @Bean + MockEntityManager mockEntityManager(MockDataSourceProperties properties) { + return new MockEntityManager(); + } + + } + + static class MockEntityManager implements LoadTimeWeaverAware { + + @Override + public void setLoadTimeWeaver(LoadTimeWeaver loadTimeWeaver) { + } + + } + + @ConfigurationProperties("spring.datasource") + public static class MockDataSourceProperties { + + private String url; + + public String getUrl() { + return this.url; + } + + public void setUrl(String url) { + this.url = url; + } + + } + + static class Containers { + + @Container + static PostgreSQLContainer container = new PostgreSQLContainer<>(DockerImageNames.postgresql()); + + @DynamicPropertySource + static void setConnectionProperties(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", container::getJdbcUrl); + registry.add("spring.datasource.password", container::getPassword); + registry.add("spring.datasource.username", container::getUsername); + } + + } + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfigurationTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfigurationTests.java index 723a4203f0b8..90103b24d78e 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -16,6 +16,9 @@ package org.springframework.boot.testcontainers.properties; +import java.util.ArrayList; +import java.util.List; + import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -25,6 +28,7 @@ import org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleApplicationContextInitializer; import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable; import org.springframework.boot.testsupport.testcontainers.RedisContainer; +import org.springframework.context.ApplicationEvent; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -46,11 +50,16 @@ class TestcontainersPropertySourceAutoConfigurationTests { @Test void containerBeanMethodContributesProperties() { - this.contextRunner.withUserConfiguration(ContainerAndPropertiesConfiguration.class).run((context) -> { - TestBean testBean = context.getBean(TestBean.class); - RedisContainer redisContainer = context.getBean(RedisContainer.class); - assertThat(testBean.getUsingPort()).isEqualTo(redisContainer.getFirstMappedPort()); - }); + List events = new ArrayList<>(); + this.contextRunner.withUserConfiguration(ContainerAndPropertiesConfiguration.class) + .withInitializer((context) -> context.addApplicationListener(events::add)) + .run((context) -> { + TestBean testBean = context.getBean(TestBean.class); + RedisContainer redisContainer = context.getBean(RedisContainer.class); + assertThat(testBean.getUsingPort()).isEqualTo(redisContainer.getFirstMappedPort()); + assertThat(events.stream().filter(BeforeTestcontainersPropertySuppliedEvent.class::isInstance)) + .hasSize(1); + }); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceTests.java index 7958666992da..4dce61ffc942 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -16,10 +16,15 @@ package org.springframework.boot.testcontainers.properties; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.PropertySource; import org.springframework.mock.env.MockEnvironment; @@ -101,4 +106,20 @@ void attachWhenAlreadyAttachedReturnsExisting() { assertThat(p1).isSameAs(p2); } + @Test + void getPropertyPublishesEvent() { + try (GenericApplicationContext applicationContext = new GenericApplicationContext()) { + List events = new ArrayList<>(); + applicationContext.addApplicationListener(events::add); + DynamicPropertyRegistry registry = TestcontainersPropertySource.attach(applicationContext.getEnvironment(), + (BeanDefinitionRegistry) applicationContext.getBeanFactory()); + applicationContext.refresh(); + registry.add("test", () -> "spring"); + assertThat(applicationContext.getEnvironment().containsProperty("test")).isTrue(); + assertThat(events.isEmpty()); + assertThat(applicationContext.getEnvironment().getProperty("test")).isEqualTo("spring"); + assertThat(events.stream().filter(BeforeTestcontainersPropertySuppliedEvent.class::isInstance)).hasSize(1); + } + } + } From 88a8550609113ed49a2ecbd12ca37d4983f62a8a Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 16 Jan 2024 10:47:44 -0800 Subject: [PATCH 1060/1215] Make OTEL tstcontainers integration test more resilient Tweak awaitility assertions to fix timing error that often occurs on local builds. --- ...etricsContainerConnectionDetailsFactoryIntegrationTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java index 57a951b4bc54..299244128601 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/otlp/OpenTelemetryMetricsContainerConnectionDetailsFactoryIntegrationTests.java @@ -88,7 +88,7 @@ void connectionCanBeMadeToOpenTelemetryCollectorContainer() { .untilAsserted(() -> whenPrometheusScraped().then() .statusCode(200) .contentType(OPENMETRICS_001) - .body(endsWith("# EOF\n"))); + .body(endsWith("# EOF\n"), containsString("service_name"))); whenPrometheusScraped().then() .body(containsString( "{job=\"test\",service_name=\"test\",telemetry_sdk_language=\"java\",telemetry_sdk_name=\"io.micrometer\""), From 3274205709a41ebac9c76b0353d0f673bdedc011 Mon Sep 17 00:00:00 2001 From: Wzy19930507 <1208931582@qq.com> Date: Mon, 15 Jan 2024 10:10:58 +0800 Subject: [PATCH 1061/1215] Use the term "tags" in documentation consistently See gh-39125 --- .../src/docs/asciidoc/actuator/observability.adoc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc index 68187d94296e..2cf79a176d7f 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc @@ -9,7 +9,7 @@ To create your own observations (which will lead to metrics and traces), you can include::code:MyCustomObservation[] -NOTE: Low cardinality key-values will be added to metrics and traces, while high cardinality key-values will only be added to traces. +NOTE: Low cardinality tags will be added to metrics and traces, while high cardinality tags will only be added to traces. Beans of type `ObservationPredicate`, `GlobalObservationConvention`, `ObservationFilter` and `ObservationHandler` will be automatically registered on the `ObservationRegistry`. You can additionally register any number of `ObservationRegistryCustomizer` beans to further configure the registry. @@ -27,10 +27,10 @@ Read more about it https://jdbc-observations.github.io/datasource-micrometer/doc TIP: Observability for R2DBC is built into Spring Boot. To enable it, add the `io.r2dbc:r2dbc-proxy` dependency to your project. -[[actuator.observability.common-key-values]] -=== Common Key-Values -Common key-values are generally used for dimensional drill-down on the operating environment, such as host, instance, region, stack, and others. -Common key-values are applied to all observations as low cardinality key-values and can be configured, as the following example shows: +[[actuator.observability.common-tags]] +=== Common tags +Common tags are generally used for dimensional drill-down on the operating environment, such as host, instance, region, stack, and others. +Common tags are applied to all observations as low cardinality tags and can be configured, as the following example shows: [source,yaml,indent=0,subs="verbatim",configprops,configblocks] ---- @@ -41,7 +41,7 @@ Common key-values are applied to all observations as low cardinality key-values stack: "prod" ---- -The preceding example adds `region` and `stack` key-values to all observations with a value of `us-east-1` and `prod`, respectively. +The preceding example adds `region` and `stack` tags to all observations with a value of `us-east-1` and `prod`, respectively. [[actuator.observability.preventing-observations]] === Preventing Observations From 5a38662f5f69bce6889e4a0b94d81a4a87f56e51 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 16 Jan 2024 11:16:02 -0800 Subject: [PATCH 1062/1215] Polish 'Use the term "tags" in documentation consistently' See gh-39125 --- .../src/docs/asciidoc/anchor-rewrite.properties | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties index 783ff820a907..593f2ec75ae4 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties @@ -1043,3 +1043,6 @@ features.testing.testcontainers.at-development-time=features.testcontainers.at-d features.testing.testcontainers.at-development-time.dynamic-properties=features.testcontainers.at-development-time.dynamic-properties features.testing.testcontainers.at-development-time.importing-container-declarations=features.testcontainers.at-development-time.importing-container-declarations features.testing.testcontainers.at-development-time.devtools=features.testcontainers.at-development-time.devtools + +# gh-39125 +actuator.observability.common-key-values=actuator.observability.common-tags From 267b7ab248a03750ecd085160fb683e516530d2a Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 16 Jan 2024 11:20:05 -0800 Subject: [PATCH 1063/1215] Polish formatting --- .../src/docs/asciidoc/actuator/observability.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc index 2cf79a176d7f..e0b02262c7b6 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc @@ -1,6 +1,5 @@ [[actuator.observability]] == Observability - Observability is the ability to observe the internal state of a running system from the outside. It consists of the three pillars logging, metrics and traces. @@ -27,6 +26,8 @@ Read more about it https://jdbc-observations.github.io/datasource-micrometer/doc TIP: Observability for R2DBC is built into Spring Boot. To enable it, add the `io.r2dbc:r2dbc-proxy` dependency to your project. + + [[actuator.observability.common-tags]] === Common tags Common tags are generally used for dimensional drill-down on the operating environment, such as host, instance, region, stack, and others. @@ -90,6 +91,5 @@ The next sections will provide more details about logging, metrics and traces. [[actuator.observability.annotations]] === Micrometer Observation Annotations support - To enable scanning of metrics and tracing annotations like `@Timed`, `@Counted`, `@MeterTag` and `@NewSpan` annotations, you will need to set the configprop:micrometer.observations.annotations.enabled[] property to `true`. This feature is supported Micrometer directly, please refer to the {micrometer-concepts-docs}#_the_timed_annotation[Micrometer] and {micrometer-tracing-docs}/api.html#_aspect_oriented_programming[Micrometer Tracing] reference docs. From 6ed8dc2970352f456ed0acb1996d11feb5d447ec Mon Sep 17 00:00:00 2001 From: Won Joon Thomas Choi <113500771+724thomas@users.noreply.github.com> Date: Fri, 29 Dec 2023 08:47:00 +0900 Subject: [PATCH 1064/1215] Improve reference documentation Address a series of minor typos and phrasing inconsistencies identified in few sections of documentation to enhance overall clarity and readability. See gh-38942 --- .../spring-boot-docs/src/docs/asciidoc/howto/batch.adoc | 2 +- .../src/docs/asciidoc/howto/data-initialization.adoc | 8 ++++---- .../src/docs/asciidoc/howto/docker-compose.adoc | 4 ++-- .../spring-boot-docs/src/docs/asciidoc/howto/logging.adoc | 6 +++--- .../spring-boot-docs/src/docs/asciidoc/howto/nosql.adoc | 6 +++--- .../src/docs/asciidoc/howto/security.adoc | 6 +++--- .../spring-boot-docs/src/docs/asciidoc/howto/testing.adoc | 2 +- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc index 2df1dbbf8516..c03c81520509 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc @@ -23,7 +23,7 @@ For more info about Spring Batch, see the {spring-batch}[Spring Batch project pa === Running Spring Batch Jobs on Startup Spring Batch auto-configuration is enabled by adding `spring-boot-starter-batch` to your application's classpath. -If a single `Job` is found in the application context, it is executed on startup (see {spring-boot-autoconfigure-module-code}/batch/JobLauncherApplicationRunner.java[`JobLauncherApplicationRunner`] for details). +If a single `Job` bean is found in the application context, it is executed on startup (see {spring-boot-autoconfigure-module-code}/batch/JobLauncherApplicationRunner.java[`JobLauncherApplicationRunner`] for details). If multiple `Job` beans are found, the job that should be executed must be specified using configprop:spring.batch.job.name[]. To disable running a `Job` found in the application context, set the configprop:spring.batch.job.enabled[] to `false`. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc index cf7a4a90cee5..e2882829707c 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc @@ -19,11 +19,11 @@ This is controlled through two external properties: [[howto.data-initialization.using-hibernate]] === Initialize a Database Using Hibernate -You can set `spring.jpa.hibernate.ddl-auto` explicitly and the standard Hibernate property values are `none`, `validate`, `update`, `create`, and `create-drop`. +You can set `spring.jpa.hibernate.ddl-auto` explicitly, and the standard Hibernate property values are `none`, `validate`, `update`, `create`, and `create-drop`. Spring Boot chooses a default value for you based on whether it thinks your database is embedded. It defaults to `create-drop` if no schema manager has been detected or `none` in all other cases. An embedded database is detected by looking at the `Connection` type and JDBC url. -`hsqldb`, `h2`, and `derby` are candidates, and others are not. +`hsqldb`, `h2`, and `derby` are candidates, while others are not. Be careful when switching from in-memory to a '`real`' database that you do not make assumptions about the existence of the tables and data in the new platform. You either have to set `ddl-auto` explicitly or use one of the other mechanisms to initialize the database. @@ -41,8 +41,8 @@ It is a Hibernate feature (and has nothing to do with Spring). Spring Boot can automatically create the schema (DDL scripts) of your JDBC `DataSource` or R2DBC `ConnectionFactory` and initialize its data (DML scripts). By default, it loads schema scripts from `optional:classpath*:schema.sql` and data scripts from `optional:classpath*:data.sql`. -The locations of these schema and data scripts can customized using configprop:spring.sql.init.schema-locations[] and configprop:spring.sql.init.data-locations[] respectively. -The `optional:` prefix means that the application will start when the files do not exist. +The locations of these schema and data scripts can be customized using configprop:spring.sql.init.schema-locations[] and configprop:spring.sql.init.data-locations[] respectively. +The `optional:` prefix means that the application will start even when the files do not exist. To have the application fail to start when the files are absent, remove the `optional:` prefix. In addition, Spring Boot processes the `optional:classpath*:schema-$\{platform}.sql` and `optional:classpath*:data-$\{platform}.sql` files (if present), where `$\{platform}` is the value of configprop:spring.sql.init.platform[]. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/docker-compose.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/docker-compose.adoc index 6d85a95109fc..d889637d1964 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/docker-compose.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/docker-compose.adoc @@ -33,6 +33,6 @@ With this Docker Compose file in place, the JDBC URL used is `jdbc:postgresql:// === Sharing services between multiple applications If you want to share services between multiple applications, create the `compose.yaml` file in one of the applications and then use the configuration property configprop:spring.docker.compose.file[] in the other applications to reference the `compose.yaml` file. -You should also set configprop:spring.docker.compose.lifecycle-management[] to `start-only`, as it defaults to `start-and-stop` and stopping one application would shut down the shared services for the other still running applications, too. -Setting it to `start-only` won't stop the shared services on application stop, but a caveat is that if you shut down all applications, the services stay running. +You should also set configprop:spring.docker.compose.lifecycle-management[] to `start-only`, as it defaults to `start-and-stop` and stopping one application would shut down the shared services for the other still running applications as well. +Setting it to `start-only` won't stop the shared services on application stop, but a caveat is that if you shut down all applications, the services remain running. You can stop the services manually by running `docker compose stop` on the command line in the directory which contains the `compose.yaml` file. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/logging.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/logging.adoc index 1f17bc00d6ce..5c97fa275c30 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/logging.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/logging.adoc @@ -1,9 +1,9 @@ [[howto.logging]] == Logging Spring Boot has no mandatory logging dependency, except for the Commons Logging API, which is typically provided by Spring Framework's `spring-jcl` module. -To use https://logback.qos.ch[Logback], you need to include it and `spring-jcl` on the classpath. +To use https://logback.qos.ch[Logback], you need to include it and `spring-jcl` in the classpath. The recommended way to do that is through the starters, which all depend on `spring-boot-starter-logging`. -For a web application, you need only `spring-boot-starter-web`, since it depends transitively on the logging starter. +For a web application, you only need `spring-boot-starter-web`, since it depends transitively on the logging starter. If you use Maven, the following dependency adds logging for you: [source,xml,indent=0,subs="verbatim"] @@ -27,7 +27,7 @@ If the only change you need to make to logging is to set the levels of various l org.hibernate: "error" ---- -You can also set the location of a file to which to write the log (in addition to the console) by using `logging.file.name`. +You can also set the location of a file to which the log will be written (in addition to the console) by using `logging.file.name`. To configure the more fine-grained settings of a logging system, you need to use the native configuration format supported by the `LoggingSystem` in question. By default, Spring Boot picks up the native configuration from its default location for the system (such as `classpath:logback.xml` for Logback), but you can set the location of the config file by using the configprop:logging.config[] property. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/nosql.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/nosql.adoc index c7be21231a0b..67fc1d167799 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/nosql.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/nosql.adoc @@ -9,9 +9,9 @@ This section answers questions that arise from using NoSQL with Spring Boot. === Use Jedis Instead of Lettuce By default, the Spring Boot starter (`spring-boot-starter-data-redis`) uses https://github.com/lettuce-io/lettuce-core/[Lettuce]. You need to exclude that dependency and include the https://github.com/xetorthio/jedis/[Jedis] one instead. -Spring Boot manages both of these dependencies so you can switch to Jedis without specifying a version. +Spring Boot manages both of these dependencies, allowing you to switch to Jedis without specifying a version. -The following example shows how to do so in Maven: +The following example shows how to accomplish this in Maven: [source,xml,indent=0,subs="verbatim"] ---- @@ -31,7 +31,7 @@ The following example shows how to do so in Maven: ---- -The following example shows how to do so in Gradle: +The following example shows how to accomplish this in Gradle: [source,gradle,indent=0,subs="verbatim"] ---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/security.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/security.adoc index 9f7509e035f1..5f813b9c76fb 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/security.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/security.adoc @@ -8,7 +8,7 @@ For more about Spring Security, see the {spring-security}[Spring Security projec [[howto.security.switch-off-spring-boot-configuration]] === Switch off the Spring Boot Security Configuration -If you define a `@Configuration` with a `SecurityFilterChain` bean in your application, it switches off the default webapp security settings in Spring Boot. +If you define a `@Configuration` with a `SecurityFilterChain` bean in your application, this action switches off the default webapp security settings in Spring Boot. @@ -17,14 +17,14 @@ If you define a `@Configuration` with a `SecurityFilterChain` bean in your appli If you provide a `@Bean` of type `AuthenticationManager`, `AuthenticationProvider`, or `UserDetailsService`, the default `@Bean` for `InMemoryUserDetailsManager` is not created. This means you have the full feature set of Spring Security available (such as {spring-security-docs}/servlet/authentication/index.html[various authentication options]). -The easiest way to add user accounts is to provide your own `UserDetailsService` bean. +The easiest way to add user accounts is by providing your own `UserDetailsService` bean. [[howto.security.enable-https]] === Enable HTTPS When Running behind a Proxy Server Ensuring that all your main endpoints are only available over HTTPS is an important chore for any application. -If you use Tomcat as a servlet container, then Spring Boot adds Tomcat's own `RemoteIpValve` automatically if it detects some environment settings, and you should be able to rely on the `HttpServletRequest` to report whether it is secure or not (even downstream of a proxy server that handles the real SSL termination). +If you use Tomcat as a servlet container, then Spring Boot adds Tomcat's own `RemoteIpValve` automatically if it detects some environment settings, allowing you to rely on the `HttpServletRequest` to report whether it is secure or not (even downstream of a proxy server that handles the real SSL termination). The standard behavior is determined by the presence or absence of certain request headers (`x-forwarded-for` and `x-forwarded-proto`), whose names are conventional, so it should work with most front-end proxies. You can switch on the valve by adding some entries to `application.properties`, as shown in the following example: diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/testing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/testing.adoc index 2c76b8689b8f..f0a1a1f92327 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/testing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/testing.adoc @@ -12,7 +12,7 @@ For example, the test in the snippet below will run with an authenticated user t include::code:MySecurityTests[] -Spring Security provides comprehensive integration with Spring MVC Test and this can also be used when testing controllers using the `@WebMvcTest` slice and `MockMvc`. +Spring Security provides comprehensive integration with Spring MVC Test, and this can also be used when testing controllers using the `@WebMvcTest` slice and `MockMvc`. For additional details on Spring Security's testing support, see Spring Security's {spring-security-docs}/servlet/test/index.html[reference documentation]. From ac00a0c28b8be9ac84b1a03ec0d683de07f4c8e6 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 16 Jan 2024 12:42:38 -0800 Subject: [PATCH 1065/1215] Polish 'Improve reference documentation' See gh-38942 --- .../src/docs/asciidoc/howto/data-initialization.adoc | 2 +- .../spring-boot-docs/src/docs/asciidoc/howto/logging.adoc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc index e2882829707c..d602fed56b84 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc @@ -19,7 +19,7 @@ This is controlled through two external properties: [[howto.data-initialization.using-hibernate]] === Initialize a Database Using Hibernate -You can set `spring.jpa.hibernate.ddl-auto` explicitly, and the standard Hibernate property values are `none`, `validate`, `update`, `create`, and `create-drop`. +You can set `spring.jpa.hibernate.ddl-auto` explicitly to one of the standard Hibernate property values which are `none`, `validate`, `update`, `create`, and `create-drop`. Spring Boot chooses a default value for you based on whether it thinks your database is embedded. It defaults to `create-drop` if no schema manager has been detected or `none` in all other cases. An embedded database is detected by looking at the `Connection` type and JDBC url. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/logging.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/logging.adoc index 5c97fa275c30..50c3d0f52ecd 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/logging.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/logging.adoc @@ -1,7 +1,7 @@ [[howto.logging]] == Logging Spring Boot has no mandatory logging dependency, except for the Commons Logging API, which is typically provided by Spring Framework's `spring-jcl` module. -To use https://logback.qos.ch[Logback], you need to include it and `spring-jcl` in the classpath. +To use https://logback.qos.ch[Logback], you need to include it and `spring-jcl` on the classpath. The recommended way to do that is through the starters, which all depend on `spring-boot-starter-logging`. For a web application, you only need `spring-boot-starter-web`, since it depends transitively on the logging starter. If you use Maven, the following dependency adds logging for you: From 072d6dadcbe6b8a178a6cdd31269829ddce3d853 Mon Sep 17 00:00:00 2001 From: Olga MaciaszekSharma Date: Tue, 16 Jan 2024 16:19:42 +0100 Subject: [PATCH 1066/1215] Remove OCI starter info from README The project has been archived and is no longer maintained. See gh-39145 --- spring-boot-project/spring-boot-starters/README.adoc | 3 --- 1 file changed, 3 deletions(-) diff --git a/spring-boot-project/spring-boot-starters/README.adoc b/spring-boot-project/spring-boot-starters/README.adoc index b857a9598a54..9c4e2a97af63 100644 --- a/spring-boot-project/spring-boot-starters/README.adoc +++ b/spring-boot-project/spring-boot-starters/README.adoc @@ -190,9 +190,6 @@ do as they were designed before this was clarified. | https://www.optaplanner.org/[OptaPlanner] | https://github.com/kiegroup/optaplanner/tree/master/optaplanner-spring-integration/optaplanner-spring-boot-starter -| https://www.oracle.com/cloud/[Oracle Cloud Infrastructure (OCI)] -| https://github.com/oracle/spring-cloud-oci/tree/main/spring-cloud-oci-starters - | https://spring.coherence.community/3.0.0/refdocs/reference/html/spring-boot.html[Oracle Coherence] | https://github.com/coherence-community/coherence-spring/tree/main/coherence-spring-boot-starter From 6845f42f7072b5b9f531229863ed94047da1aa0d Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 17 Jan 2024 14:12:34 +0100 Subject: [PATCH 1067/1215] Document virtual threads limitations This commit adds a new section in the Spring Boot reference documentation to mention potential throughput limitations with Java virtual threads support. This section links to the official Java documentation which expands much more on this matter. Closes gh-38883 --- .../src/docs/asciidoc/features/spring-application.adoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc index 3601cde03b2d..6618484a237b 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc @@ -384,6 +384,9 @@ Spring Boot can also be configured to expose a {spring-boot-actuator-restapi-doc === Virtual threads If you're running on Java 21 or up, you can enable virtual threads by setting the property configprop:spring.threads.virtual.enabled[] to `true`. +Before turning on this option for your application, you should consider https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html[reading the official Java virtual threads documentation]. +In some cases, applications can experience lower throughput because of "Pinned Virtual Threads"; this page also explains how to detect such cases with JDK Flight Recorder or the `jcmd` CLI. + WARNING: One side effect of virtual threads is that these threads are daemon threads. A JVM will exit if there are no non-daemon threads. This behavior can be a problem when you rely on, e.g. `@Scheduled` beans to keep your application alive. @@ -391,3 +394,4 @@ If you use virtual threads, the scheduler thread is a virtual thread and therefo This does not only affect scheduling, but can be the case with other technologies, too! To keep the JVM running in all cases, it is recommended to set the property configprop:spring.main.keep-alive[] to `true`. This ensures that the JVM is kept alive, even if all threads are virtual threads. + From cfc9b5109ad3ec5858203f1c2e2fdefaf685e979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Wed, 17 Jan 2024 14:18:19 +0100 Subject: [PATCH 1068/1215] Update CRaC support status link See gh-39170 --- .../src/docs/asciidoc/deployment/efficient.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc index 1ecbedd6af9c..37ee8b8fade2 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/deployment/efficient.adoc @@ -78,7 +78,7 @@ Then at some point, potentially after some workloads that will warm up your JVM A memory representation of the running JVM, including its warmness, is then serialized to disk, allowing a fast restoration at a later point, potentially on another machine with a similar operating system and CPU architecture. The restored process retains all the capabilities of the HotSpot JVM, including further JIT optimizations at runtime. -Based on the foundations provided by Spring Framework, Spring Boot provides support for checkpointing and restoring your application, and manages out-of-the-box the lifecycle of resources such as socket, files and thread pools https://github.com/spring-projects/spring-checkpoint-restore-smoke-tests/blob/main/STATUS.adoc[on a limited scope]. +Based on the foundations provided by Spring Framework, Spring Boot provides support for checkpointing and restoring your application, and manages out-of-the-box the lifecycle of resources such as socket, files and thread pools https://github.com/spring-projects/spring-lifecycle-smoke-tests/blob/main/STATUS.adoc[on a limited scope]. Additional lifecycle management is expected for other dependencies and potentially for the application code dealing with such resources. You can find more details about the two modes supported ("on demand checkpoint/restore of a running application" and "automatic checkpoint/restore at startup"), how to enable checkpoint and restore support and some guidelines in {spring-framework-docs}/integration/checkpoint-restore.html[the Spring Framework JVM Checkpoint Restore support documentation]. From 7d4d8e955e8e7f4d3f9cabd36f1bd1d8f5c327eb Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 17 Jan 2024 14:30:15 +0100 Subject: [PATCH 1069/1215] Register classes that implement multiple Servlet interfaces Closes gh-39056 --- .../ServletContextInitializerBeans.java | 48 ++++++++++++-- .../ServletContextInitializerBeansTests.java | 33 +++++++++- .../TestServletAndFilterAndListener.java | 64 +++++++++++++++++++ 3 files changed, 137 insertions(+), 8 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/TestServletAndFilterAndListener.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletContextInitializerBeans.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletContextInitializerBeans.java index 7a5c24cc1d31..e771b5ce26d2 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletContextInitializerBeans.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletContextInitializerBeans.java @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.EventListener; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; @@ -67,7 +68,7 @@ public class ServletContextInitializerBeans extends AbstractCollection seen = new HashSet<>(); + private final Seen seen = new Seen(); private final MultiValueMap, ServletContextInitializer> initializers; @@ -129,7 +130,7 @@ private void addServletContextInitializerBean(Class type, String beanName, Se this.initializers.add(type, initializer); if (source != null) { // Mark the underlying source as seen in case it wraps an existing bean - this.seen.add(source); + this.seen.add(type, source); } if (logger.isTraceEnabled()) { String resourceDescription = getResourceDescription(beanName, beanFactory); @@ -174,7 +175,7 @@ private void addAsRegistrationBean(ListableBeanFactory beanFact for (Entry entry : entries) { String beanName = entry.getKey(); B bean = entry.getValue(); - if (this.seen.add(bean)) { + if (this.seen.add(type, bean)) { // One that we haven't already seen RegistrationBean registration = adapter.createRegistrationBean(beanName, bean, entries.size()); int order = getOrder(bean); @@ -198,17 +199,17 @@ public int getOrder(Object obj) { } private List> getOrderedBeansOfType(ListableBeanFactory beanFactory, Class type) { - return getOrderedBeansOfType(beanFactory, type, Collections.emptySet()); + return getOrderedBeansOfType(beanFactory, type, Seen.empty()); } private List> getOrderedBeansOfType(ListableBeanFactory beanFactory, Class type, - Set excludes) { + Seen seen) { String[] names = beanFactory.getBeanNamesForType(type, true, false); Map map = new LinkedHashMap<>(); for (String name : names) { - if (!excludes.contains(name) && !ScopedProxyUtils.isScopedTarget(name)) { + if (!seen.contains(type, name) && !ScopedProxyUtils.isScopedTarget(name)) { T bean = beanFactory.getBean(name, type); - if (!excludes.contains(bean)) { + if (!seen.contains(type, bean)) { map.put(name, bean); } } @@ -310,4 +311,37 @@ public RegistrationBean createRegistrationBean(String name, EventListener source } + private static final class Seen { + + private final Map, Set> seen = new HashMap<>(); + + boolean add(Class type, Object object) { + if (contains(type, object)) { + return false; + } + return this.seen.computeIfAbsent(type, (ignore) -> new HashSet<>()).add(object); + } + + boolean contains(Class type, Object object) { + if (this.seen.isEmpty()) { + return false; + } + // If it has been directly seen, or the implemented ServletContextInitializer + // has been seen already + if (type != ServletContextInitializer.class + && this.seen.getOrDefault(type, Collections.emptySet()).contains(object)) { + return true; + } + if (this.seen.getOrDefault(ServletContextInitializer.class, Collections.emptySet()).contains(object)) { + return true; + } + return false; + } + + static Seen empty() { + return new Seen(); + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/ServletContextInitializerBeansTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/ServletContextInitializerBeansTests.java index 2f8272265c6a..62b0c1d59db5 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/ServletContextInitializerBeansTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/ServletContextInitializerBeansTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -38,6 +38,7 @@ * Tests for {@link ServletContextInitializerBeans}. * * @author Andy Wilkinson + * @author Moritz Halbritter */ class ServletContextInitializerBeansTests { @@ -82,6 +83,26 @@ void whenAnHttpSessionIdListenerBeanIsDefinedThenARegistrationBeanIsCreatedForIt .isInstanceOf(HttpSessionIdListener.class); } + @Test + void classesThatImplementMultipleInterfacesAreRegisteredForAllOfThem() { + load(MultipleInterfacesConfiguration.class); + ServletContextInitializerBeans initializerBeans = new ServletContextInitializerBeans( + this.context.getBeanFactory()); + assertThat(initializerBeans).hasSize(3); + assertThat(initializerBeans).element(0) + .isInstanceOf(ServletRegistrationBean.class) + .extracting((initializer) -> ((ServletRegistrationBean) initializer).getServlet()) + .isInstanceOf(TestServletAndFilterAndListener.class); + assertThat(initializerBeans).element(1) + .isInstanceOf(FilterRegistrationBean.class) + .extracting((initializer) -> ((FilterRegistrationBean) initializer).getFilter()) + .isInstanceOf(TestServletAndFilterAndListener.class); + assertThat(initializerBeans).element(2) + .isInstanceOf(ServletListenerRegistrationBean.class) + .extracting((initializer) -> ((ServletListenerRegistrationBean) initializer).getListener()) + .isInstanceOf(TestServletAndFilterAndListener.class); + } + private void load(Class... configuration) { this.context = new AnnotationConfigApplicationContext(configuration); } @@ -106,6 +127,16 @@ TestFilter testFilter() { } + @Configuration(proxyBeanMethods = false) + static class MultipleInterfacesConfiguration { + + @Bean + TestServletAndFilterAndListener testServletAndFilterAndListener() { + return new TestServletAndFilterAndListener(); + } + + } + @Configuration(proxyBeanMethods = false) static class TestConfiguration { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/TestServletAndFilterAndListener.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/TestServletAndFilterAndListener.java new file mode 100644 index 000000000000..f3ed7aa2fae2 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/TestServletAndFilterAndListener.java @@ -0,0 +1,64 @@ +/* + * 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.web.servlet; + +import java.io.IOException; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletRequestListener; +import jakarta.servlet.ServletResponse; + +class TestServletAndFilterAndListener implements Servlet, Filter, ServletRequestListener { + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + + } + + @Override + public void init(ServletConfig servletConfig) throws ServletException { + + } + + @Override + public ServletConfig getServletConfig() { + return null; + } + + @Override + public void service(ServletRequest servletRequest, ServletResponse servletResponse) + throws ServletException, IOException { + + } + + @Override + public String getServletInfo() { + return null; + } + + @Override + public void destroy() { + + } + +} From c6bf1ac096009b9d8c50bc8e6a166307952238b4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 15:56:38 +0000 Subject: [PATCH 1070/1215] Upgrade to Byte Buddy 1.14.11 Closes gh-39184 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 54afb5db2aae..db982ef3af34 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -123,7 +123,7 @@ bom { ] } } - library("Byte Buddy", "1.14.10") { + library("Byte Buddy", "1.14.11") { group("net.bytebuddy") { modules = [ "byte-buddy", From 335256505195e15c2a3b35ff3b723c51521f8a17 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 15:56:43 +0000 Subject: [PATCH 1071/1215] Upgrade to Groovy 4.0.17 Closes gh-39185 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index db982ef3af34..d92c91d895d3 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -339,7 +339,7 @@ bom { ] } } - library("Groovy", "4.0.16") { + library("Groovy", "4.0.17") { group("org.apache.groovy") { imports = [ "groovy-bom" From 112b07d0d5be8a494e7e3244aaa82e765c703e3e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 15:56:47 +0000 Subject: [PATCH 1072/1215] Upgrade to jOOQ 3.18.9 Closes gh-39186 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d92c91d895d3..5a63cfc6d3b0 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -701,7 +701,7 @@ bom { ] } } - library("jOOQ", "3.18.7") { + library("jOOQ", "3.18.9") { group("org.jooq") { modules = [ "jooq", From c34d334afe3f12d0c2a9c9fc5b88992f2c559509 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 15:56:52 +0000 Subject: [PATCH 1073/1215] Upgrade to Kotlin 1.9.22 Closes gh-39187 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 39b223c7fb52..35feb8431abc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ commonsCodecVersion=1.16.0 hamcrestVersion=2.2 jacksonVersion=2.15.3 junitJupiterVersion=5.10.1 -kotlinVersion=1.9.21 +kotlinVersion=1.9.22 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.28 springFrameworkVersion=6.1.3 From f1bc7c91f5242c7b4d66f2dc531da4b068db9718 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 15:56:57 +0000 Subject: [PATCH 1074/1215] Upgrade to Lettuce 6.3.1.RELEASE Closes gh-39188 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5a63cfc6d3b0..c757f3d7fc74 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -825,7 +825,7 @@ bom { ] } } - library("Lettuce", "6.3.0.RELEASE") { + library("Lettuce", "6.3.1.RELEASE") { group("io.lettuce") { modules = [ "lettuce-core" From c151a8d0dd048df25c6783792c4a139eea8aea0c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 15:57:02 +0000 Subject: [PATCH 1075/1215] Upgrade to Netty 4.1.105.Final Closes gh-39189 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c757f3d7fc74..0715a9a81ceb 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1085,7 +1085,7 @@ bom { ] } } - library("Netty", "4.1.104.Final") { + library("Netty", "4.1.105.Final") { group("io.netty") { imports = [ "netty-bom" From 0ae22fd89f98520d89d6ebb41a092552f4daff99 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 15:57:06 +0000 Subject: [PATCH 1076/1215] Upgrade to Pulsar 3.1.2 Closes gh-39190 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 0715a9a81ceb..e7c0f37f12bf 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1141,7 +1141,7 @@ bom { ] } } - library("Pulsar", "3.1.1") { + library("Pulsar", "3.1.2") { group("org.apache.pulsar") { modules = [ "bouncy-castle-bc", From bb2182cffd97dc4d45aa12a037e971adeda4b8dd Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 15:57:11 +0000 Subject: [PATCH 1077/1215] Upgrade to Pulsar Reactive 0.5.2 Closes gh-39191 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e7c0f37f12bf..fe72bdc80f86 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1220,7 +1220,7 @@ bom { ] } } - library("Pulsar Reactive", "0.5.1") { + library("Pulsar Reactive", "0.5.2") { group("org.apache.pulsar") { modules = [ "pulsar-client-reactive-adapter", From 41f4111faf3cc2957a892ec61b8c411a38140ce8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 15:57:16 +0000 Subject: [PATCH 1078/1215] Upgrade to R2DBC MySQL 1.0.6 Closes gh-39192 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index fe72bdc80f86..7d65333d17d0 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1271,7 +1271,7 @@ bom { ] } } - library("R2DBC MySQL", "1.0.5") { + library("R2DBC MySQL", "1.0.6") { group("io.asyncer") { modules = [ "r2dbc-mysql" From c7b769673cf0db8cb16c2b1794952fb67be891a6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 15:57:20 +0000 Subject: [PATCH 1079/1215] Upgrade to R2DBC Postgresql 1.0.4.RELEASE Closes gh-39193 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 7d65333d17d0..59bfb0aede59 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1286,7 +1286,7 @@ bom { ] } } - library("R2DBC Postgresql", "1.0.3.RELEASE") { + library("R2DBC Postgresql", "1.0.4.RELEASE") { considerSnapshots() group("org.postgresql") { modules = [ From c901d09ecda42b1edfbe6ab15c4125e16e5f052a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 15:57:25 +0000 Subject: [PATCH 1080/1215] Upgrade to R2DBC Proxy 1.1.4.RELEASE Closes gh-39194 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 59bfb0aede59..240e59778caa 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1294,7 +1294,7 @@ bom { ] } } - library("R2DBC Proxy", "1.1.3.RELEASE") { + library("R2DBC Proxy", "1.1.4.RELEASE") { considerSnapshots() group("io.r2dbc") { modules = [ From 5a6c7245064adeb39e688d32c809870388f7d370 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 15:57:30 +0000 Subject: [PATCH 1081/1215] Upgrade to SLF4J 2.0.11 Closes gh-39195 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 240e59778caa..5c950fa0907c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1475,7 +1475,7 @@ bom { ] } } - library("SLF4J", "2.0.9") { + library("SLF4J", "2.0.11") { group("org.slf4j") { modules = [ "jcl-over-slf4j", From f6fbd105f0e6d57021a64df1b45ea4708a67449c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 15:57:35 +0000 Subject: [PATCH 1082/1215] Upgrade to Tomcat 10.1.18 Closes gh-39196 --- gradle.properties | 2 +- .../springframework/boot/web/server/mime-mappings.properties | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 35feb8431abc..d6dd96dafc1a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,6 +13,6 @@ kotlinVersion=1.9.22 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.28 springFrameworkVersion=6.1.3 -tomcatVersion=10.1.17 +tomcatVersion=10.1.18 kotlin.stdlib.default.dependency=false diff --git a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/web/server/mime-mappings.properties b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/web/server/mime-mappings.properties index 4723c0b9327c..f74b7c3e1606 100644 --- a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/web/server/mime-mappings.properties +++ b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/web/server/mime-mappings.properties @@ -1,4 +1,4 @@ -# Copyright 2012-2022 the original author or authors. +# 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. @@ -293,6 +293,7 @@ gdl=model/vnd.gdl geo=application/vnd.dynageo gex=application/vnd.geometry-explorer ggb=application/vnd.geogebra.file +ggs=application/vnd.geogebra.slides ggt=application/vnd.geogebra.tool ghf=application/vnd.groove-help gif=image/gif @@ -469,6 +470,7 @@ mif=application/x-mif mime=message/rfc822 mj2=video/mj2 mjp2=video/mj2 +mjs=text/javascript mk3d=video/x-matroska mka=audio/x-matroska mks=video/x-matroska @@ -576,6 +578,7 @@ onetoc2=application/onenote opf=application/oebps-package+xml opml=text/x-opml oprc=application/vnd.palm +opus=audio/ogg org=application/vnd.lotus-organizer osf=application/vnd.yamaha.openscoreformat osfpvg=application/vnd.yamaha.openscoreformat.osfpvg+xml From dbfd038a4d133d6f38d9e718d178376a17fed5ba Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Wed, 17 Jan 2024 12:05:18 -0600 Subject: [PATCH 1083/1215] Validate combined RestTemplate and RestClient usage in mock REST config Fixes gh-38820 --- ...ockRestServiceServerAutoConfiguration.java | 11 ++- ...AndRestClientTogetherIntegrationTests.java | 83 +++++++++++++++++++ 2 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestRestTemplateAndRestClientTogetherIntegrationTests.java diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/MockRestServiceServerAutoConfiguration.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/MockRestServiceServerAutoConfiguration.java index 6aba6245a020..a0ada9a512c4 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/MockRestServiceServerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/client/MockRestServiceServerAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -136,9 +136,12 @@ private RequestExpectationManager getDelegate() { .getExpectationManagers(); Map restClientExpectationManagers = this.restClientCustomizer .getExpectationManagers(); - Assert.state(!(restTemplateExpectationManagers.isEmpty() && restClientExpectationManagers.isEmpty()), - "Unable to use auto-configured MockRestServiceServer since " - + "a mock server customizer has not been bound to a RestTemplate or RestClient"); + boolean neitherBound = restTemplateExpectationManagers.isEmpty() && restClientExpectationManagers.isEmpty(); + boolean bothBound = !restTemplateExpectationManagers.isEmpty() && !restClientExpectationManagers.isEmpty(); + Assert.state(!neitherBound, "Unable to use auto-configured MockRestServiceServer since " + + "a mock server customizer has not been bound to a RestTemplate or RestClient"); + Assert.state(!bothBound, "Unable to use auto-configured MockRestServiceServer since " + + "mock server customizers have been bound to both a RestTemplate and a RestClient"); if (!restTemplateExpectationManagers.isEmpty()) { Assert.state(restTemplateExpectationManagers.size() == 1, "Unable to use auto-configured MockRestServiceServer since " diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestRestTemplateAndRestClientTogetherIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestRestTemplateAndRestClientTogetherIntegrationTests.java new file mode 100644 index 000000000000..92edda4ef598 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientTestRestTemplateAndRestClientTogetherIntegrationTests.java @@ -0,0 +1,83 @@ +/* + * 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.test.autoconfigure.web.client; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.web.client.MockServerRestClientCustomizer; +import org.springframework.boot.test.web.client.MockServerRestTemplateCustomizer; +import org.springframework.http.MediaType; +import org.springframework.test.web.client.MockRestServiceServer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * Tests for {@link RestClientTest @RestClientTest} with a {@code RestTemplate} and a + * {@code RestClient} clients. + * + * @author Scott Frederick + */ +@RestClientTest({ ExampleRestTemplateService.class, ExampleRestClientService.class }) +class RestClientTestRestTemplateAndRestClientTogetherIntegrationTests { + + @Autowired + private ExampleRestTemplateService restTemplateClient; + + @Autowired + private ExampleRestClientService restClientClient; + + @Autowired + private MockServerRestTemplateCustomizer templateCustomizer; + + @Autowired + private MockServerRestClientCustomizer clientCustomizer; + + @Autowired + private MockRestServiceServer server; + + @Test + void serverShouldNotWork() { + assertThatIllegalStateException().isThrownBy( + () -> this.server.expect(requestTo(uri("/test"))).andRespond(withSuccess("hello", MediaType.TEXT_HTML))) + .withMessageContaining("Unable to use auto-configured"); + } + + @Test + void restTemplateClientRestCallViaCustomizer() { + this.templateCustomizer.getServer() + .expect(requestTo("/test")) + .andRespond(withSuccess("hello", MediaType.TEXT_HTML)); + assertThat(this.restTemplateClient.test()).isEqualTo("hello"); + } + + @Test + void restClientClientRestCallViaCustomizer() { + this.clientCustomizer.getServer() + .expect(requestTo(uri("/test"))) + .andRespond(withSuccess("there", MediaType.TEXT_HTML)); + assertThat(this.restClientClient.test()).isEqualTo("there"); + } + + private static String uri(String path) { + return "https://example.com" + path; + } + +} From cc05d6fc53b67739b5a03b3d454246e9189a681a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 18:54:38 +0000 Subject: [PATCH 1084/1215] Upgrade to Brave 5.17.1 Closes gh-39201 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 7b4aabe872c1..e537407bca74 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -109,7 +109,7 @@ bom { ] } } - library("Brave", "5.17.0") { + library("Brave", "5.17.1") { group("io.zipkin.brave") { imports = [ "brave-bom" From 77c9000586984be988bbaf1c301933e27c1ec812 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 18:54:44 +0000 Subject: [PATCH 1085/1215] Upgrade to jOOQ 3.19.2 Closes gh-39202 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e537407bca74..5bc496fc13c9 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -702,7 +702,7 @@ bom { ] } } - library("jOOQ", "3.19.1") { + library("jOOQ", "3.19.2") { group("org.jooq") { modules = [ "jooq", From 24942c17ec3c799640a3effb8132bda43492ba68 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 18:54:49 +0000 Subject: [PATCH 1086/1215] Upgrade to Lettuce 6.3.1.RELEASE Closes gh-39203 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5bc496fc13c9..54285a046cb1 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -826,7 +826,7 @@ bom { ] } } - library("Lettuce", "6.3.0.RELEASE") { + library("Lettuce", "6.3.1.RELEASE") { group("io.lettuce") { modules = [ "lettuce-core" From bfe7137817124877b686a5a8c09085d5558a7d32 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 18:54:55 +0000 Subject: [PATCH 1087/1215] Upgrade to Maven Failsafe Plugin 3.2.5 Closes gh-39204 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 54285a046cb1..40ebe50964b0 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -923,7 +923,7 @@ bom { ] } } - library("Maven Failsafe Plugin", "3.2.3") { + library("Maven Failsafe Plugin", "3.2.5") { group("org.apache.maven.plugins") { plugins = [ "maven-failsafe-plugin" From e129f5cc6403d16ed4701be177ea4ee2652ffcd9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 18:55:01 +0000 Subject: [PATCH 1088/1215] Upgrade to Maven Surefire Plugin 3.2.5 Closes gh-39205 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 40ebe50964b0..7288a9f5b0c9 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -986,7 +986,7 @@ bom { ] } } - library("Maven Surefire Plugin", "3.2.3") { + library("Maven Surefire Plugin", "3.2.5") { group("org.apache.maven.plugins") { plugins = [ "maven-surefire-plugin" From 11b1a91706cf316b63e402fb67ffcccdd74c9c42 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 18:55:07 +0000 Subject: [PATCH 1089/1215] Upgrade to Mockito 5.9.0 Closes gh-39206 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 7288a9f5b0c9..e495eb929b37 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1022,7 +1022,7 @@ bom { ] } } - library("Mockito", "5.8.0") { + library("Mockito", "5.9.0") { group("org.mockito") { imports = [ "mockito-bom" From e588fcb4d15cb05646a79953a5cb69a19fb95d23 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 18:55:12 +0000 Subject: [PATCH 1090/1215] Upgrade to Netty 4.1.105.Final Closes gh-39207 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e495eb929b37..f99e82cf57df 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1086,7 +1086,7 @@ bom { ] } } - library("Netty", "4.1.104.Final") { + library("Netty", "4.1.105.Final") { group("io.netty") { imports = [ "netty-bom" From a2f34f604a095f1958b1fa381972d80401928d42 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 18:55:17 +0000 Subject: [PATCH 1091/1215] Upgrade to Pulsar Reactive 0.5.2 Closes gh-39208 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f99e82cf57df..2e75cf524c01 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1221,7 +1221,7 @@ bom { ] } } - library("Pulsar Reactive", "0.5.1") { + library("Pulsar Reactive", "0.5.2") { group("org.apache.pulsar") { modules = [ "pulsar-client-reactive-adapter", From 0d02f216a99a3756fbb960906ef8f33c7a8f9a63 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 18:55:22 +0000 Subject: [PATCH 1092/1215] Upgrade to R2DBC MySQL 1.0.6 Closes gh-39209 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 2e75cf524c01..5609c57f0c56 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1272,7 +1272,7 @@ bom { ] } } - library("R2DBC MySQL", "1.0.5") { + library("R2DBC MySQL", "1.0.6") { group("io.asyncer") { modules = [ "r2dbc-mysql" From 6c9c6ab367d46f0a465ca8220f8f8cd0ce8dcebc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 18:55:27 +0000 Subject: [PATCH 1093/1215] Upgrade to R2DBC Postgresql 1.0.4.RELEASE Closes gh-39210 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5609c57f0c56..978aecd96595 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1287,7 +1287,7 @@ bom { ] } } - library("R2DBC Postgresql", "1.0.3.RELEASE") { + library("R2DBC Postgresql", "1.0.4.RELEASE") { considerSnapshots() group("org.postgresql") { modules = [ From cbbb727f9e2f0cfacfa2963ecff7e0c97819c79f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 18:55:32 +0000 Subject: [PATCH 1094/1215] Upgrade to R2DBC Proxy 1.1.4.RELEASE Closes gh-39211 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 978aecd96595..61ff967ce017 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1295,7 +1295,7 @@ bom { ] } } - library("R2DBC Proxy", "1.1.3.RELEASE") { + library("R2DBC Proxy", "1.1.4.RELEASE") { considerSnapshots() group("io.r2dbc") { modules = [ From b5244ec0a20447c123e1104a54c9a2243cc367d4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 18:55:37 +0000 Subject: [PATCH 1095/1215] Upgrade to SLF4J 2.0.11 Closes gh-39212 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 61ff967ce017..7575a03eae53 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1476,7 +1476,7 @@ bom { ] } } - library("SLF4J", "2.0.10") { + library("SLF4J", "2.0.11") { group("org.slf4j") { modules = [ "jcl-over-slf4j", From eeb508112e9ae5f2b9deb98f9c822c907c73565e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 18:55:42 +0000 Subject: [PATCH 1096/1215] Upgrade to Tomcat 10.1.18 Closes gh-39213 --- gradle.properties | 2 +- .../springframework/boot/web/server/mime-mappings.properties | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index b50c786ea445..f4ad205c4196 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,6 +13,6 @@ kotlinVersion=1.9.22 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.28 springFrameworkVersion=6.1.3 -tomcatVersion=10.1.17 +tomcatVersion=10.1.18 kotlin.stdlib.default.dependency=false diff --git a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/web/server/mime-mappings.properties b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/web/server/mime-mappings.properties index 4723c0b9327c..f74b7c3e1606 100644 --- a/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/web/server/mime-mappings.properties +++ b/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/web/server/mime-mappings.properties @@ -1,4 +1,4 @@ -# Copyright 2012-2022 the original author or authors. +# 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. @@ -293,6 +293,7 @@ gdl=model/vnd.gdl geo=application/vnd.dynageo gex=application/vnd.geometry-explorer ggb=application/vnd.geogebra.file +ggs=application/vnd.geogebra.slides ggt=application/vnd.geogebra.tool ghf=application/vnd.groove-help gif=image/gif @@ -469,6 +470,7 @@ mif=application/x-mif mime=message/rfc822 mj2=video/mj2 mjp2=video/mj2 +mjs=text/javascript mk3d=video/x-matroska mka=audio/x-matroska mks=video/x-matroska @@ -576,6 +578,7 @@ onetoc2=application/onenote opf=application/oebps-package+xml opml=text/x-opml oprc=application/vnd.palm +opus=audio/ogg org=application/vnd.lotus-organizer osf=application/vnd.yamaha.openscoreformat osfpvg=application/vnd.yamaha.openscoreformat.osfpvg+xml From fee359ff5ec26f86c29f42c9a1b99811137fde1f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 17 Jan 2024 20:11:59 +0000 Subject: [PATCH 1097/1215] Downgrade to production-ready version of Oracle Database This reverts commit d2325d111091122478200436a8b9e9dc98eeec59. Closes gh-38943 --- .../jdbc/OracleUcpDataSourceConfigurationTests.java | 7 +++---- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpDataSourceConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpDataSourceConfigurationTests.java index 971ab21d37a5..a31f631d3b92 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpDataSourceConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/OracleUcpDataSourceConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -17,7 +17,6 @@ package org.springframework.boot.autoconfigure.jdbc; import java.sql.Connection; -import java.time.Duration; import javax.sql.DataSource; @@ -83,10 +82,10 @@ void testDataSourceDefaultsPreserved() { this.contextRunner.run((context) -> { PoolDataSourceImpl ds = context.getBean(PoolDataSourceImpl.class); assertThat(ds.getInitialPoolSize()).isZero(); - assertThat(ds.getMinPoolSize()).isEqualTo(1); + assertThat(ds.getMinPoolSize()).isZero(); assertThat(ds.getMaxPoolSize()).isEqualTo(Integer.MAX_VALUE); assertThat(ds.getInactiveConnectionTimeout()).isZero(); - assertThat(ds.getConnectionWaitDuration()).isEqualTo(Duration.ofSeconds(3)); + assertThat(ds.getConnectionWaitTimeout()).isEqualTo(3); assertThat(ds.getTimeToLiveConnectionTimeout()).isZero(); assertThat(ds.getAbandonedConnectionTimeout()).isZero(); assertThat(ds.getTimeoutCheckInterval()).isEqualTo(30); diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 5c950fa0907c..63c2c287ab98 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1106,7 +1106,7 @@ bom { ] } } - library("Oracle Database", "23.3.0.23.09") { + library("Oracle Database", "21.9.0.0") { group("com.oracle.database.jdbc") { imports = [ "ojdbc-bom" From eb0040c225c31213320e068b0e1361a5d1872596 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 17 Jan 2024 13:14:09 -0800 Subject: [PATCH 1098/1215] Fix ZipCentralDirectoryFileHeaderRecord entry comment read offset Update `ZipCentralDirectoryFileHeaderRecord.copyTo` comment read offset to account for the record position. Fixes gh-39166 --- .../ZipCentralDirectoryFileHeaderRecord.java | 4 +- .../boot/loader/jar/NestedJarFileTests.java | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipCentralDirectoryFileHeaderRecord.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipCentralDirectoryFileHeaderRecord.java index a9ca0a54ad85..673620f155bd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipCentralDirectoryFileHeaderRecord.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/ZipCentralDirectoryFileHeaderRecord.java @@ -97,8 +97,8 @@ void copyTo(DataBlock dataBlock, long pos, ZipEntry zipEntry) throws IOException dataBlock.readFully(buffer, extraPos); zipEntry.setExtra(buffer.array()); } - if ((fileCommentLength() & 0xFFFF) > 0) { - long commentPos = MINIMUM_SIZE + fileNameLength + extraLength; + if (commentLength > 0) { + long commentPos = pos + MINIMUM_SIZE + fileNameLength + extraLength; zipEntry.setComment(ZipString.readString(dataBlock, commentPos, commentLength)); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java index ac6da2187b87..782dd1369ac8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java @@ -23,10 +23,15 @@ import java.io.InputStream; import java.lang.ref.Cleaner.Cleanable; import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.Enumeration; +import java.util.List; +import java.util.UUID; import java.util.jar.JarEntry; import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; import java.util.jar.Manifest; +import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.assertj.core.extractor.Extractors; @@ -386,4 +391,42 @@ void versionedStreamStreamsEntries() throws IOException { } } + @Test // gh-39166 + void getCommentAlignsWithJdkJar() throws Exception { + File file = new File(this.tempDir, "testcomments.jar"); + try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(file))) { + jar.putNextEntry(new ZipEntry("BOOT-INF/")); + jar.closeEntry(); + jar.putNextEntry(new ZipEntry("BOOT-INF/classes/")); + jar.closeEntry(); + for (int i = 0; i < 5; i++) { + ZipEntry entry = new ZipEntry("BOOT-INF/classes/T" + i + ".class"); + entry.setComment("T" + i); + jar.putNextEntry(entry); + jar.write(UUID.randomUUID().toString().getBytes()); + jar.closeEntry(); + } + } + List jdk = collectComments(new JarFile(file)); + List nested = collectComments(new NestedJarFile(file, "BOOT-INF/classes/")); + assertThat(nested).isEqualTo(jdk); + } + + private List collectComments(JarFile jarFile) throws IOException { + try { + List comments = new ArrayList<>(); + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + String comment = entries.nextElement().getComment(); + if (comment != null) { + comments.add(comment); + } + } + return comments; + } + finally { + jarFile.close(); + } + } + } From 961da4e4280d6468a826e68e974a21b93db3b3ca Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 18 Jan 2024 14:14:39 +0000 Subject: [PATCH 1099/1215] Make user details only back off without custom username or password Closes gh-38864 --- ...veUserDetailsServiceAutoConfiguration.java | 37 ++++++++++++++++--- .../UserDetailsServiceAutoConfiguration.java | 36 ++++++++++++++++-- ...rDetailsServiceAutoConfigurationTests.java | 14 ++++++- ...rDetailsServiceAutoConfigurationTests.java | 21 ++++++++++- 4 files changed, 96 insertions(+), 12 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration.java index 17d6e1315f54..596f0d9c0b9d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -29,6 +29,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration; import org.springframework.boot.autoconfigure.security.SecurityProperties; @@ -58,13 +59,12 @@ */ @AutoConfiguration(before = ReactiveSecurityAutoConfiguration.class, after = RSocketMessagingAutoConfiguration.class) @ConditionalOnClass({ ReactiveAuthenticationManager.class }) -@ConditionalOnMissingClass({ "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository", - "org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector" }) @ConditionalOnMissingBean( value = { ReactiveAuthenticationManager.class, ReactiveUserDetailsService.class, ReactiveAuthenticationManagerResolver.class }, type = { "org.springframework.security.oauth2.jwt.ReactiveJwtDecoder" }) -@Conditional(ReactiveUserDetailsServiceAutoConfiguration.ReactiveUserDetailsServiceCondition.class) +@Conditional({ ReactiveUserDetailsServiceAutoConfiguration.RSocketEnabledOrReactiveWebApplication.class, + ReactiveUserDetailsServiceAutoConfiguration.MissingAlternativeOrUserPropertiesConfigured.class }) @EnableConfigurationProperties(SecurityProperties.class) public class ReactiveUserDetailsServiceAutoConfiguration { @@ -98,9 +98,9 @@ private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder return NOOP_PASSWORD_PREFIX + password; } - static class ReactiveUserDetailsServiceCondition extends AnyNestedCondition { + static class RSocketEnabledOrReactiveWebApplication extends AnyNestedCondition { - ReactiveUserDetailsServiceCondition() { + RSocketEnabledOrReactiveWebApplication() { super(ConfigurationPhase.REGISTER_BEAN); } @@ -116,4 +116,29 @@ static class ReactiveWebApplicationCondition { } + static final class MissingAlternativeOrUserPropertiesConfigured extends AnyNestedCondition { + + MissingAlternativeOrUserPropertiesConfigured() { + super(ConfigurationPhase.PARSE_CONFIGURATION); + } + + @ConditionalOnMissingClass({ + "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository", + "org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenIntrospector" }) + static final class MissingAlternative { + + } + + @ConditionalOnProperty(prefix = "spring.security.user", name = "name") + static final class NameConfigured { + + } + + @ConditionalOnProperty(prefix = "spring.security.user", name = "password") + static final class PasswordConfigured { + + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfiguration.java index 396b50856c34..9c4fef69ea0a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -25,12 +25,16 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration.MissingAlternativeOrUserPropertiesConfigured; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManagerResolver; import org.springframework.security.authentication.AuthenticationProvider; @@ -53,9 +57,7 @@ */ @AutoConfiguration @ConditionalOnClass(AuthenticationManager.class) -@ConditionalOnMissingClass({ "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository", - "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector", - "org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository" }) +@Conditional(MissingAlternativeOrUserPropertiesConfigured.class) @ConditionalOnBean(ObjectPostProcessor.class) @ConditionalOnMissingBean(value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class, AuthenticationManagerResolver.class }, type = "org.springframework.security.oauth2.jwt.JwtDecoder") @@ -93,4 +95,30 @@ private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder return NOOP_PASSWORD_PREFIX + password; } + static final class MissingAlternativeOrUserPropertiesConfigured extends AnyNestedCondition { + + MissingAlternativeOrUserPropertiesConfigured() { + super(ConfigurationPhase.PARSE_CONFIGURATION); + } + + @ConditionalOnMissingClass({ + "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository", + "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector", + "org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository" }) + static final class MissingAlternative { + + } + + @ConditionalOnProperty(prefix = "spring.security.user", name = "name") + static final class NameConfigured { + + } + + @ConditionalOnProperty(prefix = "spring.security.user", name = "password") + static final class PasswordConfigured { + + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfigurationTests.java index 96efa632a667..1b673b6dc962 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/reactive/ReactiveUserDetailsServiceAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -121,6 +121,18 @@ void doesNotConfigureDefaultUserIfResourceServerIsPresent() { this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ReactiveUserDetailsService.class)); } + @Test + void configuresDefaultUserWhenResourceServerIsPresentAndUsernameIsConfigured() { + this.contextRunner.withPropertyValues("spring.security.user.name=carol") + .run((context) -> assertThat(context).hasSingleBean(ReactiveUserDetailsService.class)); + } + + @Test + void configuresDefaultUserWhenResourceServerIsPresentAndPasswordIsConfigured() { + this.contextRunner.withPropertyValues("spring.security.user.password=p4ssw0rd") + .run((context) -> assertThat(context).hasSingleBean(ReactiveUserDetailsService.class)); + } + @Test void userDetailsServiceWhenPasswordEncoderAbsentAndDefaultPassword() { this.contextRunner diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfigurationTests.java index 660a2ea97247..9cffa54313ec 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/UserDetailsServiceAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -23,8 +23,10 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.logging.LogLevel; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.system.CapturedOutput; @@ -176,6 +178,23 @@ void userDetailsServiceWhenRelyingPartyRegistrationRepositoryPresent() { .run(((context) -> assertThat(context).doesNotHaveBean(InMemoryUserDetailsManager.class))); } + @Test + void userDetailsServiceWhenRelyingPartyRegistrationRepositoryPresentAndUsernameConfigured() { + this.contextRunner + .withClassLoader(new FilteredClassLoader(ClientRegistrationRepository.class, OpaqueTokenIntrospector.class)) + .withPropertyValues("spring.security.user.name=alice") + .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO)) + .run(((context) -> assertThat(context).hasSingleBean(InMemoryUserDetailsManager.class))); + } + + @Test + void userDetailsServiceWhenRelyingPartyRegistrationRepositoryPresentAndPasswordConfigured() { + this.contextRunner + .withClassLoader(new FilteredClassLoader(ClientRegistrationRepository.class, OpaqueTokenIntrospector.class)) + .withPropertyValues("spring.security.user.password=secret") + .run(((context) -> assertThat(context).hasSingleBean(InMemoryUserDetailsManager.class))); + } + private Function noOtherFormsOfAuthenticationOnTheClasspath() { return (contextRunner) -> contextRunner .withClassLoader(new FilteredClassLoader(ClientRegistrationRepository.class, OpaqueTokenIntrospector.class, From bf010b65323adf22fcc6690d408580a5d5b8bb73 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Fri, 19 Jan 2024 07:01:14 +0000 Subject: [PATCH 1100/1215] Next development version (v3.2.3-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d6dd96dafc1a..22d7baf4a925 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.2.2-SNAPSHOT +version=3.2.3-SNAPSHOT org.gradle.caching=true org.gradle.parallel=true From ca799f7b2151616a7c3d47747e58704ed5daa7ce Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 19 Jan 2024 15:31:27 +0100 Subject: [PATCH 1101/1215] Tolerate actuator endpoints with the same id Closes gh-39249 --- ...figurationMetadataAnnotationProcessor.java | 27 ++++++++++---- .../MetadataCollector.java | 29 ++++++++++++++- .../EndpointMetadataGenerationTests.java | 24 ++++++++++++- .../endpoint/SimpleEndpoint2.java | 35 ++++++++++++++++++ .../endpoint/SimpleEndpoint3.java | 36 +++++++++++++++++++ 5 files changed, 142 insertions(+), 9 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/SimpleEndpoint2.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/SimpleEndpoint3.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java index 93fe3392d29c..0c4fc7c15165 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -59,6 +59,7 @@ * @author Kris De Volder * @author Jonas Keßler * @author Scott Frederick + * @author Moritz Halbritter * @since 1.2.0 */ @SupportedAnnotationTypes({ ConfigurationMetadataAnnotationProcessor.AUTO_CONFIGURATION_ANNOTATION, @@ -291,18 +292,30 @@ private void processEndpoint(AnnotationMirror annotation, TypeElement element) { return; // Can't process that endpoint } String endpointKey = ItemMetadata.newItemMetadataPrefix("management.endpoint.", endpointId); - Boolean enabledByDefault = (Boolean) elementValues.get("enableByDefault"); + boolean enabledByDefault = (boolean) elementValues.getOrDefault("enableByDefault", true); String type = this.metadataEnv.getTypeUtils().getQualifiedName(element); - this.metadataCollector.add(ItemMetadata.newGroup(endpointKey, type, type, null)); - this.metadataCollector.add(ItemMetadata.newProperty(endpointKey, "enabled", Boolean.class.getName(), type, null, - String.format("Whether to enable the %s endpoint.", endpointId), - (enabledByDefault != null) ? enabledByDefault : true, null)); + this.metadataCollector.addIfAbsent(ItemMetadata.newGroup(endpointKey, type, type, null)); + this.metadataCollector.add( + ItemMetadata.newProperty(endpointKey, "enabled", Boolean.class.getName(), type, null, + "Whether to enable the %s endpoint.".formatted(endpointId), enabledByDefault, null), + (existing) -> checkEnabledValueMatchesExisting(existing, enabledByDefault, type)); if (hasMainReadOperation(element)) { - this.metadataCollector.add(ItemMetadata.newProperty(endpointKey, "cache.time-to-live", + this.metadataCollector.addIfAbsent(ItemMetadata.newProperty(endpointKey, "cache.time-to-live", Duration.class.getName(), type, null, "Maximum time that a response can be cached.", "0ms", null)); } } + private void checkEnabledValueMatchesExisting(ItemMetadata existing, boolean enabledByDefault, String sourceType) { + boolean existingDefaultValue = (boolean) existing.getDefaultValue(); + if (enabledByDefault == existingDefaultValue) { + return; + } + throw new IllegalStateException( + "Existing property '%s' from type %s has a conflicting value. Existing value: %b, new value from type %s: %b" + .formatted(existing.getName(), existing.getSourceType(), existingDefaultValue, sourceType, + enabledByDefault)); + } + private boolean hasMainReadOperation(TypeElement element) { for (ExecutableElement method : ElementFilter.methodsIn(element.getEnclosedElements())) { if (this.metadataEnv.getReadOperationAnnotation(method) != null diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataCollector.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataCollector.java index c6fe7f81d79e..2aa1a55a074e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataCollector.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataCollector.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * 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. @@ -20,6 +20,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.function.Consumer; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; @@ -35,6 +36,7 @@ * * @author Andy Wilkinson * @author Kris De Volder + * @author Moritz Halbritter * @since 1.2.2 */ public class MetadataCollector { @@ -76,6 +78,24 @@ public void add(ItemMetadata metadata) { this.metadataItems.add(metadata); } + public void add(ItemMetadata metadata, Consumer onConflict) { + ItemMetadata existing = find(metadata.getName()); + if (existing != null) { + onConflict.accept(existing); + return; + } + add(metadata); + } + + public boolean addIfAbsent(ItemMetadata metadata) { + ItemMetadata existing = find(metadata.getName()); + if (existing != null) { + return false; + } + add(metadata); + return true; + } + public boolean hasSimilarGroup(ItemMetadata metadata) { if (!metadata.isOfItemType(ItemMetadata.ItemType.GROUP)) { throw new IllegalStateException("item " + metadata + " must be a group"); @@ -105,6 +125,13 @@ public ConfigurationMetadata getMetadata() { return metadata; } + private ItemMetadata find(String name) { + return this.metadataItems.stream() + .filter((candidate) -> name.equals(candidate.getName())) + .findFirst() + .orElse(null); + } + private boolean shouldBeMerged(ItemMetadata itemMetadata) { String sourceType = itemMetadata.getSourceType(); return (sourceType != null && !deletedInCurrentBuild(sourceType) && !processedInCurrentBuild(sourceType)); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/EndpointMetadataGenerationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/EndpointMetadataGenerationTests.java index 4c77c1794b66..e9d88b28ba09 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/EndpointMetadataGenerationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/EndpointMetadataGenerationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -27,16 +27,20 @@ import org.springframework.boot.configurationsample.endpoint.DisabledEndpoint; import org.springframework.boot.configurationsample.endpoint.EnabledEndpoint; import org.springframework.boot.configurationsample.endpoint.SimpleEndpoint; +import org.springframework.boot.configurationsample.endpoint.SimpleEndpoint2; +import org.springframework.boot.configurationsample.endpoint.SimpleEndpoint3; import org.springframework.boot.configurationsample.endpoint.SpecificEndpoint; import org.springframework.boot.configurationsample.endpoint.incremental.IncrementalEndpoint; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatRuntimeException; /** * Metadata generation tests for Actuator endpoints. * * @author Stephane Nicoll * @author Scott Frederick + * @author Moritz Halbritter */ class EndpointMetadataGenerationTests extends AbstractMetadataGenerationTests { @@ -148,6 +152,24 @@ void incrementalEndpointBuildEnableSpecificEndpoint() { assertThat(metadata.getItems()).hasSize(3); } + @Test + void shouldTolerateEndpointWithSameId() { + ConfigurationMetadata metadata = compile(SimpleEndpoint.class, SimpleEndpoint2.class); + assertThat(metadata).has(Metadata.withGroup("management.endpoint.simple").fromSource(SimpleEndpoint.class)); + assertThat(metadata).has(enabledFlag("simple", "simple", true)); + assertThat(metadata).has(cacheTtl("simple")); + assertThat(metadata.getItems()).hasSize(3); + } + + @Test + void shouldFailIfEndpointWithSameIdButWithConflictingEnabledByDefaultSetting() { + assertThatRuntimeException().isThrownBy(() -> compile(SimpleEndpoint.class, SimpleEndpoint3.class)) + .havingRootCause() + .isInstanceOf(IllegalStateException.class) + .withMessage( + "Existing property 'management.endpoint.simple.enabled' from type org.springframework.boot.configurationsample.endpoint.SimpleEndpoint has a conflicting value. Existing value: true, new value from type org.springframework.boot.configurationsample.endpoint.SimpleEndpoint3: false"); + } + private Metadata.MetadataItemCondition enabledFlag(String endpointId, String endpointSuffix, Boolean defaultValue) { return Metadata.withEnabledFlag("management.endpoint." + endpointSuffix + ".enabled") .withDefaultValue(defaultValue) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/SimpleEndpoint2.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/SimpleEndpoint2.java new file mode 100644 index 000000000000..971064af4489 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/SimpleEndpoint2.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.configurationsample.endpoint; + +import org.springframework.boot.configurationsample.Endpoint; +import org.springframework.boot.configurationsample.ReadOperation; + +/** + * A simple endpoint with no default override, with the same id as {@link SimpleEndpoint}. + * + * @author Moritz Halbritter + */ +@Endpoint(id = "simple") +public class SimpleEndpoint2 { + + @ReadOperation + public String invoke() { + return "test"; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/SimpleEndpoint3.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/SimpleEndpoint3.java new file mode 100644 index 000000000000..48c88f16f410 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/endpoint/SimpleEndpoint3.java @@ -0,0 +1,36 @@ +/* + * 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.configurationsample.endpoint; + +import org.springframework.boot.configurationsample.Endpoint; +import org.springframework.boot.configurationsample.ReadOperation; + +/** + * A simple endpoint with no default override, with the same id as {@link SimpleEndpoint}, + * but not enabled by default. + * + * @author Moritz Halbritter + */ +@Endpoint(id = "simple", enableByDefault = false) +public class SimpleEndpoint3 { + + @ReadOperation + public String invoke() { + return "test"; + } + +} From a09cc22841aca96b12dd4b5180aaeddd346ee739 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 10 Aug 2022 17:12:56 +0100 Subject: [PATCH 1102/1215] Allow a WebEndpointTest to only run against certain infrastructure Closes gh-32054 --- .../endpoint/web/test/WebEndpointTest.java | 49 +++++++++++++++++-- ...EndpointTestInvocationContextProvider.java | 22 ++++----- 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTest.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTest.java index 395fd4de9ad4..86ffbea6cb08 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTest.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * 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. @@ -20,13 +20,18 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.List; +import java.util.function.Function; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTestInvocationContextProvider.WebEndpointsInvocationContext; +import org.springframework.context.ConfigurableApplicationContext; + /** - * Signals that a test should be performed against all web endpoint implementations - * (Jersey, Web MVC, and WebFlux) + * Signals that a test should be run against one or more of the web endpoint + * infrastructure implementations (Jersey, Web MVC, and WebFlux) * * @author Andy Wilkinson */ @@ -36,4 +41,42 @@ @ExtendWith(WebEndpointTestInvocationContextProvider.class) public @interface WebEndpointTest { + /** + * The infrastructure against which the test should run. + * @return the infrastructure to run the tests against + */ + Infrastructure[] infrastructure() default { Infrastructure.JERSEY, Infrastructure.MVC, Infrastructure.WEBFLUX }; + + enum Infrastructure { + + /** + * Actuator running on the Jersey-based infrastructure. + */ + JERSEY("Jersey", WebEndpointTestInvocationContextProvider::createJerseyContext), + + /** + * Actuator running on the WebMVC-based infrastructure. + */ + MVC("WebMvc", WebEndpointTestInvocationContextProvider::createWebMvcContext), + + /** + * Actuator running on the WebFlux-based infrastructure. + */ + WEBFLUX("WebFlux", WebEndpointTestInvocationContextProvider::createWebFluxContext); + + private final String name; + + private final Function>, ConfigurableApplicationContext> contextFactory; + + Infrastructure(String name, Function>, ConfigurableApplicationContext> contextFactory) { + this.name = name; + this.contextFactory = contextFactory; + } + + WebEndpointsInvocationContext createInvocationContext() { + return new WebEndpointsInvocationContext(this.name, this.contextFactory); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java index debfdb60787b..c0ac708f205e 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/test/WebEndpointTestInvocationContextProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -36,6 +36,7 @@ import org.junit.jupiter.api.extension.ParameterResolver; import org.junit.jupiter.api.extension.TestTemplateInvocationContext; import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; +import org.junit.platform.commons.util.AnnotationUtils; import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper; import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; @@ -45,6 +46,7 @@ import org.springframework.boot.actuate.endpoint.web.jersey.JerseyEndpointResourceFactory; import org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndpointHandlerMapping; import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping; +import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest.Infrastructure; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; @@ -91,16 +93,14 @@ public boolean supportsTestTemplate(ExtensionContext context) { @Override public Stream provideTestTemplateInvocationContexts( ExtensionContext extensionContext) { - return Stream.of( - new WebEndpointsInvocationContext("Jersey", - WebEndpointTestInvocationContextProvider::createJerseyContext), - new WebEndpointsInvocationContext("WebMvc", - WebEndpointTestInvocationContextProvider::createWebMvcContext), - new WebEndpointsInvocationContext("WebFlux", - WebEndpointTestInvocationContextProvider::createWebFluxContext)); + WebEndpointTest webEndpointTest = AnnotationUtils + .findAnnotation(extensionContext.getRequiredTestMethod(), WebEndpointTest.class) + .orElseThrow(() -> new IllegalStateException("Unable to find WebEndpointTest annotation on %s" + .formatted(extensionContext.getRequiredTestMethod()))); + return Stream.of(webEndpointTest.infrastructure()).distinct().map(Infrastructure::createInvocationContext); } - private static ConfigurableApplicationContext createJerseyContext(List> classes) { + static ConfigurableApplicationContext createJerseyContext(List> classes) { AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext(); classes.add(JerseyEndpointConfiguration.class); context.register(ClassUtils.toClassArray(classes)); @@ -108,7 +108,7 @@ private static ConfigurableApplicationContext createJerseyContext(List> return context; } - private static ConfigurableApplicationContext createWebMvcContext(List> classes) { + static ConfigurableApplicationContext createWebMvcContext(List> classes) { AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext(); classes.add(WebMvcEndpointConfiguration.class); context.register(ClassUtils.toClassArray(classes)); @@ -116,7 +116,7 @@ private static ConfigurableApplicationContext createWebMvcContext(List> return context; } - private static ConfigurableApplicationContext createWebFluxContext(List> classes) { + static ConfigurableApplicationContext createWebFluxContext(List> classes) { AnnotationConfigReactiveWebServerApplicationContext context = new AnnotationConfigReactiveWebServerApplicationContext(); classes.add(WebFluxEndpointConfiguration.class); context.register(ClassUtils.toClassArray(classes)); From 6a9eb7754f6b776f1d24b1e388e20e5b62bcae01 Mon Sep 17 00:00:00 2001 From: Vedran Pavic Date: Tue, 9 Aug 2022 23:36:38 +0200 Subject: [PATCH 1103/1215] Provide an Actuator endpoint for non-indexed session repositories At present, Actuator sessions endpoint is supported only on a Servlet stack and also requires an indexed session repository. With Spring Session moving to non-indexed session repositories as a default for some session stores, this means that sessions endpoint won't be available unless users opt into a (non-default) indexed session repository. This commit updates SessionEndpoint so that it is able to work with a non-indexed session repository. In such setup, it exposes operations for fetching session by id and deleting the session. Additionally, this also adds support for reactive stack by introducing ReactiveSessionEndpoint and its auto-configuration support. See gh-32046 --- .../SessionsEndpointAutoConfiguration.java | 39 +++++- .../SessionsEndpointDocumentationTests.java | 2 +- ...essionsEndpointAutoConfigurationTests.java | 111 ++++++++++++++---- .../session/ReactiveSessionsEndpoint.java | 60 ++++++++++ .../actuate/session/SessionDescriptor.java | 78 ++++++++++++ .../actuate/session/SessionsEndpoint.java | 20 +++- .../ReactiveSessionsEndpointTests.java | 74 ++++++++++++ ...veSessionsEndpointWebIntegrationTests.java | 88 ++++++++++++++ .../session/SessionsEndpointTests.java | 32 +++-- .../SessionsEndpointWebIntegrationTests.java | 22 +++- 10 files changed, 477 insertions(+), 49 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpoint.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionDescriptor.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointWebIntegrationTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfiguration.java index 3ec9589a29f5..10f5e92f05d2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfiguration.java @@ -16,17 +16,24 @@ package org.springframework.boot.actuate.autoconfigure.session; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; +import org.springframework.boot.actuate.session.ReactiveSessionsEndpoint; import org.springframework.boot.actuate.session.SessionsEndpoint; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.session.SessionAutoConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.ReactiveSessionRepository; import org.springframework.session.Session; +import org.springframework.session.SessionRepository; /** * {@link EnableAutoConfiguration Auto-configuration} for {@link SessionsEndpoint}. @@ -35,15 +42,35 @@ * @since 2.0.0 */ @AutoConfiguration(after = SessionAutoConfiguration.class) -@ConditionalOnClass(FindByIndexNameSessionRepository.class) +@ConditionalOnClass(Session.class) @ConditionalOnAvailableEndpoint(endpoint = SessionsEndpoint.class) public class SessionsEndpointAutoConfiguration { - @Bean - @ConditionalOnBean(FindByIndexNameSessionRepository.class) - @ConditionalOnMissingBean - public SessionsEndpoint sessionEndpoint(FindByIndexNameSessionRepository sessionRepository) { - return new SessionsEndpoint(sessionRepository); + @Configuration(proxyBeanMethods = false) + @ConditionalOnWebApplication(type = Type.SERVLET) + @ConditionalOnBean(SessionRepository.class) + static class ServletSessionEndpointConfiguration { + + @Bean + @ConditionalOnMissingBean + SessionsEndpoint sessionEndpoint(SessionRepository sessionRepository, + ObjectProvider> indexedSessionRepository) { + return new SessionsEndpoint(sessionRepository, indexedSessionRepository.getIfAvailable()); + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnWebApplication(type = Type.REACTIVE) + @ConditionalOnBean(ReactiveSessionRepository.class) + static class ReactiveSessionEndpointConfiguration { + + @Bean + @ConditionalOnMissingBean + ReactiveSessionsEndpoint sessionsEndpoint(ReactiveSessionRepository sessionRepository) { + return new ReactiveSessionsEndpoint(sessionRepository); + } + } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/SessionsEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/SessionsEndpointDocumentationTests.java index 0d43d8fa122d..b18e125846d4 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/SessionsEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/SessionsEndpointDocumentationTests.java @@ -125,7 +125,7 @@ static class TestConfiguration { @Bean SessionsEndpoint endpoint(FindByIndexNameSessionRepository sessionRepository) { - return new SessionsEndpoint(sessionRepository); + return new SessionsEndpoint(sessionRepository, sessionRepository); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfigurationTests.java index 576fe6bbaee5..6477fdeb0b16 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -16,14 +16,19 @@ package org.springframework.boot.actuate.autoconfigure.session; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.session.ReactiveSessionsEndpoint; import org.springframework.boot.actuate.session.SessionsEndpoint; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.ReactiveSessionRepository; +import org.springframework.session.SessionRepository; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -35,33 +40,93 @@ */ class SessionsEndpointAutoConfigurationTests { - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(SessionsEndpointAutoConfiguration.class)) - .withUserConfiguration(SessionConfiguration.class); + @Nested + class ServletSessionEndpointConfigurationTests { - @Test - void runShouldHaveEndpointBean() { - this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=sessions") - .run((context) -> assertThat(context).hasSingleBean(SessionsEndpoint.class)); - } + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(SessionsEndpointAutoConfiguration.class)) + .withUserConfiguration(IndexedSessionRepositoryConfiguration.class); - @Test - void runWhenNotExposedShouldNotHaveEndpointBean() { - this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(SessionsEndpoint.class)); - } + @Test + void runShouldHaveEndpointBean() { + this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=sessions") + .run((context) -> assertThat(context).hasSingleBean(SessionsEndpoint.class)); + } + + @Test + void runWhenNoIndexedSessionRepositoryShouldHaveEndpointBean() { + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(SessionsEndpointAutoConfiguration.class)) + .withUserConfiguration(SessionRepositoryConfiguration.class) + .withPropertyValues("management.endpoints.web.exposure.include=sessions") + .run((context) -> assertThat(context).hasSingleBean(SessionsEndpoint.class)); + } + + @Test + void runWhenNotExposedShouldNotHaveEndpointBean() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(SessionsEndpoint.class)); + } + + @Test + void runWhenEnabledPropertyIsFalseShouldNotHaveEndpointBean() { + this.contextRunner.withPropertyValues("management.endpoint.sessions.enabled:false") + .run((context) -> assertThat(context).doesNotHaveBean(SessionsEndpoint.class)); + } + + @Configuration(proxyBeanMethods = false) + static class IndexedSessionRepositoryConfiguration { + + @Bean + FindByIndexNameSessionRepository sessionRepository() { + return mock(FindByIndexNameSessionRepository.class); + } + + } + + @Configuration(proxyBeanMethods = false) + static class SessionRepositoryConfiguration { + + @Bean + SessionRepository sessionRepository() { + return mock(SessionRepository.class); + } + + } - @Test - void runWhenEnabledPropertyIsFalseShouldNotHaveEndpointBean() { - this.contextRunner.withPropertyValues("management.endpoint.sessions.enabled:false") - .run((context) -> assertThat(context).doesNotHaveBean(SessionsEndpoint.class)); } - @Configuration(proxyBeanMethods = false) - static class SessionConfiguration { + @Nested + class ReactiveSessionEndpointConfigurationTests { + + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(SessionsEndpointAutoConfiguration.class)) + .withUserConfiguration(ReactiveSessionRepositoryConfiguration.class); + + @Test + void runShouldHaveEndpointBean() { + this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=sessions") + .run((context) -> assertThat(context).hasSingleBean(ReactiveSessionsEndpoint.class)); + } + + @Test + void runWhenNotExposedShouldNotHaveEndpointBean() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ReactiveSessionsEndpoint.class)); + } + + @Test + void runWhenEnabledPropertyIsFalseShouldNotHaveEndpointBean() { + this.contextRunner.withPropertyValues("management.endpoint.sessions.enabled:false") + .run((context) -> assertThat(context).doesNotHaveBean(ReactiveSessionsEndpoint.class)); + } + + @Configuration(proxyBeanMethods = false) + static class ReactiveSessionRepositoryConfiguration { + + @Bean + ReactiveSessionRepository sessionRepository() { + return mock(ReactiveSessionRepository.class); + } - @Bean - FindByIndexNameSessionRepository sessionRepository() { - return mock(FindByIndexNameSessionRepository.class); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpoint.java new file mode 100644 index 000000000000..298764d5b32f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpoint.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2022 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.actuate.session; + +import reactor.core.publisher.Mono; + +import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.Selector; +import org.springframework.session.ReactiveSessionRepository; +import org.springframework.session.Session; +import org.springframework.util.Assert; + +/** + * {@link Endpoint @Endpoint} to expose information about HTTP {@link Session}s on a + * reactive stack. + * + * @author Vedran Pavic + * @since 3.0.0 + */ +@Endpoint(id = "sessions") +public class ReactiveSessionsEndpoint { + + private final ReactiveSessionRepository sessionRepository; + + /** + * Create a new {@link ReactiveSessionsEndpoint} instance. + * @param sessionRepository the session repository + */ + public ReactiveSessionsEndpoint(ReactiveSessionRepository sessionRepository) { + Assert.notNull(sessionRepository, "ReactiveSessionRepository must not be null"); + this.sessionRepository = sessionRepository; + } + + @ReadOperation + public Mono getSession(@Selector String sessionId) { + return this.sessionRepository.findById(sessionId).map(SessionDescriptor::new); + } + + @DeleteOperation + public Mono deleteSession(@Selector String sessionId) { + return this.sessionRepository.deleteById(sessionId); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionDescriptor.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionDescriptor.java new file mode 100644 index 000000000000..71e5cc8aebf2 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionDescriptor.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2022 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.actuate.session; + +import java.time.Instant; +import java.util.Set; + +import org.springframework.session.Session; + +/** + * A description of user's {@link Session session} exposed by {@code sessions} endpoint. + * Primarily intended for serialization to JSON. + * + * @author Vedran Pavic + * @since 3.0.0 + */ +public final class SessionDescriptor { + + private final String id; + + private final Set attributeNames; + + private final Instant creationTime; + + private final Instant lastAccessedTime; + + private final long maxInactiveInterval; + + private final boolean expired; + + SessionDescriptor(Session session) { + this.id = session.getId(); + this.attributeNames = session.getAttributeNames(); + this.creationTime = session.getCreationTime(); + this.lastAccessedTime = session.getLastAccessedTime(); + this.maxInactiveInterval = session.getMaxInactiveInterval().getSeconds(); + this.expired = session.isExpired(); + } + + public String getId() { + return this.id; + } + + public Set getAttributeNames() { + return this.attributeNames; + } + + public Instant getCreationTime() { + return this.creationTime; + } + + public Instant getLastAccessedTime() { + return this.lastAccessedTime; + } + + public long getMaxInactiveInterval() { + return this.maxInactiveInterval; + } + + public boolean isExpired() { + return this.expired; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java index b333d8e23c2b..db60d0459e3c 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java @@ -28,9 +28,12 @@ import org.springframework.boot.actuate.endpoint.annotation.Selector; import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.Session; +import org.springframework.session.SessionRepository; +import org.springframework.util.Assert; /** - * {@link Endpoint @Endpoint} to expose a user's {@link Session}s. + * {@link Endpoint @Endpoint} to expose information about HTTP {@link Session}s on a + * Servlet stack. * * @author Vedran Pavic * @since 2.0.0 @@ -38,19 +41,28 @@ @Endpoint(id = "sessions") public class SessionsEndpoint { - private final FindByIndexNameSessionRepository sessionRepository; + private final SessionRepository sessionRepository; + + private final FindByIndexNameSessionRepository indexedSessionRepository; /** * Create a new {@link SessionsEndpoint} instance. * @param sessionRepository the session repository + * @param indexedSessionRepository the indexed session repository */ - public SessionsEndpoint(FindByIndexNameSessionRepository sessionRepository) { + public SessionsEndpoint(SessionRepository sessionRepository, + FindByIndexNameSessionRepository indexedSessionRepository) { + Assert.notNull(sessionRepository, "SessionRepository must not be null"); this.sessionRepository = sessionRepository; + this.indexedSessionRepository = indexedSessionRepository; } @ReadOperation public SessionsDescriptor sessionsForUsername(String username) { - Map sessions = this.sessionRepository.findByPrincipalName(username); + if (this.indexedSessionRepository == null) { + return null; + } + Map sessions = this.indexedSessionRepository.findByPrincipalName(username); return new SessionsDescriptor(sessions); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointTests.java new file mode 100644 index 000000000000..39895c44234b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2022 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.actuate.session; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.session.MapSession; +import org.springframework.session.ReactiveSessionRepository; +import org.springframework.session.Session; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ReactiveSessionsEndpoint}. + * + * @author Vedran Pavic + */ +class ReactiveSessionsEndpointTests { + + private static final Session session = new MapSession(); + + @SuppressWarnings("unchecked") + private final ReactiveSessionRepository sessionRepository = mock(ReactiveSessionRepository.class); + + private final ReactiveSessionsEndpoint endpoint = new ReactiveSessionsEndpoint(this.sessionRepository); + + @Test + void getSession() { + given(this.sessionRepository.findById(session.getId())).willReturn(Mono.just(session)); + StepVerifier.create(this.endpoint.getSession(session.getId())).consumeNextWith((result) -> { + assertThat(result.getId()).isEqualTo(session.getId()); + assertThat(result.getAttributeNames()).isEqualTo(session.getAttributeNames()); + assertThat(result.getCreationTime()).isEqualTo(session.getCreationTime()); + assertThat(result.getLastAccessedTime()).isEqualTo(session.getLastAccessedTime()); + assertThat(result.getMaxInactiveInterval()).isEqualTo(session.getMaxInactiveInterval().getSeconds()); + assertThat(result.isExpired()).isEqualTo(session.isExpired()); + }).verifyComplete(); + then(this.sessionRepository).should().findById(session.getId()); + } + + @Test + void getSessionWithIdNotFound() { + given(this.sessionRepository.findById("not-found")).willReturn(Mono.empty()); + StepVerifier.create(this.endpoint.getSession("not-found")).verifyComplete(); + then(this.sessionRepository).should().findById("not-found"); + } + + @Test + void deleteSession() { + given(this.sessionRepository.deleteById(session.getId())).willReturn(Mono.empty()); + StepVerifier.create(this.endpoint.deleteSession(session.getId())).verifyComplete(); + then(this.sessionRepository).should().deleteById(session.getId()); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointWebIntegrationTests.java new file mode 100644 index 000000000000..7f401484d72c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointWebIntegrationTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2022 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.actuate.session; + +import reactor.core.publisher.Mono; + +import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest; +import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest.Infrastructure; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.session.MapSession; +import org.springframework.session.ReactiveSessionRepository; +import org.springframework.session.Session; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Integration tests for {@link ReactiveSessionsEndpoint} exposed by WebFlux. + * + * @author Vedran Pavic + */ +class ReactiveSessionsEndpointWebIntegrationTests { + + private static final Session session = new MapSession(); + + @SuppressWarnings("unchecked") + private static final ReactiveSessionRepository sessionRepository = mock(ReactiveSessionRepository.class); + + @WebEndpointTest(infrastructure = Infrastructure.WEBFLUX) + void sessionForIdFound(WebTestClient client) { + given(sessionRepository.findById(session.getId())).willReturn(Mono.just(session)); + client.get() + .uri((builder) -> builder.path("/actuator/sessions/{id}").build(session.getId())) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("id") + .isEqualTo(session.getId()); + } + + @WebEndpointTest(infrastructure = Infrastructure.WEBFLUX) + void sessionForIdNotFound(WebTestClient client) { + given(sessionRepository.findById("not-found")).willReturn(Mono.empty()); + client.get() + .uri((builder) -> builder.path("/actuator/sessions/not-found").build()) + .exchange() + .expectStatus() + .isNotFound(); + } + + @WebEndpointTest(infrastructure = Infrastructure.WEBFLUX) + void deleteSession(WebTestClient client) { + given(sessionRepository.deleteById(session.getId())).willReturn(Mono.empty()); + client.delete() + .uri((builder) -> builder.path("/actuator/sessions/{id}").build(session.getId())) + .exchange() + .expectStatus() + .isNoContent(); + } + + @Configuration(proxyBeanMethods = false) + static class TestConfiguration { + + @Bean + ReactiveSessionsEndpoint sessionsEndpoint() { + return new ReactiveSessionsEndpoint(sessionRepository); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java index eb1647e38910..62e728216bfe 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java @@ -21,10 +21,10 @@ import org.junit.jupiter.api.Test; -import org.springframework.boot.actuate.session.SessionsEndpoint.SessionDescriptor; import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.MapSession; import org.springframework.session.Session; +import org.springframework.session.SessionRepository; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -41,15 +41,20 @@ class SessionsEndpointTests { private static final Session session = new MapSession(); @SuppressWarnings("unchecked") - private final FindByIndexNameSessionRepository repository = mock(FindByIndexNameSessionRepository.class); + private final SessionRepository sessionRepository = mock(SessionRepository.class); - private final SessionsEndpoint endpoint = new SessionsEndpoint(this.repository); + @SuppressWarnings("unchecked") + private final FindByIndexNameSessionRepository indexedSessionRepository = mock( + FindByIndexNameSessionRepository.class); + + private final SessionsEndpoint endpoint = new SessionsEndpoint(this.sessionRepository, + this.indexedSessionRepository); @Test void sessionsForUsername() { - given(this.repository.findByPrincipalName("user")) + given(this.indexedSessionRepository.findByPrincipalName("user")) .willReturn(Collections.singletonMap(session.getId(), session)); - List result = this.endpoint.sessionsForUsername("user").getSessions(); + List result = this.endpoint.sessionsForUsername("user").getSessions(); assertThat(result).hasSize(1); assertThat(result.get(0).getId()).isEqualTo(session.getId()); assertThat(result.get(0).getAttributeNames()).isEqualTo(session.getAttributeNames()); @@ -57,30 +62,39 @@ void sessionsForUsername() { assertThat(result.get(0).getLastAccessedTime()).isEqualTo(session.getLastAccessedTime()); assertThat(result.get(0).getMaxInactiveInterval()).isEqualTo(session.getMaxInactiveInterval().getSeconds()); assertThat(result.get(0).isExpired()).isEqualTo(session.isExpired()); + then(this.indexedSessionRepository).should().findByPrincipalName("user"); + } + + @Test + void sessionsForUsernameWhenNoIndexedRepository() { + SessionsEndpoint endpoint = new SessionsEndpoint(this.sessionRepository, null); + assertThat(endpoint.sessionsForUsername("user")).isNull(); } @Test void getSession() { - given(this.repository.findById(session.getId())).willReturn(session); - SessionDescriptor result = this.endpoint.getSession(session.getId()); + given(this.sessionRepository.findById(session.getId())).willReturn(session); + SessionsEndpoint.SessionDescriptor result = this.endpoint.getSession(session.getId()); assertThat(result.getId()).isEqualTo(session.getId()); assertThat(result.getAttributeNames()).isEqualTo(session.getAttributeNames()); assertThat(result.getCreationTime()).isEqualTo(session.getCreationTime()); assertThat(result.getLastAccessedTime()).isEqualTo(session.getLastAccessedTime()); assertThat(result.getMaxInactiveInterval()).isEqualTo(session.getMaxInactiveInterval().getSeconds()); assertThat(result.isExpired()).isEqualTo(session.isExpired()); + then(this.sessionRepository).should().findById(session.getId()); } @Test void getSessionWithIdNotFound() { - given(this.repository.findById("not-found")).willReturn(null); + given(this.sessionRepository.findById("not-found")).willReturn(null); assertThat(this.endpoint.getSession("not-found")).isNull(); + then(this.sessionRepository).should().findById("not-found"); } @Test void deleteSession() { this.endpoint.deleteSession(session.getId()); - then(this.repository).should().deleteById(session.getId()); + then(this.sessionRepository).should().deleteById(session.getId()); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointWebIntegrationTests.java index 0a6b28dd83b5..fcf8d5e57d83 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointWebIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointWebIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -21,6 +21,7 @@ import net.minidev.json.JSONArray; import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest; +import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest.Infrastructure; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.session.FindByIndexNameSessionRepository; @@ -45,7 +46,7 @@ class SessionsEndpointWebIntegrationTests { private static final FindByIndexNameSessionRepository repository = mock( FindByIndexNameSessionRepository.class); - @WebEndpointTest + @WebEndpointTest(infrastructure = { Infrastructure.JERSEY, Infrastructure.MVC }) void sessionsForUsernameWithoutUsernameParam(WebTestClient client) { client.get() .uri((builder) -> builder.path("/actuator/sessions").build()) @@ -54,7 +55,7 @@ void sessionsForUsernameWithoutUsernameParam(WebTestClient client) { .isBadRequest(); } - @WebEndpointTest + @WebEndpointTest(infrastructure = { Infrastructure.JERSEY, Infrastructure.MVC }) void sessionsForUsernameNoResults(WebTestClient client) { given(repository.findByPrincipalName("user")).willReturn(Collections.emptyMap()); client.get() @@ -67,7 +68,7 @@ void sessionsForUsernameNoResults(WebTestClient client) { .isEmpty(); } - @WebEndpointTest + @WebEndpointTest(infrastructure = { Infrastructure.JERSEY, Infrastructure.MVC }) void sessionsForUsernameFound(WebTestClient client) { given(repository.findByPrincipalName("user")).willReturn(Collections.singletonMap(session.getId(), session)); client.get() @@ -80,7 +81,7 @@ void sessionsForUsernameFound(WebTestClient client) { .isEqualTo(new JSONArray().appendElement(session.getId())); } - @WebEndpointTest + @WebEndpointTest(infrastructure = { Infrastructure.JERSEY, Infrastructure.MVC }) void sessionForIdNotFound(WebTestClient client) { client.get() .uri((builder) -> builder.path("/actuator/sessions/session-id-not-found").build()) @@ -89,12 +90,21 @@ void sessionForIdNotFound(WebTestClient client) { .isNotFound(); } + @WebEndpointTest(infrastructure = { Infrastructure.JERSEY, Infrastructure.MVC }) + void deleteSession(WebTestClient client) { + client.delete() + .uri((builder) -> builder.path("/actuator/sessions/{id}").build(session.getId())) + .exchange() + .expectStatus() + .isNoContent(); + } + @Configuration(proxyBeanMethods = false) static class TestConfiguration { @Bean SessionsEndpoint sessionsEndpoint() { - return new SessionsEndpoint(repository); + return new SessionsEndpoint(repository, repository); } } From de76ef1b3b7c3b999279573fff347bed3047693e Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 18 Jan 2024 10:52:58 +0100 Subject: [PATCH 1104/1215] Polish "Provide an Actuator endpoint for non-indexed session repositories" See gh-32046 --- .../actuate/session/SessionsEndpoint.java | 56 +------------------ .../ReactiveSessionsEndpointTests.java | 12 ++-- .../session/SessionsEndpointTests.java | 6 +- 3 files changed, 12 insertions(+), 62 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java index db60d0459e3c..a08e7964799f 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -16,10 +16,8 @@ package org.springframework.boot.actuate.session; -import java.time.Instant; import java.util.List; import java.util.Map; -import java.util.Set; import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation; @@ -97,56 +95,4 @@ public List getSessions() { } - /** - * Description of user's {@link Session session}. - */ - public static final class SessionDescriptor implements OperationResponseBody { - - private final String id; - - private final Set attributeNames; - - private final Instant creationTime; - - private final Instant lastAccessedTime; - - private final long maxInactiveInterval; - - private final boolean expired; - - public SessionDescriptor(Session session) { - this.id = session.getId(); - this.attributeNames = session.getAttributeNames(); - this.creationTime = session.getCreationTime(); - this.lastAccessedTime = session.getLastAccessedTime(); - this.maxInactiveInterval = session.getMaxInactiveInterval().getSeconds(); - this.expired = session.isExpired(); - } - - public String getId() { - return this.id; - } - - public Set getAttributeNames() { - return this.attributeNames; - } - - public Instant getCreationTime() { - return this.creationTime; - } - - public Instant getLastAccessedTime() { - return this.lastAccessedTime; - } - - public long getMaxInactiveInterval() { - return this.maxInactiveInterval; - } - - public boolean isExpired() { - return this.expired; - } - - } - } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointTests.java index 39895c44234b..48ca95536fa8 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -16,6 +16,8 @@ package org.springframework.boot.actuate.session; +import java.time.Duration; + import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -53,21 +55,23 @@ void getSession() { assertThat(result.getLastAccessedTime()).isEqualTo(session.getLastAccessedTime()); assertThat(result.getMaxInactiveInterval()).isEqualTo(session.getMaxInactiveInterval().getSeconds()); assertThat(result.isExpired()).isEqualTo(session.isExpired()); - }).verifyComplete(); + }).expectComplete().verify(Duration.ofSeconds(1)); then(this.sessionRepository).should().findById(session.getId()); } @Test void getSessionWithIdNotFound() { given(this.sessionRepository.findById("not-found")).willReturn(Mono.empty()); - StepVerifier.create(this.endpoint.getSession("not-found")).verifyComplete(); + StepVerifier.create(this.endpoint.getSession("not-found")).expectComplete().verify(Duration.ofSeconds(1)); then(this.sessionRepository).should().findById("not-found"); } @Test void deleteSession() { given(this.sessionRepository.deleteById(session.getId())).willReturn(Mono.empty()); - StepVerifier.create(this.endpoint.deleteSession(session.getId())).verifyComplete(); + StepVerifier.create(this.endpoint.deleteSession(session.getId())) + .expectComplete() + .verify(Duration.ofSeconds(1)); then(this.sessionRepository).should().deleteById(session.getId()); } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java index 62e728216bfe..e37279c51f35 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -54,7 +54,7 @@ class SessionsEndpointTests { void sessionsForUsername() { given(this.indexedSessionRepository.findByPrincipalName("user")) .willReturn(Collections.singletonMap(session.getId(), session)); - List result = this.endpoint.sessionsForUsername("user").getSessions(); + List result = this.endpoint.sessionsForUsername("user").getSessions(); assertThat(result).hasSize(1); assertThat(result.get(0).getId()).isEqualTo(session.getId()); assertThat(result.get(0).getAttributeNames()).isEqualTo(session.getAttributeNames()); @@ -74,7 +74,7 @@ void sessionsForUsernameWhenNoIndexedRepository() { @Test void getSession() { given(this.sessionRepository.findById(session.getId())).willReturn(session); - SessionsEndpoint.SessionDescriptor result = this.endpoint.getSession(session.getId()); + SessionDescriptor result = this.endpoint.getSession(session.getId()); assertThat(result.getId()).isEqualTo(session.getId()); assertThat(result.getAttributeNames()).isEqualTo(session.getAttributeNames()); assertThat(result.getCreationTime()).isEqualTo(session.getCreationTime()); From 6e3d4ed8786d9c9060b87031261b8e1e3a61e16a Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Thu, 18 Jan 2024 11:12:26 +0100 Subject: [PATCH 1105/1215] Use ReactiveFindByIndexNameSessionRepository See gh-32046 --- .../SessionsEndpointAutoConfiguration.java | 8 +- ...essionsEndpointAutoConfigurationTests.java | 24 ++++- .../session/ReactiveSessionsEndpoint.java | 22 ++++- .../actuate/session/SessionDescriptor.java | 78 --------------- .../actuate/session/SessionsDescriptor.java | 98 +++++++++++++++++++ .../actuate/session/SessionsEndpoint.java | 32 +++--- .../ReactiveSessionsEndpointTests.java | 36 ++++++- ...veSessionsEndpointWebIntegrationTests.java | 49 +++++++++- .../session/SessionsEndpointTests.java | 1 + 9 files changed, 241 insertions(+), 107 deletions(-) delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionDescriptor.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsDescriptor.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfiguration.java index 10f5e92f05d2..30c6a155c1d9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -31,6 +31,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.ReactiveFindByIndexNameSessionRepository; import org.springframework.session.ReactiveSessionRepository; import org.springframework.session.Session; import org.springframework.session.SessionRepository; @@ -67,8 +68,9 @@ static class ReactiveSessionEndpointConfiguration { @Bean @ConditionalOnMissingBean - ReactiveSessionsEndpoint sessionsEndpoint(ReactiveSessionRepository sessionRepository) { - return new ReactiveSessionsEndpoint(sessionRepository); + ReactiveSessionsEndpoint sessionsEndpoint(ReactiveSessionRepository sessionRepository, + ObjectProvider> indexedSessionRepository) { + return new ReactiveSessionsEndpoint(sessionRepository, indexedSessionRepository.getIfAvailable()); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfigurationTests.java index 6477fdeb0b16..491e4e3d6186 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/session/SessionsEndpointAutoConfigurationTests.java @@ -27,6 +27,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.ReactiveFindByIndexNameSessionRepository; import org.springframework.session.ReactiveSessionRepository; import org.springframework.session.SessionRepository; @@ -37,6 +38,7 @@ * Tests for {@link SessionsEndpointAutoConfiguration}. * * @author Vedran Pavic + * @author Moritz Halbritter */ class SessionsEndpointAutoConfigurationTests { @@ -100,7 +102,8 @@ class ReactiveSessionEndpointConfigurationTests { private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(SessionsEndpointAutoConfiguration.class)) - .withUserConfiguration(ReactiveSessionRepositoryConfiguration.class); + .withUserConfiguration(ReactiveSessionRepositoryConfiguration.class, + ReactiveIndexedSessionRepositoryConfiguration.class); @Test void runShouldHaveEndpointBean() { @@ -108,6 +111,15 @@ void runShouldHaveEndpointBean() { .run((context) -> assertThat(context).hasSingleBean(ReactiveSessionsEndpoint.class)); } + @Test + void runWhenNoIndexedSessionRepositoryShouldHaveEndpointBean() { + new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(SessionsEndpointAutoConfiguration.class)) + .withUserConfiguration(ReactiveSessionRepositoryConfiguration.class) + .withPropertyValues("management.endpoints.web.exposure.include=sessions") + .run((context) -> assertThat(context).hasSingleBean(ReactiveSessionsEndpoint.class)); + } + @Test void runWhenNotExposedShouldNotHaveEndpointBean() { this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ReactiveSessionsEndpoint.class)); @@ -119,6 +131,16 @@ void runWhenEnabledPropertyIsFalseShouldNotHaveEndpointBean() { .run((context) -> assertThat(context).doesNotHaveBean(ReactiveSessionsEndpoint.class)); } + @Configuration(proxyBeanMethods = false) + static class ReactiveIndexedSessionRepositoryConfiguration { + + @Bean + ReactiveFindByIndexNameSessionRepository indexedSessionRepository() { + return mock(ReactiveFindByIndexNameSessionRepository.class); + } + + } + @Configuration(proxyBeanMethods = false) static class ReactiveSessionRepositoryConfiguration { diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpoint.java index 298764d5b32f..9ee15d69fc02 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -22,6 +22,8 @@ import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; +import org.springframework.boot.actuate.session.SessionsDescriptor.SessionDescriptor; +import org.springframework.session.ReactiveFindByIndexNameSessionRepository; import org.springframework.session.ReactiveSessionRepository; import org.springframework.session.Session; import org.springframework.util.Assert; @@ -31,20 +33,34 @@ * reactive stack. * * @author Vedran Pavic - * @since 3.0.0 + * @author Moritz Halbritter + * @since 3.3.0 */ @Endpoint(id = "sessions") public class ReactiveSessionsEndpoint { private final ReactiveSessionRepository sessionRepository; + private final ReactiveFindByIndexNameSessionRepository indexedSessionRepository; + /** * Create a new {@link ReactiveSessionsEndpoint} instance. * @param sessionRepository the session repository + * @param indexedSessionRepository the indexed session repository */ - public ReactiveSessionsEndpoint(ReactiveSessionRepository sessionRepository) { + public ReactiveSessionsEndpoint(ReactiveSessionRepository sessionRepository, + ReactiveFindByIndexNameSessionRepository indexedSessionRepository) { Assert.notNull(sessionRepository, "ReactiveSessionRepository must not be null"); this.sessionRepository = sessionRepository; + this.indexedSessionRepository = indexedSessionRepository; + } + + @ReadOperation + public Mono sessionsForUsername(String username) { + if (this.indexedSessionRepository == null) { + return Mono.empty(); + } + return this.indexedSessionRepository.findByPrincipalName(username).map(SessionsDescriptor::new); } @ReadOperation diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionDescriptor.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionDescriptor.java deleted file mode 100644 index 71e5cc8aebf2..000000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionDescriptor.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2012-2022 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.actuate.session; - -import java.time.Instant; -import java.util.Set; - -import org.springframework.session.Session; - -/** - * A description of user's {@link Session session} exposed by {@code sessions} endpoint. - * Primarily intended for serialization to JSON. - * - * @author Vedran Pavic - * @since 3.0.0 - */ -public final class SessionDescriptor { - - private final String id; - - private final Set attributeNames; - - private final Instant creationTime; - - private final Instant lastAccessedTime; - - private final long maxInactiveInterval; - - private final boolean expired; - - SessionDescriptor(Session session) { - this.id = session.getId(); - this.attributeNames = session.getAttributeNames(); - this.creationTime = session.getCreationTime(); - this.lastAccessedTime = session.getLastAccessedTime(); - this.maxInactiveInterval = session.getMaxInactiveInterval().getSeconds(); - this.expired = session.isExpired(); - } - - public String getId() { - return this.id; - } - - public Set getAttributeNames() { - return this.attributeNames; - } - - public Instant getCreationTime() { - return this.creationTime; - } - - public Instant getLastAccessedTime() { - return this.lastAccessedTime; - } - - public long getMaxInactiveInterval() { - return this.maxInactiveInterval; - } - - public boolean isExpired() { - return this.expired; - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsDescriptor.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsDescriptor.java new file mode 100644 index 000000000000..24e12de097b8 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsDescriptor.java @@ -0,0 +1,98 @@ +/* + * 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.actuate.session; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.boot.actuate.endpoint.OperationResponseBody; +import org.springframework.session.Session; + +/** + * Description of user's {@link Session sessions}. + * + * @author Moritz Halbritter + * @since 3.3.0 + */ +public final class SessionsDescriptor implements OperationResponseBody { + + private final List sessions; + + public SessionsDescriptor(Map sessions) { + this.sessions = sessions.values().stream().map(SessionDescriptor::new).toList(); + } + + public List getSessions() { + return this.sessions; + } + + /** + * A description of user's {@link Session session} exposed by {@code sessions} + * endpoint. Primarily intended for serialization to JSON. + */ + public static final class SessionDescriptor { + + private final String id; + + private final Set attributeNames; + + private final Instant creationTime; + + private final Instant lastAccessedTime; + + private final long maxInactiveInterval; + + private final boolean expired; + + SessionDescriptor(Session session) { + this.id = session.getId(); + this.attributeNames = session.getAttributeNames(); + this.creationTime = session.getCreationTime(); + this.lastAccessedTime = session.getLastAccessedTime(); + this.maxInactiveInterval = session.getMaxInactiveInterval().getSeconds(); + this.expired = session.isExpired(); + } + + public String getId() { + return this.id; + } + + public Set getAttributeNames() { + return this.attributeNames; + } + + public Instant getCreationTime() { + return this.creationTime; + } + + public Instant getLastAccessedTime() { + return this.lastAccessedTime; + } + + public long getMaxInactiveInterval() { + return this.maxInactiveInterval; + } + + public boolean isExpired() { + return this.expired; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java index a08e7964799f..1b89c83a1132 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/session/SessionsEndpoint.java @@ -16,14 +16,13 @@ package org.springframework.boot.actuate.session; -import java.util.List; import java.util.Map; -import org.springframework.boot.actuate.endpoint.OperationResponseBody; import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; +import org.springframework.boot.actuate.session.SessionsDescriptor.SessionDescriptor; import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.Session; import org.springframework.session.SessionRepository; @@ -43,10 +42,22 @@ public class SessionsEndpoint { private final FindByIndexNameSessionRepository indexedSessionRepository; + /** + * Create a new {@link SessionsEndpoint} instance. + * @param sessionRepository the session repository + * @deprecated since 3.3.0 for removal in 3.5.0 in favor of + * {@link #SessionsEndpoint(SessionRepository, FindByIndexNameSessionRepository)} + */ + @Deprecated(since = "3.3.0", forRemoval = true) + public SessionsEndpoint(FindByIndexNameSessionRepository sessionRepository) { + this(sessionRepository, sessionRepository); + } + /** * Create a new {@link SessionsEndpoint} instance. * @param sessionRepository the session repository * @param indexedSessionRepository the indexed session repository + * @since 3.3.0 */ public SessionsEndpoint(SessionRepository sessionRepository, FindByIndexNameSessionRepository indexedSessionRepository) { @@ -78,21 +89,4 @@ public void deleteSession(@Selector String sessionId) { this.sessionRepository.deleteById(sessionId); } - /** - * Description of user's {@link Session sessions}. - */ - public static final class SessionsDescriptor implements OperationResponseBody { - - private final List sessions; - - public SessionsDescriptor(Map sessions) { - this.sessions = sessions.values().stream().map(SessionDescriptor::new).toList(); - } - - public List getSessions() { - return this.sessions; - } - - } - } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointTests.java index 48ca95536fa8..d338e69f2c66 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointTests.java @@ -17,12 +17,16 @@ package org.springframework.boot.actuate.session; import java.time.Duration; +import java.util.Collections; +import java.util.List; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import org.springframework.boot.actuate.session.SessionsDescriptor.SessionDescriptor; import org.springframework.session.MapSession; +import org.springframework.session.ReactiveFindByIndexNameSessionRepository; import org.springframework.session.ReactiveSessionRepository; import org.springframework.session.Session; @@ -35,6 +39,7 @@ * Tests for {@link ReactiveSessionsEndpoint}. * * @author Vedran Pavic + * @author Moritz Halbritter */ class ReactiveSessionsEndpointTests { @@ -43,7 +48,36 @@ class ReactiveSessionsEndpointTests { @SuppressWarnings("unchecked") private final ReactiveSessionRepository sessionRepository = mock(ReactiveSessionRepository.class); - private final ReactiveSessionsEndpoint endpoint = new ReactiveSessionsEndpoint(this.sessionRepository); + @SuppressWarnings("unchecked") + private final ReactiveFindByIndexNameSessionRepository indexedSessionRepository = mock( + ReactiveFindByIndexNameSessionRepository.class); + + private final ReactiveSessionsEndpoint endpoint = new ReactiveSessionsEndpoint(this.sessionRepository, + this.indexedSessionRepository); + + @Test + void sessionsForUsername() { + given(this.indexedSessionRepository.findByPrincipalName("user")) + .willReturn(Mono.just(Collections.singletonMap(session.getId(), session))); + StepVerifier.create(this.endpoint.sessionsForUsername("user")).consumeNextWith((sessions) -> { + List result = sessions.getSessions(); + assertThat(result).hasSize(1); + assertThat(result.get(0).getId()).isEqualTo(session.getId()); + assertThat(result.get(0).getAttributeNames()).isEqualTo(session.getAttributeNames()); + assertThat(result.get(0).getCreationTime()).isEqualTo(session.getCreationTime()); + assertThat(result.get(0).getLastAccessedTime()).isEqualTo(session.getLastAccessedTime()); + assertThat(result.get(0).getMaxInactiveInterval()).isEqualTo(session.getMaxInactiveInterval().getSeconds()); + assertThat(result.get(0).isExpired()).isEqualTo(session.isExpired()); + + }).expectComplete().verify(Duration.ofSeconds(1)); + then(this.indexedSessionRepository).should().findByPrincipalName("user"); + } + + @Test + void sessionsForUsernameWhenNoIndexedRepository() { + ReactiveSessionsEndpoint endpoint = new ReactiveSessionsEndpoint(this.sessionRepository, null); + StepVerifier.create(endpoint.sessionsForUsername("user")).expectComplete().verify(Duration.ofSeconds(1)); + } @Test void getSession() { diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointWebIntegrationTests.java index 7f401484d72c..c9b13949916e 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointWebIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointWebIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -16,6 +16,9 @@ package org.springframework.boot.actuate.session; +import java.util.Collections; + +import net.minidev.json.JSONArray; import reactor.core.publisher.Mono; import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest; @@ -23,6 +26,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.session.MapSession; +import org.springframework.session.ReactiveFindByIndexNameSessionRepository; import org.springframework.session.ReactiveSessionRepository; import org.springframework.session.Session; import org.springframework.test.web.reactive.server.WebTestClient; @@ -34,6 +38,7 @@ * Integration tests for {@link ReactiveSessionsEndpoint} exposed by WebFlux. * * @author Vedran Pavic + * @author Moritz Halbritter */ class ReactiveSessionsEndpointWebIntegrationTests { @@ -42,6 +47,46 @@ class ReactiveSessionsEndpointWebIntegrationTests { @SuppressWarnings("unchecked") private static final ReactiveSessionRepository sessionRepository = mock(ReactiveSessionRepository.class); + @SuppressWarnings("unchecked") + private static final ReactiveFindByIndexNameSessionRepository indexedSessionRepository = mock( + ReactiveFindByIndexNameSessionRepository.class); + + @WebEndpointTest(infrastructure = Infrastructure.WEBFLUX) + void sessionsForUsernameWithoutUsernameParam(WebTestClient client) { + client.get() + .uri((builder) -> builder.path("/actuator/sessions").build()) + .exchange() + .expectStatus() + .is5xxServerError(); // https://github.com/spring-projects/spring-boot/issues/39236 + } + + @WebEndpointTest(infrastructure = Infrastructure.WEBFLUX) + void sessionsForUsernameNoResults(WebTestClient client) { + given(indexedSessionRepository.findByPrincipalName("user")).willReturn(Mono.just(Collections.emptyMap())); + client.get() + .uri((builder) -> builder.path("/actuator/sessions").queryParam("username", "user").build()) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("sessions") + .isEmpty(); + } + + @WebEndpointTest(infrastructure = Infrastructure.WEBFLUX) + void sessionsForUsernameFound(WebTestClient client) { + given(indexedSessionRepository.findByPrincipalName("user")) + .willReturn(Mono.just(Collections.singletonMap(session.getId(), session))); + client.get() + .uri((builder) -> builder.path("/actuator/sessions").queryParam("username", "user").build()) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("sessions.[*].id") + .isEqualTo(new JSONArray().appendElement(session.getId())); + } + @WebEndpointTest(infrastructure = Infrastructure.WEBFLUX) void sessionForIdFound(WebTestClient client) { given(sessionRepository.findById(session.getId())).willReturn(Mono.just(session)); @@ -80,7 +125,7 @@ static class TestConfiguration { @Bean ReactiveSessionsEndpoint sessionsEndpoint() { - return new ReactiveSessionsEndpoint(sessionRepository); + return new ReactiveSessionsEndpoint(sessionRepository, indexedSessionRepository); } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java index e37279c51f35..8047c825aba4 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/SessionsEndpointTests.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.session.SessionsDescriptor.SessionDescriptor; import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.MapSession; import org.springframework.session.Session; From 088b313ae5cffd55853b67bc7632e3a6bd8bd2a6 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 22 Jan 2024 11:57:37 -0800 Subject: [PATCH 1106/1215] Polish --- .../session/ReactiveSessionsEndpointTests.java | 1 - .../ConfigurationMetadataAnnotationProcessor.java | 11 +++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointTests.java index d338e69f2c66..e5b54926505e 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/session/ReactiveSessionsEndpointTests.java @@ -68,7 +68,6 @@ void sessionsForUsername() { assertThat(result.get(0).getLastAccessedTime()).isEqualTo(session.getLastAccessedTime()); assertThat(result.get(0).getMaxInactiveInterval()).isEqualTo(session.getMaxInactiveInterval().getSeconds()); assertThat(result.get(0).isExpired()).isEqualTo(session.isExpired()); - }).expectComplete().verify(Duration.ofSeconds(1)); then(this.indexedSessionRepository).should().findByPrincipalName("user"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java index 0c4fc7c15165..c34a83f8d6cd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java @@ -307,13 +307,12 @@ private void processEndpoint(AnnotationMirror annotation, TypeElement element) { private void checkEnabledValueMatchesExisting(ItemMetadata existing, boolean enabledByDefault, String sourceType) { boolean existingDefaultValue = (boolean) existing.getDefaultValue(); - if (enabledByDefault == existingDefaultValue) { - return; + if (enabledByDefault != existingDefaultValue) { + throw new IllegalStateException( + "Existing property '%s' from type %s has a conflicting value. Existing value: %b, new value from type %s: %b" + .formatted(existing.getName(), existing.getSourceType(), existingDefaultValue, sourceType, + enabledByDefault)); } - throw new IllegalStateException( - "Existing property '%s' from type %s has a conflicting value. Existing value: %b, new value from type %s: %b" - .formatted(existing.getName(), existing.getSourceType(), existingDefaultValue, sourceType, - enabledByDefault)); } private boolean hasMainReadOperation(TypeElement element) { From eb940c390777939329b979693f929add6d7dbc1b Mon Sep 17 00:00:00 2001 From: PhilKes Date: Sun, 21 Jan 2024 15:07:37 +0100 Subject: [PATCH 1107/1215] Add Docker Compose service connection support for OpenLDAP See gh-39258 --- .../ldap/LdapAutoConfiguration.java | 17 +++- .../ldap/LdapConnectionDetails.java | 59 +++++++++++ .../ldap/PropertiesLdapConnectionDetails.java | 58 +++++++++++ .../ldap/LdapAutoConfigurationTests.java | 51 ++++++++++ ...DockerComposeConnectionDetailsFactory.java | 99 +++++++++++++++++++ .../service/connection/ldap/package-info.java | 20 ++++ .../main/resources/META-INF/spring.factories | 1 + ...nectionDetailsFactoryIntegrationTests.java | 48 +++++++++ .../service/connection/ldap/ldap-compose.yaml | 11 +++ .../spring-boot-testcontainers/build.gradle | 1 + ...LdapContainerConnectionDetailsFactory.java | 90 +++++++++++++++++ .../service/connection/ldap/package-info.java | 20 ++++ .../main/resources/META-INF/spring.factories | 1 + ...nectionDetailsFactoryIntegrationTests.java | 76 ++++++++++++++ .../testcontainers/DockerImageNames.java | 10 ++ .../testcontainers/LdapContainer.java | 35 +++++++ 16 files changed, 592 insertions(+), 5 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapConnectionDetails.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/PropertiesLdapConnectionDetails.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ldap/LdapDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ldap/package-info.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ldap/LdapDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/ldap/ldap-compose.yaml create mode 100644 spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ldap/LdapContainerConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ldap/package-info.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ldap/LdapContainerConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/LdapContainer.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfiguration.java index 0144f9bf99ae..0bd691445d10 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfiguration.java @@ -46,18 +46,25 @@ @EnableConfigurationProperties(LdapProperties.class) public class LdapAutoConfiguration { + @Bean + @ConditionalOnMissingBean(LdapConnectionDetails.class) + PropertiesLdapConnectionDetails propertiesLdapConnectionDetails(LdapProperties properties, + Environment environment) { + return new PropertiesLdapConnectionDetails(properties, environment); + } + @Bean @ConditionalOnMissingBean - public LdapContextSource ldapContextSource(LdapProperties properties, Environment environment, + public LdapContextSource ldapContextSource(LdapConnectionDetails connectionDetails, LdapProperties properties, ObjectProvider dirContextAuthenticationStrategy) { LdapContextSource source = new LdapContextSource(); dirContextAuthenticationStrategy.ifUnique(source::setAuthenticationStrategy); PropertyMapper propertyMapper = PropertyMapper.get().alwaysApplyingWhenNonNull(); - propertyMapper.from(properties.getUsername()).to(source::setUserDn); - propertyMapper.from(properties.getPassword()).to(source::setPassword); + propertyMapper.from(connectionDetails.getUsername()).to(source::setUserDn); + propertyMapper.from(connectionDetails.getPassword()).to(source::setPassword); propertyMapper.from(properties.getAnonymousReadOnly()).to(source::setAnonymousReadOnly); - propertyMapper.from(properties.getBase()).to(source::setBase); - propertyMapper.from(properties.determineUrls(environment)).to(source::setUrls); + propertyMapper.from(connectionDetails.getBase()).to(source::setBase); + propertyMapper.from(connectionDetails.getUrls()).to(source::setUrls); propertyMapper.from(properties.getBaseEnvironment()) .to((baseEnvironment) -> source.setBaseEnvironmentProperties(Collections.unmodifiableMap(baseEnvironment))); return source; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapConnectionDetails.java new file mode 100644 index 000000000000..9e034221ce78 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapConnectionDetails.java @@ -0,0 +1,59 @@ +/* + * 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.ldap; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to a Ldap service. + * + * @author Philipp Kessler + * @since 3.3.0 + */ +public interface LdapConnectionDetails extends ConnectionDetails { + + /** + * LDAP URLs of the server. + * @return list of the LDAP urls to use + */ + String[] getUrls(); + + /** + * Base suffix from which all operations should originate. + * @return base suffix from which all operations should originate or null. + */ + default String getBase() { + return null; + } + + /** + * Login username of the server. + * @return login username of the server or null. + */ + default String getUsername() { + return null; + } + + /** + * Login password of the server. + * @return login password of the server or null. + */ + default String getPassword() { + return null; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/PropertiesLdapConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/PropertiesLdapConnectionDetails.java new file mode 100644 index 000000000000..517adc811663 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/PropertiesLdapConnectionDetails.java @@ -0,0 +1,58 @@ +/* + * 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.ldap; + +import org.springframework.core.env.Environment; + +/** + * Adapts {@link LdapProperties} to {@link LdapConnectionDetails}. + * + * @author Philipp Kessler + * @since 3.3.0 + */ +public class PropertiesLdapConnectionDetails implements LdapConnectionDetails { + + private final LdapProperties properties; + + private final Environment environment; + + PropertiesLdapConnectionDetails(LdapProperties properties, Environment environment) { + this.properties = properties; + this.environment = environment; + } + + @Override + public String[] getUrls() { + return this.properties.determineUrls(this.environment); + } + + @Override + public String getBase() { + return this.properties.getBase(); + } + + @Override + public String getUsername() { + return this.properties.getUsername(); + } + + @Override + public String getPassword() { + return this.properties.getPassword(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfigurationTests.java index bde394cc67a1..b4b9f8154a0f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfigurationTests.java @@ -29,6 +29,7 @@ import org.springframework.ldap.core.support.SimpleDirContextAuthenticationStrategy; import org.springframework.ldap.pool2.factory.PoolConfig; import org.springframework.ldap.pool2.factory.PooledContextSource; +import org.springframework.ldap.support.LdapUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -112,6 +113,25 @@ void contextSourceWithNoCustomization() { }); } + @Test + void definesPropertiesBasedConnectionDetailsByDefault() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(PropertiesLdapConnectionDetails.class)); + } + + @Test + void shouldUseCustomConnectionDetailsWhenDefined() { + this.contextRunner.withUserConfiguration(ConnectionDetailsConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(LdapContextSource.class) + .hasSingleBean(LdapConnectionDetails.class) + .doesNotHaveBean(PropertiesLdapConnectionDetails.class); + LdapContextSource contextSource = context.getBean(LdapContextSource.class); + assertThat(contextSource.getUrls()).isEqualTo(new String[] { "ldaps://ldap.example.com" }); + assertThat(contextSource.getBaseLdapName()).isEqualTo(LdapUtils.newLdapName("dc=base")); + assertThat(contextSource.getUserDn()).isEqualTo("ldap-user"); + assertThat(contextSource.getPassword()).isEqualTo("ldap-password"); + }); + } + @Test void templateExists() { this.contextRunner.withPropertyValues("spring.ldap.urls:ldap://localhost:389").run((context) -> { @@ -174,6 +194,37 @@ void contextSourceWithCustomNonUniqueDirContextAuthenticationStrategy() { }); } + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsConfiguration { + + @Bean + LdapConnectionDetails ldapConnectionDetails() { + return new LdapConnectionDetails() { + + @Override + public String[] getUrls() { + return new String[] { "ldaps://ldap.example.com" }; + } + + @Override + public String getBase() { + return "dc=base"; + } + + @Override + public String getUsername() { + return "ldap-user"; + } + + @Override + public String getPassword() { + return "ldap-password"; + } + }; + } + + } + @Configuration(proxyBeanMethods = false) static class PooledContextSourceConfig { diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ldap/LdapDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ldap/LdapDockerComposeConnectionDetailsFactory.java new file mode 100644 index 000000000000..59850f2228cb --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ldap/LdapDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,99 @@ +/* + * 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.docker.compose.service.connection.ldap; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.boot.autoconfigure.ldap.LdapConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link LdapConnectionDetails} + * for an {@code ldap} service. + * + * @author Philipp Kessler + */ +class LdapDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { + + protected LdapDockerComposeConnectionDetailsFactory() { + super("osixia/openldap"); + } + + @Override + protected LdapConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new LdapDockerComposeConnectionDetails(source.getRunningService()); + } + + /** + * {@link LdapConnectionDetails} backed by an {@code openldap} {@link RunningService}. + */ + static class LdapDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements LdapConnectionDetails { + + private final String[] urls; + + private final String base; + + private final String username; + + private final String password; + + LdapDockerComposeConnectionDetails(RunningService service) { + super(service); + Map env = service.env(); + boolean usesTls = Boolean.parseBoolean(env.getOrDefault("LDAP_TLS", "true")); + String ldapPort = usesTls ? env.getOrDefault("LDAPS_PORT", "636") : env.getOrDefault("LDAP_PORT", "389"); + this.urls = new String[] { "%s://%s:%d".formatted(usesTls ? "ldaps" : "ldap", service.host(), + service.ports().get(Integer.parseInt(ldapPort))) }; + String baseDn = env.getOrDefault("LDAP_BASE_DN", null); + if (baseDn == null) { + baseDn = Arrays.stream(env.getOrDefault("LDAP_DOMAIN", "example.org").split("\\.")) + .map("dc=%s"::formatted) + .collect(Collectors.joining(",")); + } + this.base = baseDn; + this.password = env.getOrDefault("LDAP_ADMIN_PASSWORD", "admin"); + this.username = "cn=admin,%s".formatted(this.base); + } + + @Override + public String[] getUrls() { + return this.urls; + } + + @Override + public String getBase() { + return this.base; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public String getPassword() { + return this.password; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ldap/package-info.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ldap/package-info.java new file mode 100644 index 000000000000..1953e3f3b901 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ldap/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. + */ + +/** + * Auto-configuration for docker compose Ldap service connections. + */ +package org.springframework.boot.docker.compose.service.connection.ldap; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories index f00fc1938155..71a9417c44fc 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories @@ -9,6 +9,7 @@ org.springframework.boot.docker.compose.service.connection.activemq.ActiveMQDock org.springframework.boot.docker.compose.service.connection.cassandra.CassandraDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.elasticsearch.ElasticsearchDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.flyway.JdbcAdaptingFlywayConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.ldap.LdapDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.liquibase.JdbcAdaptingLiquibaseConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.mariadb.MariaDbJdbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.mariadb.MariaDbR2dbcDockerComposeConnectionDetailsFactory,\ diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ldap/LdapDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ldap/LdapDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..150e3e11d2e5 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ldap/LdapDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,48 @@ +/* + * 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.docker.compose.service.connection.ldap; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.ldap.LdapConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link LdapDockerComposeConnectionDetailsFactory}. + * + * @author Philipp Kessler + */ +class LdapDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { + + LdapDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("ldap-compose.yaml", DockerImageNames.ldap()); + } + + @Test + void runCreatesConnectionDetails() { + LdapConnectionDetails connectionDetails = run(LdapConnectionDetails.class); + assertThat(connectionDetails.getUsername()).isEqualTo("cn=admin,dc=ldap,dc=example,dc=org"); + assertThat(connectionDetails.getPassword()).isEqualTo("somepassword"); + assertThat(connectionDetails.getBase()).isEqualTo("dc=ldap,dc=example,dc=org"); + assertThat(connectionDetails.getUrls()).hasSize(1); + assertThat(connectionDetails.getUrls()[0]).startsWith("ldaps://"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/ldap/ldap-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/ldap/ldap-compose.yaml new file mode 100644 index 000000000000..a55e16be4358 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/ldap/ldap-compose.yaml @@ -0,0 +1,11 @@ +services: + ldap: + image: '{imageName}' + environment: + - 'LDAP_DOMAIN=ldap.example.org' + - 'LDAP_ADMIN_PASSWORD=somepassword' + - 'LDAP_TLS=true' + hostname: ldap + ports: + - "389" + - "636" diff --git a/spring-boot-project/spring-boot-testcontainers/build.gradle b/spring-boot-project/spring-boot-testcontainers/build.gradle index 7d7792b8ade8..28b62213219a 100644 --- a/spring-boot-project/spring-boot-testcontainers/build.gradle +++ b/spring-boot-project/spring-boot-testcontainers/build.gradle @@ -57,6 +57,7 @@ dependencies { testImplementation("org.springframework:spring-r2dbc") testImplementation("org.springframework.amqp:spring-rabbit") testImplementation("org.springframework.kafka:spring-kafka") + testImplementation("org.springframework.ldap:spring-ldap-core") testImplementation("org.springframework.pulsar:spring-pulsar") testImplementation("org.testcontainers:junit-jupiter") diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ldap/LdapContainerConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ldap/LdapContainerConnectionDetailsFactory.java new file mode 100644 index 000000000000..a708347bdfc5 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ldap/LdapContainerConnectionDetailsFactory.java @@ -0,0 +1,90 @@ +/* + * 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.testcontainers.service.connection.ldap; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +import org.testcontainers.containers.Container; +import org.testcontainers.containers.GenericContainer; + +import org.springframework.boot.autoconfigure.ldap.LdapConnectionDetails; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; + +/** + * {@link ContainerConnectionDetailsFactory} to create {@link LdapConnectionDetails} from + * a {@link ServiceConnection @ServiceConnection}-annotated {@link GenericContainer} using + * the {@code "osixia/openldap"} image. + * + * @author Philipp Kessler + */ +class LdapContainerConnectionDetailsFactory + extends ContainerConnectionDetailsFactory, LdapConnectionDetails> { + + LdapContainerConnectionDetailsFactory() { + super("osixia/openldap"); + } + + @Override + protected LdapConnectionDetails getContainerConnectionDetails(ContainerConnectionSource> source) { + return new LdapContainerConnectionDetailsFactory.LdapContainerConnectionDetails(source); + } + + private static final class LdapContainerConnectionDetails extends ContainerConnectionDetails> + implements LdapConnectionDetails { + + private LdapContainerConnectionDetails(ContainerConnectionSource> source) { + super(source); + } + + @Override + public String[] getUrls() { + Map env = getContainer().getEnvMap(); + boolean usesTls = Boolean.parseBoolean(env.getOrDefault("LDAP_TLS", "true")); + String ldapPort = usesTls ? env.getOrDefault("LDAPS_PORT", "636") : env.getOrDefault("LDAP_PORT", "389"); + return new String[] { "%s://%s:%d".formatted(usesTls ? "ldaps" : "ldap", getContainer().getHost(), + getContainer().getMappedPort(Integer.parseInt(ldapPort))) }; + } + + @Override + public String getBase() { + String baseDn = getContainer().getEnvMap().getOrDefault("LDAP_BASE_DN", null); + if (baseDn == null) { + baseDn = Arrays + .stream(getContainer().getEnvMap().getOrDefault("LDAP_DOMAIN", "example.org").split("\\.")) + .map("dc=%s"::formatted) + .collect(Collectors.joining(",")); + } + return baseDn; + } + + @Override + public String getUsername() { + return "cn=admin,%s".formatted(getBase()); + } + + @Override + public String getPassword() { + return getContainer().getEnvMap().getOrDefault("LDAP_ADMIN_PASSWORD", "admin"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ldap/package-info.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ldap/package-info.java new file mode 100644 index 000000000000..0de36b546985 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ldap/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. + */ + +/** + * Support for testcontainers Ldap service connections. + */ +package org.springframework.boot.testcontainers.service.connection.ldap; diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories index 386f2a1a3553..a8851d15b910 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories @@ -16,6 +16,7 @@ org.springframework.boot.testcontainers.service.connection.flyway.FlywayContaine org.springframework.boot.testcontainers.service.connection.elasticsearch.ElasticsearchContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.jdbc.JdbcContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.kafka.KafkaContainerConnectionDetailsFactory,\ +org.springframework.boot.testcontainers.service.connection.ldap.LdapContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.liquibase.LiquibaseContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.mongo.MongoContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.neo4j.Neo4jContainerConnectionDetailsFactory,\ diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ldap/LdapContainerConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ldap/LdapContainerConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..f44c47afff37 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ldap/LdapContainerConnectionDetailsFactoryIntegrationTests.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.testcontainers.service.connection.ldap; + +import java.util.List; + +import javax.naming.NamingException; +import javax.naming.directory.Attributes; + +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.testcontainers.LdapContainer; +import org.springframework.context.annotation.Configuration; +import org.springframework.ldap.core.AttributesMapper; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.ldap.query.LdapQueryBuilder; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link LdapContainerConnectionDetailsFactory}. + * + * @author Philipp Kessler + */ +@SpringJUnitConfig +@Testcontainers(disabledWithoutDocker = true) +class LdapContainerConnectionDetailsFactoryIntegrationTests { + + @Container + @ServiceConnection + static final LdapContainer openLdap = new LdapContainer().withEnv("LDAP_TLS", "false"); + + @Autowired + private LdapTemplate ldapTemplate; + + @Test + void connectionCanBeMadeToLdapContainer() { + List cn = this.ldapTemplate.search(LdapQueryBuilder.query().where("objectclass").is("dcObject"), + new AttributesMapper() { + @Override + public String mapFromAttributes(Attributes attributes) throws NamingException { + return attributes.get("dc").get().toString(); + } + }); + assertThat(cn).hasSize(1); + assertThat(cn.get(0)).isEqualTo("example"); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration({ LdapAutoConfiguration.class }) + static class TestConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java index 4bb98d22018d..19ee2b3d6e39 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java @@ -40,6 +40,8 @@ public final class DockerImageNames { private static final String KAFKA_VERSION = "7.4.0"; + private static final String LDAP_VERSION = "1.5.0"; + private static final String MARIADB_VERSION = "10.10"; private static final String MONGO_VERSION = "5.0.17"; @@ -119,6 +121,14 @@ public static DockerImageName kafka() { return DockerImageName.parse("confluentinc/cp-kafka").withTag(KAFKA_VERSION); } + /** + * Return a {@link DockerImageName} suitable for running OpenLdap. + * @return a docker image name for running OpenLdap + */ + public static DockerImageName ldap() { + return DockerImageName.parse("osixia/openldap").withTag(LDAP_VERSION); + } + /** * Return a {@link DockerImageName} suitable for running MariaDB. * @return a docker image name for running MariaDB diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/LdapContainer.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/LdapContainer.java new file mode 100644 index 000000000000..3405e138ed31 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/LdapContainer.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.testsupport.testcontainers; + +import org.testcontainers.containers.GenericContainer; + +/** + * A {@link GenericContainer} for OpenLdap. + * + * @author Philipp Kessler + */ +public class LdapContainer extends GenericContainer { + + private static final int DEFAULT_LDAP_PORT = 389; + + public LdapContainer() { + super(DockerImageNames.ldap()); + addExposedPorts(DEFAULT_LDAP_PORT); + } + +} From bee6fe899c16b1bae911bf7e84e3aa78089fc80f Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Mon, 22 Jan 2024 16:33:59 -0600 Subject: [PATCH 1108/1215] Polish "Add Docker Compose service connection support for OpenLDAP" See gh-39258 --- .../ldap/LdapAutoConfiguration.java | 2 +- .../ldap/LdapConnectionDetails.java | 10 ++++---- .../ldap/LdapAutoConfigurationTests.java | 4 ++-- ...ockerComposeConnectionDetailsFactory.java} | 20 ++++++++-------- .../service/connection/ldap/package-info.java | 2 +- .../main/resources/META-INF/spring.factories | 2 +- ...ectionDetailsFactoryIntegrationTests.java} | 8 +++---- .../asciidoc/features/docker-compose.adoc | 3 +++ ...dapContainerConnectionDetailsFactory.java} | 23 +++++++++---------- .../main/resources/META-INF/spring.factories | 2 +- ...ectionDetailsFactoryIntegrationTests.java} | 18 ++++----------- .../testcontainers/DockerImageNames.java | 14 +++++------ ...pContainer.java => OpenLdapContainer.java} | 8 +++---- 13 files changed, 56 insertions(+), 60 deletions(-) rename spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ldap/{LdapDockerComposeConnectionDetailsFactory.java => OpenLdapDockerComposeConnectionDetailsFactory.java} (80%) rename spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ldap/{LdapDockerComposeConnectionDetailsFactoryIntegrationTests.java => OpenLdapDockerComposeConnectionDetailsFactoryIntegrationTests.java} (83%) rename spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ldap/{LdapContainerConnectionDetailsFactory.java => OpenLdapContainerConnectionDetailsFactory.java} (78%) rename spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ldap/{LdapContainerConnectionDetailsFactoryIntegrationTests.java => OpenLdapContainerConnectionDetailsFactoryIntegrationTests.java} (78%) rename spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/{LdapContainer.java => OpenLdapContainer.java} (82%) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfiguration.java index 0bd691445d10..205af4c3e0ac 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapConnectionDetails.java index 9e034221ce78..efec54659440 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapConnectionDetails.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/LdapConnectionDetails.java @@ -19,7 +19,7 @@ import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; /** - * Details required to establish a connection to a Ldap service. + * Details required to establish a connection to an LDAP service. * * @author Philipp Kessler * @since 3.3.0 @@ -28,13 +28,13 @@ public interface LdapConnectionDetails extends ConnectionDetails { /** * LDAP URLs of the server. - * @return list of the LDAP urls to use + * @return the LDAP URLs to use */ String[] getUrls(); /** * Base suffix from which all operations should originate. - * @return base suffix from which all operations should originate or null. + * @return base suffix */ default String getBase() { return null; @@ -42,7 +42,7 @@ default String getBase() { /** * Login username of the server. - * @return login username of the server or null. + * @return login username */ default String getUsername() { return null; @@ -50,7 +50,7 @@ default String getUsername() { /** * Login password of the server. - * @return login password of the server or null. + * @return login password */ default String getPassword() { return null; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfigurationTests.java index b4b9f8154a0f..2d51f84a9b7b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ldap/LdapAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -119,7 +119,7 @@ void definesPropertiesBasedConnectionDetailsByDefault() { } @Test - void shouldUseCustomConnectionDetailsWhenDefined() { + void usesCustomConnectionDetailsWhenDefined() { this.contextRunner.withUserConfiguration(ConnectionDetailsConfiguration.class).run((context) -> { assertThat(context).hasSingleBean(LdapContextSource.class) .hasSingleBean(LdapConnectionDetails.class) diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ldap/LdapDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ldap/OpenLdapDockerComposeConnectionDetailsFactory.java similarity index 80% rename from spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ldap/LdapDockerComposeConnectionDetailsFactory.java rename to spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ldap/OpenLdapDockerComposeConnectionDetailsFactory.java index 59850f2228cb..b5674f152ff4 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ldap/LdapDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ldap/OpenLdapDockerComposeConnectionDetailsFactory.java @@ -31,21 +31,22 @@ * * @author Philipp Kessler */ -class LdapDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { +class OpenLdapDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { - protected LdapDockerComposeConnectionDetailsFactory() { + protected OpenLdapDockerComposeConnectionDetailsFactory() { super("osixia/openldap"); } @Override protected LdapConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { - return new LdapDockerComposeConnectionDetails(source.getRunningService()); + return new OpenLdapDockerComposeConnectionDetails(source.getRunningService()); } /** * {@link LdapConnectionDetails} backed by an {@code openldap} {@link RunningService}. */ - static class LdapDockerComposeConnectionDetails extends DockerComposeConnectionDetails + static class OpenLdapDockerComposeConnectionDetails extends DockerComposeConnectionDetails implements LdapConnectionDetails { private final String[] urls; @@ -56,20 +57,21 @@ static class LdapDockerComposeConnectionDetails extends DockerComposeConnectionD private final String password; - LdapDockerComposeConnectionDetails(RunningService service) { + OpenLdapDockerComposeConnectionDetails(RunningService service) { super(service); Map env = service.env(); boolean usesTls = Boolean.parseBoolean(env.getOrDefault("LDAP_TLS", "true")); String ldapPort = usesTls ? env.getOrDefault("LDAPS_PORT", "636") : env.getOrDefault("LDAP_PORT", "389"); this.urls = new String[] { "%s://%s:%d".formatted(usesTls ? "ldaps" : "ldap", service.host(), service.ports().get(Integer.parseInt(ldapPort))) }; - String baseDn = env.getOrDefault("LDAP_BASE_DN", null); - if (baseDn == null) { - baseDn = Arrays.stream(env.getOrDefault("LDAP_DOMAIN", "example.org").split("\\.")) + if (env.containsKey("LDAP_BASE_DN")) { + this.base = env.get("LDAP_BASE_DN"); + } + else { + this.base = Arrays.stream(env.getOrDefault("LDAP_DOMAIN", "example.org").split("\\.")) .map("dc=%s"::formatted) .collect(Collectors.joining(",")); } - this.base = baseDn; this.password = env.getOrDefault("LDAP_ADMIN_PASSWORD", "admin"); this.username = "cn=admin,%s".formatted(this.base); } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ldap/package-info.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ldap/package-info.java index 1953e3f3b901..489148d2777e 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ldap/package-info.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/ldap/package-info.java @@ -15,6 +15,6 @@ */ /** - * Auto-configuration for docker compose Ldap service connections. + * Auto-configuration for Docker Compose LDAP service connections. */ package org.springframework.boot.docker.compose.service.connection.ldap; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories index 71a9417c44fc..3ffd311e3631 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories @@ -9,7 +9,7 @@ org.springframework.boot.docker.compose.service.connection.activemq.ActiveMQDock org.springframework.boot.docker.compose.service.connection.cassandra.CassandraDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.elasticsearch.ElasticsearchDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.flyway.JdbcAdaptingFlywayConnectionDetailsFactory,\ -org.springframework.boot.docker.compose.service.connection.ldap.LdapDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.ldap.OpenLdapDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.liquibase.JdbcAdaptingLiquibaseConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.mariadb.MariaDbJdbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.mariadb.MariaDbR2dbcDockerComposeConnectionDetailsFactory,\ diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ldap/LdapDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ldap/OpenLdapDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 83% rename from spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ldap/LdapDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ldap/OpenLdapDockerComposeConnectionDetailsFactoryIntegrationTests.java index 150e3e11d2e5..57756fe01240 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ldap/LdapDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ldap/OpenLdapDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -25,14 +25,14 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link LdapDockerComposeConnectionDetailsFactory}. + * Integration tests for {@link OpenLdapDockerComposeConnectionDetailsFactory}. * * @author Philipp Kessler */ -class LdapDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { +class OpenLdapDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { - LdapDockerComposeConnectionDetailsFactoryIntegrationTests() { - super("ldap-compose.yaml", DockerImageNames.ldap()); + OpenLdapDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("ldap-compose.yaml", DockerImageNames.openLdap()); } @Test diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc index 6ee9c0172a71..6b8c5688e232 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc @@ -81,6 +81,9 @@ The following service connections are currently supported: | `JdbcConnectionDetails` | Containers named "gvenzl/oracle-free", "gvenzl/oracle-xe", "mariadb", "mssql/server", "mysql", or "postgres" +| `LdapConnectionDetails` +| Containers named "osixia/openldap" + | `MongoConnectionDetails` | Containers named "mongo" diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ldap/LdapContainerConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ldap/OpenLdapContainerConnectionDetailsFactory.java similarity index 78% rename from spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ldap/LdapContainerConnectionDetailsFactory.java rename to spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ldap/OpenLdapContainerConnectionDetailsFactory.java index a708347bdfc5..fd752811cdde 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ldap/LdapContainerConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ldap/OpenLdapContainerConnectionDetailsFactory.java @@ -35,22 +35,22 @@ * * @author Philipp Kessler */ -class LdapContainerConnectionDetailsFactory +class OpenLdapContainerConnectionDetailsFactory extends ContainerConnectionDetailsFactory, LdapConnectionDetails> { - LdapContainerConnectionDetailsFactory() { + OpenLdapContainerConnectionDetailsFactory() { super("osixia/openldap"); } @Override protected LdapConnectionDetails getContainerConnectionDetails(ContainerConnectionSource> source) { - return new LdapContainerConnectionDetailsFactory.LdapContainerConnectionDetails(source); + return new OpenLdapContainerConnectionDetails(source); } - private static final class LdapContainerConnectionDetails extends ContainerConnectionDetails> + private static final class OpenLdapContainerConnectionDetails extends ContainerConnectionDetails> implements LdapConnectionDetails { - private LdapContainerConnectionDetails(ContainerConnectionSource> source) { + private OpenLdapContainerConnectionDetails(ContainerConnectionSource> source) { super(source); } @@ -65,14 +65,13 @@ public String[] getUrls() { @Override public String getBase() { - String baseDn = getContainer().getEnvMap().getOrDefault("LDAP_BASE_DN", null); - if (baseDn == null) { - baseDn = Arrays - .stream(getContainer().getEnvMap().getOrDefault("LDAP_DOMAIN", "example.org").split("\\.")) - .map("dc=%s"::formatted) - .collect(Collectors.joining(",")); + Map env = getContainer().getEnvMap(); + if (env.containsKey("LDAP_BASE_DN")) { + return env.get("LDAP_BASE_DN"); } - return baseDn; + return Arrays.stream(env.getOrDefault("LDAP_DOMAIN", "example.org").split("\\.")) + .map("dc=%s"::formatted) + .collect(Collectors.joining(",")); } @Override diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories index a8851d15b910..5b8bda347fcb 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories @@ -16,7 +16,7 @@ org.springframework.boot.testcontainers.service.connection.flyway.FlywayContaine org.springframework.boot.testcontainers.service.connection.elasticsearch.ElasticsearchContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.jdbc.JdbcContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.kafka.KafkaContainerConnectionDetailsFactory,\ -org.springframework.boot.testcontainers.service.connection.ldap.LdapContainerConnectionDetailsFactory,\ +org.springframework.boot.testcontainers.service.connection.ldap.OpenLdapContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.liquibase.LiquibaseContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.mongo.MongoContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.neo4j.Neo4jContainerConnectionDetailsFactory,\ diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ldap/LdapContainerConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ldap/OpenLdapContainerConnectionDetailsFactoryIntegrationTests.java similarity index 78% rename from spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ldap/LdapContainerConnectionDetailsFactoryIntegrationTests.java rename to spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ldap/OpenLdapContainerConnectionDetailsFactoryIntegrationTests.java index f44c47afff37..4bae83e11bc9 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ldap/LdapContainerConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/ldap/OpenLdapContainerConnectionDetailsFactoryIntegrationTests.java @@ -18,9 +18,6 @@ import java.util.List; -import javax.naming.NamingException; -import javax.naming.directory.Attributes; - import org.junit.jupiter.api.Test; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -29,7 +26,7 @@ import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.springframework.boot.testsupport.testcontainers.LdapContainer; +import org.springframework.boot.testsupport.testcontainers.OpenLdapContainer; import org.springframework.context.annotation.Configuration; import org.springframework.ldap.core.AttributesMapper; import org.springframework.ldap.core.LdapTemplate; @@ -39,17 +36,17 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link LdapContainerConnectionDetailsFactory}. + * Tests for {@link OpenLdapContainerConnectionDetailsFactory}. * * @author Philipp Kessler */ @SpringJUnitConfig @Testcontainers(disabledWithoutDocker = true) -class LdapContainerConnectionDetailsFactoryIntegrationTests { +class OpenLdapContainerConnectionDetailsFactoryIntegrationTests { @Container @ServiceConnection - static final LdapContainer openLdap = new LdapContainer().withEnv("LDAP_TLS", "false"); + static final OpenLdapContainer openLdap = new OpenLdapContainer().withEnv("LDAP_TLS", "false"); @Autowired private LdapTemplate ldapTemplate; @@ -57,12 +54,7 @@ class LdapContainerConnectionDetailsFactoryIntegrationTests { @Test void connectionCanBeMadeToLdapContainer() { List cn = this.ldapTemplate.search(LdapQueryBuilder.query().where("objectclass").is("dcObject"), - new AttributesMapper() { - @Override - public String mapFromAttributes(Attributes attributes) throws NamingException { - return attributes.get("dc").get().toString(); - } - }); + (AttributesMapper) (attributes) -> attributes.get("dc").get().toString()); assertThat(cn).hasSize(1); assertThat(cn.get(0)).isEqualTo("example"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java index 19ee2b3d6e39..81a5673d8fec 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -40,8 +40,6 @@ public final class DockerImageNames { private static final String KAFKA_VERSION = "7.4.0"; - private static final String LDAP_VERSION = "1.5.0"; - private static final String MARIADB_VERSION = "10.10"; private static final String MONGO_VERSION = "5.0.17"; @@ -50,6 +48,8 @@ public final class DockerImageNames { private static final String NEO4J_VERSION = "4.4.11"; + private static final String OPEN_LDAP_VERSION = "1.5.0"; + private static final String ORACLE_FREE_VERSION = "23.3-slim"; private static final String ORACLE_XE_VERSION = "18.4.0-slim"; @@ -122,11 +122,11 @@ public static DockerImageName kafka() { } /** - * Return a {@link DockerImageName} suitable for running OpenLdap. - * @return a docker image name for running OpenLdap + * Return a {@link DockerImageName} suitable for running OpenLDAP. + * @return a docker image name for running OpenLDAP */ - public static DockerImageName ldap() { - return DockerImageName.parse("osixia/openldap").withTag(LDAP_VERSION); + public static DockerImageName openLdap() { + return DockerImageName.parse("osixia/openldap").withTag(OPEN_LDAP_VERSION); } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/LdapContainer.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/OpenLdapContainer.java similarity index 82% rename from spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/LdapContainer.java rename to spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/OpenLdapContainer.java index 3405e138ed31..93ac6e8abf3a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/LdapContainer.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/OpenLdapContainer.java @@ -19,16 +19,16 @@ import org.testcontainers.containers.GenericContainer; /** - * A {@link GenericContainer} for OpenLdap. + * A {@link GenericContainer} for OpenLDAP. * * @author Philipp Kessler */ -public class LdapContainer extends GenericContainer { +public class OpenLdapContainer extends GenericContainer { private static final int DEFAULT_LDAP_PORT = 389; - public LdapContainer() { - super(DockerImageNames.ldap()); + public OpenLdapContainer() { + super(DockerImageNames.openLdap()); addExposedPorts(DEFAULT_LDAP_PORT); } From c3aa95335a4401e0ef77d511df0cdd13b9a841bc Mon Sep 17 00:00:00 2001 From: Dennis Melzer Date: Wed, 13 Dec 2023 11:02:28 +0100 Subject: [PATCH 1109/1215] Add conditional bean for jOOQ translator Introduce an jOOQ `ExecuteListener` sub-interface specifically for exception translation with the auto-configured `DefaultExecuteListenerProvider` instance. Users can now define a bean that implements the interface or omit it and continue to use the existing exception translation logic. See gh-38762 --- .../jooq/JooqAutoConfiguration.java | 10 ++++- .../jooq/JooqExceptionTranslator.java | 5 +-- .../jooq/JooqExceptionTranslatorListener.java | 38 ++++++++++++++++++ .../jooq/JooqAutoConfigurationTests.java | 40 +++++++++++++++++++ 4 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorListener.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java index 72944ddf3f63..26b882e811a7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java @@ -70,8 +70,14 @@ public SpringTransactionProvider transactionProvider(PlatformTransactionManager @Bean @Order(0) - public DefaultExecuteListenerProvider jooqExceptionTranslatorExecuteListenerProvider() { - return new DefaultExecuteListenerProvider(new JooqExceptionTranslator()); + public DefaultExecuteListenerProvider jooqExceptionTranslatorExecuteListenerProvider(JooqExceptionTranslatorListener jooqExceptionTranslator) { + return new DefaultExecuteListenerProvider(jooqExceptionTranslator); + } + + @Bean + @ConditionalOnMissingBean(JooqExceptionTranslatorListener.class) + public JooqExceptionTranslatorListener jooqExceptionTranslator() { + return new JooqExceptionTranslator(); } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslator.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslator.java index 383da48c975b..b27d99213c81 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslator.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslator.java @@ -21,7 +21,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jooq.ExecuteContext; -import org.jooq.ExecuteListener; import org.jooq.SQLDialect; import org.springframework.dao.DataAccessException; @@ -30,7 +29,7 @@ import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator; /** - * Transforms {@link java.sql.SQLException} into a Spring-specific + * Transforms {@link SQLException} into a Spring-specific * {@link DataAccessException}. * * @author Lukas Eder @@ -39,7 +38,7 @@ * @author Stephane Nicoll * @since 1.5.10 */ -public class JooqExceptionTranslator implements ExecuteListener { +public class JooqExceptionTranslator implements JooqExceptionTranslatorListener { // Based on the jOOQ-spring-example from https://github.com/jOOQ/jOOQ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorListener.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorListener.java new file mode 100644 index 000000000000..a02b8508f21b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorListener.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2022 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.jooq; + +import org.jooq.ExecuteContext; +import org.jooq.ExecuteListener; + +import org.springframework.dao.DataAccessException; + +/** + * A {@link ExecuteListener} which can transforms or translate {@link Exception} into a Spring-specific + * {@link DataAccessException}. + * + * @author Dennis Melzer + * @since 3.2.1 + */ +public interface JooqExceptionTranslatorListener extends ExecuteListener { + + /** + * Override the given {@link Exception} from {@link ExecuteContext} into a generic {@link DataAccessException}. + * @param context The context containing information about the execution. + */ + void exception(ExecuteContext context); +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfigurationTests.java index 1e78fee1d9ad..d2c41e3196c1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfigurationTests.java @@ -22,6 +22,7 @@ import org.jooq.ConnectionProvider; import org.jooq.ConverterProvider; import org.jooq.DSLContext; +import org.jooq.ExecuteContext; import org.jooq.ExecuteListener; import org.jooq.ExecuteListenerProvider; import org.jooq.SQLDialect; @@ -55,6 +56,7 @@ * @author Andy Wilkinson * @author Stephane Nicoll * @author Dmytro Nosan + * @author Dennis Melzer */ class JooqAutoConfigurationTests { @@ -180,6 +182,25 @@ void transactionProviderBacksOffOnExistingTransactionProvider() { }); } + @Test + void jooqExceptionTranslatorProviderFromConfigurationCustomizerOverridesJooqExceptionTranslatorBean() { + this.contextRunner + .withUserConfiguration(JooqDataSourceConfiguration.class, CustomJooqExceptionTranslatorConfiguration.class) + .run((context) -> { + JooqExceptionTranslatorListener translator = context.getBean(JooqExceptionTranslatorListener.class); + assertThat(translator).isInstanceOf(CustomJooqExceptionTranslator.class); + }); + } + + @Test + void jooqWithDefaultJooqExceptionTranslator() { + this.contextRunner.withUserConfiguration(JooqDataSourceConfiguration.class).run((context) -> { + JooqExceptionTranslatorListener translator = context.getBean(JooqExceptionTranslatorListener.class); + assertThat(translator).isInstanceOf(JooqExceptionTranslator.class); + }); + } + + @Test void transactionProviderFromConfigurationCustomizerOverridesTransactionProviderBean() { this.contextRunner @@ -254,6 +275,16 @@ TransactionProvider transactionProvider() { } + @Configuration(proxyBeanMethods = false) + static class CustomJooqExceptionTranslatorConfiguration { + + @Bean + JooqExceptionTranslatorListener jooqExceptionTranslator() { + return new CustomJooqExceptionTranslator(); + } + + } + @Configuration(proxyBeanMethods = false) static class CustomTransactionProviderFromCustomizerConfiguration { @@ -303,4 +334,13 @@ public void rollback(TransactionContext ctx) { } + static class CustomJooqExceptionTranslator implements JooqExceptionTranslatorListener { + + + @Override + public void exception(ExecuteContext context) { + + } + } + } From 4de91094cf18fe615feb96ac3467761533dcfcb3 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 22 Jan 2024 15:47:55 -0800 Subject: [PATCH 1110/1215] Polish 'Add conditional bean for jOOQ translator' See gh-38762 --- ...ultExceptionTranslatorExecuteListener.java | 126 ++++++++++++++++++ .../ExceptionTranslatorExecuteListener.java | 59 ++++++++ .../jooq/JooqAutoConfiguration.java | 13 +- .../jooq/JooqExceptionTranslator.java | 68 ++-------- .../jooq/JooqExceptionTranslatorListener.java | 38 ------ ...ceptionTranslatorExecuteListenerTests.java | 112 ++++++++++++++++ .../jooq/JooqAutoConfigurationTests.java | 29 ++-- .../jooq/JooqExceptionTranslatorTests.java | 4 +- 8 files changed, 330 insertions(+), 119 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/DefaultExceptionTranslatorExecuteListener.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/ExceptionTranslatorExecuteListener.java delete mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorListener.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/DefaultExceptionTranslatorExecuteListenerTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/DefaultExceptionTranslatorExecuteListener.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/DefaultExceptionTranslatorExecuteListener.java new file mode 100644 index 000000000000..199a62dda0e0 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/DefaultExceptionTranslatorExecuteListener.java @@ -0,0 +1,126 @@ +/* + * 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.jooq; + +import java.sql.SQLException; +import java.util.function.Function; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jooq.ExecuteContext; +import org.jooq.SQLDialect; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator; +import org.springframework.jdbc.support.SQLExceptionTranslator; +import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link ExceptionTranslatorExecuteListener} that delegates to + * an {@link SQLExceptionTranslator}. + * + * @author Lukas Eder + * @author Andreas Ahlenstorf + * @author Phillip Webb + * @author Stephane Nicoll + */ +final class DefaultExceptionTranslatorExecuteListener implements ExceptionTranslatorExecuteListener { + + // Based on the jOOQ-spring-example from https://github.com/jOOQ/jOOQ + + private static final Log defaultLogger = LogFactory.getLog(ExceptionTranslatorExecuteListener.class); + + private final Log logger; + + private Function translatorFactory; + + DefaultExceptionTranslatorExecuteListener() { + this(defaultLogger, new DefaultTranslatorFactory()); + } + + DefaultExceptionTranslatorExecuteListener(Function translatorFactory) { + this(defaultLogger, translatorFactory); + } + + DefaultExceptionTranslatorExecuteListener(Log logger) { + this(logger, new DefaultTranslatorFactory()); + } + + private DefaultExceptionTranslatorExecuteListener(Log logger, + Function translatorFactory) { + Assert.notNull(translatorFactory, "TranslatorFactory must not be null"); + this.logger = logger; + this.translatorFactory = translatorFactory; + } + + @Override + public void exception(ExecuteContext context) { + SQLExceptionTranslator translator = this.translatorFactory.apply(context); + // The exception() callback is not only triggered for SQL exceptions but also for + // "normal" exceptions. In those cases sqlException() returns null. + SQLException exception = context.sqlException(); + while (exception != null) { + handle(context, translator, exception); + exception = exception.getNextException(); + } + } + + /** + * Handle a single exception in the chain. SQLExceptions might be nested multiple + * levels deep. The outermost exception is usually the least interesting one ("Call + * getNextException to see the cause."). Therefore the innermost exception is + * propagated and all other exceptions are logged. + * @param context the execute context + * @param translator the exception translator + * @param exception the exception + */ + private void handle(ExecuteContext context, SQLExceptionTranslator translator, SQLException exception) { + DataAccessException translated = translator.translate("jOOQ", context.sql(), exception); + if (exception.getNextException() != null) { + this.logger.error("Execution of SQL statement failed.", (translated != null) ? translated : exception); + return; + } + if (translated != null) { + context.exception(translated); + } + } + + /** + * Default {@link SQLExceptionTranslator} factory that creates the translator based on + * the Spring DB name. + */ + private static final class DefaultTranslatorFactory implements Function { + + @Override + public SQLExceptionTranslator apply(ExecuteContext context) { + return apply(context.configuration().dialect()); + } + + private SQLExceptionTranslator apply(SQLDialect dialect) { + String dbName = getSpringDbName(dialect); + return (dbName != null) ? new SQLErrorCodeSQLExceptionTranslator(dbName) + : new SQLStateSQLExceptionTranslator(); + } + + private String getSpringDbName(SQLDialect dialect) { + return (dialect != null && dialect.thirdParty() != null) ? dialect.thirdParty().springDbName() : null; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/ExceptionTranslatorExecuteListener.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/ExceptionTranslatorExecuteListener.java new file mode 100644 index 000000000000..eca548dfc334 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/ExceptionTranslatorExecuteListener.java @@ -0,0 +1,59 @@ +/* + * 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.jooq; + +import java.sql.SQLException; +import java.util.function.Function; + +import org.jooq.ExecuteContext; +import org.jooq.ExecuteListener; +import org.jooq.impl.DefaultExecuteListenerProvider; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.support.SQLExceptionTranslator; + +/** + * An {@link ExecuteListener} used by the auto-configured + * {@link DefaultExecuteListenerProvider} to translate exceptions in the + * {@link ExecuteContext}. Most commonly used to translate {@link SQLException + * SQLExceptions} to Spring-specific {@link DataAccessException DataAccessExceptions} by + * adapting an existing {@link SQLExceptionTranslator}. + * + * @author Dennis Melzer + * @since 3.3.0 + * @see #DEFAULT + * @see #of(Function) + */ +public interface ExceptionTranslatorExecuteListener extends ExecuteListener { + + /** + * Default {@link ExceptionTranslatorExecuteListener} suitable for most applications. + */ + ExceptionTranslatorExecuteListener DEFAULT = new DefaultExceptionTranslatorExecuteListener(); + + /** + * Creates a new {@link ExceptionTranslatorExecuteListener} backed by an + * {@link SQLExceptionTranslator}. + * @param translatorFactory factory function used to create the + * {@link SQLExceptionTranslator} + * @return a new {@link ExceptionTranslatorExecuteListener} instance + */ + static ExceptionTranslatorExecuteListener of(Function translatorFactory) { + return new DefaultExceptionTranslatorExecuteListener(translatorFactory); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java index 26b882e811a7..4580ed021ccd 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -70,14 +70,15 @@ public SpringTransactionProvider transactionProvider(PlatformTransactionManager @Bean @Order(0) - public DefaultExecuteListenerProvider jooqExceptionTranslatorExecuteListenerProvider(JooqExceptionTranslatorListener jooqExceptionTranslator) { - return new DefaultExecuteListenerProvider(jooqExceptionTranslator); + public DefaultExecuteListenerProvider jooqExceptionTranslatorExecuteListenerProvider( + ExceptionTranslatorExecuteListener exceptionTranslatorExecuteListener) { + return new DefaultExecuteListenerProvider(exceptionTranslatorExecuteListener); } @Bean - @ConditionalOnMissingBean(JooqExceptionTranslatorListener.class) - public JooqExceptionTranslatorListener jooqExceptionTranslator() { - return new JooqExceptionTranslator(); + @ConditionalOnMissingBean(ExceptionTranslatorExecuteListener.class) + public ExceptionTranslatorExecuteListener jooqExceptionTranslator() { + return ExceptionTranslatorExecuteListener.DEFAULT; } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslator.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslator.java index b27d99213c81..102bd59b0566 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslator.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -18,79 +18,33 @@ import java.sql.SQLException; -import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jooq.ExecuteContext; -import org.jooq.SQLDialect; +import org.jooq.ExecuteListener; import org.springframework.dao.DataAccessException; -import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator; -import org.springframework.jdbc.support.SQLExceptionTranslator; -import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator; /** - * Transforms {@link SQLException} into a Spring-specific - * {@link DataAccessException}. + * Transforms {@link SQLException} into a Spring-specific {@link DataAccessException}. * * @author Lukas Eder * @author Andreas Ahlenstorf * @author Phillip Webb * @author Stephane Nicoll * @since 1.5.10 + * @deprecated since 3.3.0 for removal in 3.5.0 in favor of + * {@link ExceptionTranslatorExecuteListener#DEFAULT} or + * {@link ExceptionTranslatorExecuteListener#of} */ -public class JooqExceptionTranslator implements JooqExceptionTranslatorListener { +@Deprecated(since = "3.3.0", forRemoval = true) +public class JooqExceptionTranslator implements ExecuteListener { - // Based on the jOOQ-spring-example from https://github.com/jOOQ/jOOQ - - private static final Log logger = LogFactory.getLog(JooqExceptionTranslator.class); + private final DefaultExceptionTranslatorExecuteListener delegate = new DefaultExceptionTranslatorExecuteListener( + LogFactory.getLog(JooqExceptionTranslator.class)); @Override public void exception(ExecuteContext context) { - SQLExceptionTranslator translator = getTranslator(context); - // The exception() callback is not only triggered for SQL exceptions but also for - // "normal" exceptions. In those cases sqlException() returns null. - SQLException exception = context.sqlException(); - while (exception != null) { - handle(context, translator, exception); - exception = exception.getNextException(); - } - } - - private SQLExceptionTranslator getTranslator(ExecuteContext context) { - SQLDialect dialect = context.configuration().dialect(); - if (dialect != null && dialect.thirdParty() != null) { - String dbName = dialect.thirdParty().springDbName(); - if (dbName != null) { - return new SQLErrorCodeSQLExceptionTranslator(dbName); - } - } - return new SQLStateSQLExceptionTranslator(); - } - - /** - * Handle a single exception in the chain. SQLExceptions might be nested multiple - * levels deep. The outermost exception is usually the least interesting one ("Call - * getNextException to see the cause."). Therefore the innermost exception is - * propagated and all other exceptions are logged. - * @param context the execute context - * @param translator the exception translator - * @param exception the exception - */ - private void handle(ExecuteContext context, SQLExceptionTranslator translator, SQLException exception) { - DataAccessException translated = translate(context, translator, exception); - if (exception.getNextException() == null) { - if (translated != null) { - context.exception(translated); - } - } - else { - logger.error("Execution of SQL statement failed.", (translated != null) ? translated : exception); - } - } - - private DataAccessException translate(ExecuteContext context, SQLExceptionTranslator translator, - SQLException exception) { - return translator.translate("jOOQ", context.sql(), exception); + this.delegate.exception(context); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorListener.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorListener.java deleted file mode 100644 index a02b8508f21b..000000000000 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorListener.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2012-2022 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.jooq; - -import org.jooq.ExecuteContext; -import org.jooq.ExecuteListener; - -import org.springframework.dao.DataAccessException; - -/** - * A {@link ExecuteListener} which can transforms or translate {@link Exception} into a Spring-specific - * {@link DataAccessException}. - * - * @author Dennis Melzer - * @since 3.2.1 - */ -public interface JooqExceptionTranslatorListener extends ExecuteListener { - - /** - * Override the given {@link Exception} from {@link ExecuteContext} into a generic {@link DataAccessException}. - * @param context The context containing information about the execution. - */ - void exception(ExecuteContext context); -} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/DefaultExceptionTranslatorExecuteListenerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/DefaultExceptionTranslatorExecuteListenerTests.java new file mode 100644 index 000000000000..ae1278889d1b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/DefaultExceptionTranslatorExecuteListenerTests.java @@ -0,0 +1,112 @@ +/* + * 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.jooq; + +import java.sql.SQLException; +import java.util.function.Function; + +import org.jooq.Configuration; +import org.jooq.ExecuteContext; +import org.jooq.SQLDialect; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.jdbc.support.SQLExceptionTranslator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.assertArg; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +/** + * Tests for {@link DefaultExceptionTranslatorExecuteListener}. + * + * @author Andy Wilkinson + * @author Phillip Webb + */ +class DefaultExceptionTranslatorExecuteListenerTests { + + private final ExceptionTranslatorExecuteListener listener = new DefaultExceptionTranslatorExecuteListener(); + + @Test + void createWhenTranslatorFactoryIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new DefaultExceptionTranslatorExecuteListener( + (Function) null)) + .withMessage("TranslatorFactory must not be null"); + } + + @ParameterizedTest(name = "{0}") + @MethodSource + void exceptionTranslatesSqlExceptions(SQLDialect dialect, SQLException sqlException) { + ExecuteContext context = mockContext(dialect, sqlException); + this.listener.exception(context); + then(context).should().exception(assertArg((ex) -> assertThat(ex).isInstanceOf(BadSqlGrammarException.class))); + } + + @Test + void exceptionWhenExceptionCannotBeTranslatedDoesNotCallExecuteContextException() { + ExecuteContext context = mockContext(SQLDialect.POSTGRES, new SQLException(null, null, 123456789)); + this.listener.exception(context); + then(context).should(never()).exception(any()); + } + + @Test + void exceptionWhenHasCustomTranslatorFactory() { + SQLExceptionTranslator translator = BadSqlGrammarException::new; + ExceptionTranslatorExecuteListener listener = new DefaultExceptionTranslatorExecuteListener( + (context) -> translator); + SQLException sqlException = sqlException(123); + ExecuteContext context = mockContext(SQLDialect.DUCKDB, sqlException); + listener.exception(context); + then(context).should().exception(assertArg((ex) -> assertThat(ex).isInstanceOf(BadSqlGrammarException.class))); + } + + private ExecuteContext mockContext(SQLDialect dialect, SQLException sqlException) { + ExecuteContext context = mock(ExecuteContext.class); + Configuration configuration = mock(Configuration.class); + given(context.configuration()).willReturn(configuration); + given(configuration.dialect()).willReturn(dialect); + given(context.sqlException()).willReturn(sqlException); + return context; + } + + static Object[] exceptionTranslatesSqlExceptions() { + return new Object[] { new Object[] { SQLDialect.DERBY, sqlException("42802") }, + new Object[] { SQLDialect.H2, sqlException(42000) }, + new Object[] { SQLDialect.HSQLDB, sqlException(-22) }, + new Object[] { SQLDialect.MARIADB, sqlException(1054) }, + new Object[] { SQLDialect.MYSQL, sqlException(1054) }, + new Object[] { SQLDialect.POSTGRES, sqlException("03000") }, + new Object[] { SQLDialect.SQLITE, sqlException("21000") } }; + } + + private static SQLException sqlException(String sqlState) { + return new SQLException(null, sqlState); + } + + private static SQLException sqlException(int vendorCode) { + return new SQLException(null, null, vendorCode); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfigurationTests.java index d2c41e3196c1..a26b63349159 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -22,7 +22,6 @@ import org.jooq.ConnectionProvider; import org.jooq.ConverterProvider; import org.jooq.DSLContext; -import org.jooq.ExecuteContext; import org.jooq.ExecuteListener; import org.jooq.ExecuteListenerProvider; import org.jooq.SQLDialect; @@ -185,22 +184,23 @@ void transactionProviderBacksOffOnExistingTransactionProvider() { @Test void jooqExceptionTranslatorProviderFromConfigurationCustomizerOverridesJooqExceptionTranslatorBean() { this.contextRunner - .withUserConfiguration(JooqDataSourceConfiguration.class, CustomJooqExceptionTranslatorConfiguration.class) - .run((context) -> { - JooqExceptionTranslatorListener translator = context.getBean(JooqExceptionTranslatorListener.class); - assertThat(translator).isInstanceOf(CustomJooqExceptionTranslator.class); - }); + .withUserConfiguration(JooqDataSourceConfiguration.class, CustomJooqExceptionTranslatorConfiguration.class) + .run((context) -> { + assertThat(context.getBean(ExceptionTranslatorExecuteListener.class)) + .isInstanceOf(CustomJooqExceptionTranslator.class); + assertThat(context.getBean(DefaultExecuteListenerProvider.class).provide()) + .isInstanceOf(CustomJooqExceptionTranslator.class); + }); } @Test void jooqWithDefaultJooqExceptionTranslator() { this.contextRunner.withUserConfiguration(JooqDataSourceConfiguration.class).run((context) -> { - JooqExceptionTranslatorListener translator = context.getBean(JooqExceptionTranslatorListener.class); - assertThat(translator).isInstanceOf(JooqExceptionTranslator.class); + ExceptionTranslatorExecuteListener translator = context.getBean(ExceptionTranslatorExecuteListener.class); + assertThat(translator).isInstanceOf(DefaultExceptionTranslatorExecuteListener.class); }); } - @Test void transactionProviderFromConfigurationCustomizerOverridesTransactionProviderBean() { this.contextRunner @@ -279,7 +279,7 @@ TransactionProvider transactionProvider() { static class CustomJooqExceptionTranslatorConfiguration { @Bean - JooqExceptionTranslatorListener jooqExceptionTranslator() { + ExceptionTranslatorExecuteListener jooqExceptionTranslator() { return new CustomJooqExceptionTranslator(); } @@ -334,13 +334,8 @@ public void rollback(TransactionContext ctx) { } - static class CustomJooqExceptionTranslator implements JooqExceptionTranslatorListener { - + static class CustomJooqExceptionTranslator implements ExceptionTranslatorExecuteListener { - @Override - public void exception(ExecuteContext context) { - - } } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorTests.java index 5a9f6685f144..f53b34880b38 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -40,6 +40,8 @@ * * @author Andy Wilkinson */ +@Deprecated(since = "3.3.0") +@SuppressWarnings("removal") class JooqExceptionTranslatorTests { private final JooqExceptionTranslator exceptionTranslator = new JooqExceptionTranslator(); From 17902c9cec8ec045b02b1983c4776039e8b481f5 Mon Sep 17 00:00:00 2001 From: Tobias Lippert Date: Sun, 21 Jan 2024 17:17:43 +0100 Subject: [PATCH 1111/1215] Remove unnecessary toString calls See gh-39259 --- .../boot/web/servlet/server/StaticResourceJarsTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/StaticResourceJarsTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/StaticResourceJarsTests.java index 231a20a20517..8643e81a54c8 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/StaticResourceJarsTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/StaticResourceJarsTests.java @@ -95,7 +95,7 @@ void ignoreWildcardUrls() throws Exception { void doesNotCloseJarFromCachedConnection() throws Exception { File jarFile = createResourcesJar("test-resources.jar"); TrackedURLStreamHandler handler = new TrackedURLStreamHandler(true); - URL url = new URL("jar", null, 0, jarFile.toURI().toURL().toString() + "!/", handler); + URL url = new URL("jar", null, 0, jarFile.toURI().toURL() + "!/", handler); try { new StaticResourceJars().getUrlsFrom(url); assertThatNoException() @@ -110,7 +110,7 @@ void doesNotCloseJarFromCachedConnection() throws Exception { void closesJarFromNonCachedConnection() throws Exception { File jarFile = createResourcesJar("test-resources.jar"); TrackedURLStreamHandler handler = new TrackedURLStreamHandler(false); - URL url = new URL("jar", null, 0, jarFile.toURI().toURL().toString() + "!/", handler); + URL url = new URL("jar", null, 0, jarFile.toURI().toURL() + "!/", handler); new StaticResourceJars().getUrlsFrom(url); assertThatIllegalStateException() .isThrownBy(() -> ((JarURLConnection) handler.getConnection()).getJarFile().getComment()) From 9cdd0c3776d09587f93baf6261c621eae60faa09 Mon Sep 17 00:00:00 2001 From: Tobias Lippert Date: Sun, 21 Jan 2024 18:41:22 +0100 Subject: [PATCH 1112/1215] Remove unnecessary semicolons See gh-39259 --- .../actuate/autoconfigure/info/InfoContributorFallback.java | 2 +- .../metrics/export/signalfx/SignalFxProperties.java | 2 +- .../boot/actuate/autoconfigure/tracing/TracingProperties.java | 2 +- .../boot/autoconfigure/cassandra/CassandraProperties.java | 2 +- .../boot/autoconfigure/jackson/JacksonProperties.java | 2 +- .../org/springframework/boot/test/context/SpringBootTest.java | 2 +- .../org/springframework/boot/context/config/ConfigData.java | 2 +- .../boot/context/config/ConfigDataEnvironmentContributor.java | 2 +- .../boot/context/config/ConfigDataEnvironmentContributors.java | 2 +- .../boot/context/properties/bind/BindMethod.java | 2 +- .../springframework/boot/web/server/GracefulShutdownResult.java | 2 +- .../main/java/org/springframework/boot/web/server/Shutdown.java | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/info/InfoContributorFallback.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/info/InfoContributorFallback.java index 0f78db5cbe44..b93e148b4d73 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/info/InfoContributorFallback.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/info/InfoContributorFallback.java @@ -37,6 +37,6 @@ public enum InfoContributorFallback { /** * Do not fall back, thereby disabling the info contributor. */ - DISABLE; + DISABLE } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/signalfx/SignalFxProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/signalfx/SignalFxProperties.java index 3a5735537a3f..d1afe6fd8a9c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/signalfx/SignalFxProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/signalfx/SignalFxProperties.java @@ -116,7 +116,7 @@ public enum HistogramType { /** * Delta histogram. */ - DELTA; + DELTA } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/TracingProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/TracingProperties.java index ed1514872b41..175ff03a8175 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/TracingProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/TracingProperties.java @@ -268,7 +268,7 @@ public enum PropagationType { * B3 * multiple headers propagation. */ - B3_MULTI; + B3_MULTI } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java index bd25ed980f89..dcc1b07a1c7f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java @@ -482,7 +482,7 @@ public enum Compression { /** * No compression. */ - NONE; + NONE } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java index fe3a67e028a2..31e3da6200f0 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java @@ -224,7 +224,7 @@ public enum ConstructorDetectorStrategy { * Refuse to decide implicit mode and instead throw an InvalidDefinitionException * for ambiguous cases. */ - EXPLICIT_ONLY; + EXPLICIT_ONLY } diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTest.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTest.java index 95a053363f41..7a5cffa0387c 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTest.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTest.java @@ -211,7 +211,7 @@ enum UseMainMethod { * that class does not have a main method, a test-specific * {@link SpringApplication} will be used. */ - WHEN_AVAILABLE; + WHEN_AVAILABLE } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigData.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigData.java index a4a0cfad1d2b..1c68b25c4306 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigData.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigData.java @@ -281,7 +281,7 @@ public enum Option { * profile specific sibling imports. * @since 2.4.5 */ - PROFILE_SPECIFIC; + PROFILE_SPECIFIC } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributor.java index 4311c48e051c..d5ffe3083447 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributor.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributor.java @@ -470,7 +470,7 @@ enum Kind { /** * A valid location that contained nothing to load. */ - EMPTY_LOCATION; + EMPTY_LOCATION } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributors.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributors.java index b9c9b0ca2915..1e399785946f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributors.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributors.java @@ -326,7 +326,7 @@ enum BinderOption { /** * Throw an exception if an inactive contributor contains a bound value. */ - FAIL_ON_BIND_TO_INACTIVE_SOURCE; + FAIL_ON_BIND_TO_INACTIVE_SOURCE } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindMethod.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindMethod.java index 7039ca43ec72..fe3664449cb5 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindMethod.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindMethod.java @@ -32,6 +32,6 @@ public enum BindMethod { /** * Value object using constructor binding. */ - VALUE_OBJECT; + VALUE_OBJECT } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/GracefulShutdownResult.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/GracefulShutdownResult.java index c1a94d106e01..4cc7eaa1f668 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/GracefulShutdownResult.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/GracefulShutdownResult.java @@ -39,6 +39,6 @@ public enum GracefulShutdownResult { /** * The server was shutdown immediately, ignoring any active requests. */ - IMMEDIATE; + IMMEDIATE } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Shutdown.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Shutdown.java index c09fab575081..739234260d62 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Shutdown.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Shutdown.java @@ -33,6 +33,6 @@ public enum Shutdown { /** * The {@link WebServer} should shut down immediately. */ - IMMEDIATE; + IMMEDIATE } From def75233981181cdac0aa86d4da7be17589d315d Mon Sep 17 00:00:00 2001 From: Tobias Lippert Date: Sun, 21 Jan 2024 18:42:27 +0100 Subject: [PATCH 1113/1215] Inline redundant if statements See gh-39259 --- .../org/springframework/boot/build/bom/BomPlugin.java | 4 +--- .../bom/bomr/version/AbstractDependencyVersion.java | 5 +---- .../bomr/version/ReleaseTrainDependencyVersion.java | 5 +---- .../CheckClasspathForProhibitedDependencies.java | 5 +---- .../CloudFoundryWebEndpointDiscoverer.java | 8 +++----- .../security/reactive/EndpointRequest.java | 4 +--- .../web/servlet/WebDriverContextCustomizer.java | 5 +---- .../mock/mockito/ResetMocksTestExecutionListener.java | 4 +--- .../boot/cli/command/core/HelpCommand.java | 5 +---- .../boot/configurationprocessor/metadata/Metadata.java | 10 ++-------- .../context/properties/migrator/PropertyMigration.java | 7 ++----- ...urationPropertiesCharSequenceToObjectConverter.java | 9 +++------ .../web/servlet/ServletContextInitializerBeans.java | 5 +---- ...beddedServerContainerInvocationContextProvider.java | 5 +---- .../liquibase/SampleLiquibaseApplicationTests.java | 4 +--- .../java/smoketest/websocket/jetty/snake/Location.java | 5 +---- .../smoketest/websocket/tomcat/snake/Location.java | 5 +---- .../smoketest/websocket/undertow/snake/Location.java | 5 +---- 18 files changed, 24 insertions(+), 76 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java index 6cbd5ae787b5..f4361dc3b7bd 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java @@ -291,9 +291,7 @@ private boolean isNodeWithName(Object candidate, String name) { if ((node.name() instanceof QName qname) && name.equals(qname.getLocalPart())) { return true; } - if (name.equals(node.name())) { - return true; - } + return name.equals(node.name()); } return false; } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/AbstractDependencyVersion.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/AbstractDependencyVersion.java index 4d17ceefc81f..a595718ac7e8 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/AbstractDependencyVersion.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/AbstractDependencyVersion.java @@ -58,10 +58,7 @@ public boolean equals(Object obj) { return false; } AbstractDependencyVersion other = (AbstractDependencyVersion) obj; - if (!this.comparableVersion.equals(other.comparableVersion)) { - return false; - } - return true; + return this.comparableVersion.equals(other.comparableVersion); } @Override diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/ReleaseTrainDependencyVersion.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/ReleaseTrainDependencyVersion.java index c9c79bcdc69b..12328ee323d2 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/ReleaseTrainDependencyVersion.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/ReleaseTrainDependencyVersion.java @@ -127,10 +127,7 @@ public boolean equals(Object obj) { return false; } ReleaseTrainDependencyVersion other = (ReleaseTrainDependencyVersion) obj; - if (!this.original.equals(other.original)) { - return false; - } - return true; + return this.original.equals(other.original); } @Override diff --git a/buildSrc/src/main/java/org/springframework/boot/build/classpath/CheckClasspathForProhibitedDependencies.java b/buildSrc/src/main/java/org/springframework/boot/build/classpath/CheckClasspathForProhibitedDependencies.java index 548344b731fd..dc976b2eefdb 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/classpath/CheckClasspathForProhibitedDependencies.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/classpath/CheckClasspathForProhibitedDependencies.java @@ -100,10 +100,7 @@ private boolean prohibited(ModuleVersionIdentifier id) { if (group.equals("org.apache.geronimo.specs")) { return true; } - if (group.equals("com.sun.activation")) { - return true; - } - return false; + return group.equals("com.sun.activation"); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscoverer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscoverer.java index 173bcbe9e951..5bb1638333b5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscoverer.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscoverer.java @@ -65,11 +65,9 @@ public CloudFoundryWebEndpointDiscoverer(ApplicationContext applicationContext, @Override protected boolean isExtensionTypeExposed(Class extensionBeanType) { - if (isHealthEndpointExtension(extensionBeanType) && !isCloudFoundryHealthEndpointExtension(extensionBeanType)) { - // Filter regular health endpoint extensions so a CF version can replace them - return false; - } - return true; + // Filter regular health endpoint extensions so a CF version can replace them + return !isHealthEndpointExtension(extensionBeanType) + || isCloudFoundryHealthEndpointExtension(extensionBeanType); } private boolean isHealthEndpointExtension(Class extensionBeanType) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequest.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequest.java index 050613f1fc75..4efc65d05c05 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequest.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequest.java @@ -136,9 +136,7 @@ protected boolean ignoreApplicationContext(ApplicationContext applicationContext return true; } String managementContextId = applicationContext.getParent().getId() + ":management"; - if (!managementContextId.equals(applicationContext.getId())) { - return true; - } + return !managementContextId.equals(applicationContext.getId()); } return false; } diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverContextCustomizer.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverContextCustomizer.java index 5a7420207dec..d79d79d4b806 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverContextCustomizer.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverContextCustomizer.java @@ -39,10 +39,7 @@ public boolean equals(Object obj) { if (obj == this) { return true; } - if (obj == null || obj.getClass() != getClass()) { - return false; - } - return true; + return obj != null && obj.getClass() == getClass(); } @Override diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/ResetMocksTestExecutionListener.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/ResetMocksTestExecutionListener.java index b353cd41c6cf..1c2957d72a30 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/ResetMocksTestExecutionListener.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/ResetMocksTestExecutionListener.java @@ -119,9 +119,7 @@ private boolean isStandardBeanOrSingletonFactoryBean(ConfigurableListableBeanFac String factoryBeanName = BeanFactory.FACTORY_BEAN_PREFIX + name; if (beanFactory.containsBean(factoryBeanName)) { FactoryBean factoryBean = (FactoryBean) beanFactory.getBean(factoryBeanName); - if (!factoryBean.isSingleton()) { - return false; - } + return factoryBean.isSingleton(); } return true; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/core/HelpCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/core/HelpCommand.java index 63291a6b7b92..4f4b513b6ffd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/core/HelpCommand.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/core/HelpCommand.java @@ -81,10 +81,7 @@ public String getUsageHelp() { } private boolean isHelpShown(Command command) { - if (command instanceof HelpCommand || command instanceof HintCommand) { - return false; - } - return true; + return !(command instanceof HelpCommand) && !(command instanceof HintCommand); } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/Metadata.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/Metadata.java index 0930084cf4cc..d65b8dd4ec41 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/Metadata.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/Metadata.java @@ -157,10 +157,7 @@ public boolean matches(ConfigurationMetadata value) { if (this.deprecation == null && itemMetadata.getDeprecation() != null) { return false; } - if (this.deprecation != null && !this.deprecation.equals(itemMetadata.getDeprecation())) { - return false; - } - return true; + return this.deprecation == null || this.deprecation.equals(itemMetadata.getDeprecation()); } public MetadataItemCondition ofType(Class dataType) { @@ -348,10 +345,7 @@ public boolean matches(ItemHint value) { if (this.value != null && !this.value.equals(valueHint.getValue())) { return false; } - if (this.description != null && !this.description.equals(valueHint.getDescription())) { - return false; - } - return true; + return this.description == null || this.description.equals(valueHint.getDescription()); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertyMigration.java b/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertyMigration.java index 936467946980..24f9410052db 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertyMigration.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertyMigration.java @@ -85,11 +85,8 @@ private static boolean determineCompatibleType(ConfigurationMetadataProperty met if (replacementType.equals(currentType)) { return true; } - if (replacementType.equals(Duration.class.getName()) - && (currentType.equals(Long.class.getName()) || currentType.equals(Integer.class.getName()))) { - return true; - } - return false; + return replacementType.equals(Duration.class.getName()) + && (currentType.equals(Long.class.getName()) || currentType.equals(Integer.class.getName())); } private static String determineType(ConfigurationMetadataProperty metadata) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesCharSequenceToObjectConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesCharSequenceToObjectConverter.java index c97e882102dd..c89ce7a3a665 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesCharSequenceToObjectConverter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesCharSequenceToObjectConverter.java @@ -90,12 +90,9 @@ private boolean isStringConversionBetter(TypeDescriptor sourceType, TypeDescript return true; } } - if ((targetType.isArray() || targetType.isCollection()) && !targetType.equals(BYTE_ARRAY)) { - // StringToArrayConverter / StringToCollectionConverter are better than - // ObjectToArrayConverter / ObjectToCollectionConverter - return true; - } - return false; + // StringToArrayConverter / StringToCollectionConverter are better than + // ObjectToArrayConverter / ObjectToCollectionConverter + return (targetType.isArray() || targetType.isCollection()) && !targetType.equals(BYTE_ARRAY); } @Override diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletContextInitializerBeans.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletContextInitializerBeans.java index e771b5ce26d2..07a5933fafc2 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletContextInitializerBeans.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/ServletContextInitializerBeans.java @@ -332,10 +332,7 @@ boolean contains(Class type, Object object) { && this.seen.getOrDefault(type, Collections.emptySet()).contains(object)) { return true; } - if (this.seen.getOrDefault(ServletContextInitializer.class, Collections.emptySet()).contains(object)) { - return true; - } - return false; + return this.seen.getOrDefault(ServletContextInitializer.class, Collections.emptySet()).contains(object); } static Seen empty() { diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/intTest/java/org/springframework/boot/context/embedded/EmbeddedServerContainerInvocationContextProvider.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/intTest/java/org/springframework/boot/context/embedded/EmbeddedServerContainerInvocationContextProvider.java index 010f9e07f916..135b677d42dc 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/intTest/java/org/springframework/boot/context/embedded/EmbeddedServerContainerInvocationContextProvider.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/intTest/java/org/springframework/boot/context/embedded/EmbeddedServerContainerInvocationContextProvider.java @@ -155,10 +155,7 @@ public boolean supportsParameter(ParameterContext parameterContext, ExtensionCon if (parameterContext.getParameter().getType().equals(AbstractApplicationLauncher.class)) { return true; } - if (parameterContext.getParameter().getType().equals(RestTemplate.class)) { - return true; - } - return false; + return parameterContext.getParameter().getType().equals(RestTemplate.class); } @Override diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-liquibase/src/test/java/smoketest/liquibase/SampleLiquibaseApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-liquibase/src/test/java/smoketest/liquibase/SampleLiquibaseApplicationTests.java index abcf860f8428..b0df8bb338c2 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-liquibase/src/test/java/smoketest/liquibase/SampleLiquibaseApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-liquibase/src/test/java/smoketest/liquibase/SampleLiquibaseApplicationTests.java @@ -69,9 +69,7 @@ private boolean serverNotRunning(IllegalStateException ex) { }; if (nested.contains(ConnectException.class)) { Throwable root = nested.getRootCause(); - if (root.getMessage().contains("Connection refused")) { - return true; - } + return root.getMessage().contains("Connection refused"); } return false; } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/Location.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/Location.java index b7b02a7a1f78..02390e3a1107 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/Location.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/Location.java @@ -55,10 +55,7 @@ public boolean equals(Object o) { if (this.x != location.x) { return false; } - if (this.y != location.y) { - return false; - } - return true; + return this.y == location.y; } @Override diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/Location.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/Location.java index abbdc68232eb..21550e2ba8c1 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/Location.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/Location.java @@ -55,10 +55,7 @@ public boolean equals(Object o) { if (this.x != location.x) { return false; } - if (this.y != location.y) { - return false; - } - return true; + return this.y == location.y; } @Override diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/Location.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/Location.java index d658ba1b6b40..16050be98e79 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/Location.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/Location.java @@ -55,10 +55,7 @@ public boolean equals(Object o) { if (this.x != location.x) { return false; } - if (this.y != location.y) { - return false; - } - return true; + return this.y == location.y; } @Override From 65a1ff84e6b5ff63a734f1d0abc618137da9d95c Mon Sep 17 00:00:00 2001 From: Tobias Lippert Date: Sun, 21 Jan 2024 18:44:32 +0100 Subject: [PATCH 1114/1215] Simplify conditionals See gh-39259 --- .../endpoint/invoke/reflect/OperationMethodParameter.java | 2 +- .../boot/devtools/restart/classloader/ClassLoaderFile.java | 2 +- .../OverrideAutoConfigurationContextCustomizerFactory.java | 2 +- .../boot/loader/net/protocol/jar/JarUrlConnection.java | 6 +++--- .../NotConstructorBoundInjectionFailureAnalyzer.java | 4 ++-- .../properties/bind/DefaultBindConstructorProvider.java | 2 +- .../boot/logging/log4j2/SpringProfileArbiter.java | 2 +- .../boot/web/embedded/tomcat/NestedJarResourceSet.java | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameter.java index a4fa71ebe628..fdda353e3145 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameter.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameter.java @@ -68,7 +68,7 @@ public boolean isMandatory() { if (!ObjectUtils.isEmpty(this.parameter.getAnnotationsByType(Nullable.class))) { return false; } - return (jsr305Present) ? new Jsr305().isMandatory(this.parameter) : true; + return !jsr305Present || new Jsr305().isMandatory(this.parameter); } @Override diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/classloader/ClassLoaderFile.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/classloader/ClassLoaderFile.java index d39c569e5ebd..3073b528f472 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/classloader/ClassLoaderFile.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/classloader/ClassLoaderFile.java @@ -55,7 +55,7 @@ public ClassLoaderFile(Kind kind, byte[] contents) { */ public ClassLoaderFile(Kind kind, long lastModified, byte[] contents) { Assert.notNull(kind, "Kind must not be null"); - Assert.isTrue((kind != Kind.DELETED) ? contents != null : contents == null, + Assert.isTrue((kind != Kind.DELETED) == (contents != null), () -> "Contents must " + ((kind != Kind.DELETED) ? "not " : "") + "be null"); this.kind = kind; this.lastModified = lastModified; diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/OverrideAutoConfigurationContextCustomizerFactory.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/OverrideAutoConfigurationContextCustomizerFactory.java index 664e1e035d95..06817411d5a3 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/OverrideAutoConfigurationContextCustomizerFactory.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/OverrideAutoConfigurationContextCustomizerFactory.java @@ -44,7 +44,7 @@ public ContextCustomizer createContextCustomizer(Class testClass, } OverrideAutoConfiguration overrideAutoConfiguration = TestContextAnnotationUtils.findMergedAnnotation(testClass, OverrideAutoConfiguration.class); - boolean enabled = (overrideAutoConfiguration != null) ? overrideAutoConfiguration.enabled() : true; + boolean enabled = overrideAutoConfiguration == null || overrideAutoConfiguration.enabled(); return !enabled ? new DisableAutoConfigurationContextCustomizer() : null; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java index c39bd37a44fd..2177dae7f160 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java @@ -207,7 +207,7 @@ public InputStream getInputStream() throws IOException { @Override public boolean getAllowUserInteraction() { - return (this.jarFileConnection != null) ? this.jarFileConnection.getAllowUserInteraction() : false; + return this.jarFileConnection != null && this.jarFileConnection.getAllowUserInteraction(); } @Override @@ -219,7 +219,7 @@ public void setAllowUserInteraction(boolean allowuserinteraction) { @Override public boolean getUseCaches() { - return (this.jarFileConnection != null) ? this.jarFileConnection.getUseCaches() : true; + return this.jarFileConnection == null || this.jarFileConnection.getUseCaches(); } @Override @@ -231,7 +231,7 @@ public void setUseCaches(boolean usecaches) { @Override public boolean getDefaultUseCaches() { - return (this.jarFileConnection != null) ? this.jarFileConnection.getDefaultUseCaches() : true; + return this.jarFileConnection == null || this.jarFileConnection.getDefaultUseCaches(); } @Override diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NotConstructorBoundInjectionFailureAnalyzer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NotConstructorBoundInjectionFailureAnalyzer.java index 228aa5ed48cc..548eb266bae1 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NotConstructorBoundInjectionFailureAnalyzer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NotConstructorBoundInjectionFailureAnalyzer.java @@ -61,8 +61,8 @@ protected FailureAnalysis analyze(Throwable rootFailure, NoSuchBeanDefinitionExc } private boolean isConstructorBindingConfigurationProperties(InjectionPoint injectionPoint) { - return (injectionPoint != null && injectionPoint.getMember() instanceof Constructor constructor) - ? isConstructorBindingConfigurationProperties(constructor) : false; + return injectionPoint != null && injectionPoint.getMember() instanceof Constructor constructor + && isConstructorBindingConfigurationProperties(constructor); } private boolean isConstructorBindingConfigurationProperties(Constructor constructor) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProvider.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProvider.java index 609b8ebccc3d..a81d38cfd638 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProvider.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProvider.java @@ -127,7 +127,7 @@ private static boolean isAutowiredPresent(Class type) { return true; } Class userClass = ClassUtils.getUserClass(type); - return (userClass != type) ? isAutowiredPresent(userClass) : false; + return userClass != type && isAutowiredPresent(userClass); } private static Constructor[] getCandidateConstructors(Class type) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringProfileArbiter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringProfileArbiter.java index 6aa7f5421af3..461f529f0f87 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringProfileArbiter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringProfileArbiter.java @@ -53,7 +53,7 @@ private SpringProfileArbiter(Environment environment, String[] profiles) { @Override public boolean isCondition() { - return (this.environment != null) ? this.environment.acceptsProfiles(this.profiles) : false; + return this.environment != null && this.environment.acceptsProfiles(this.profiles); } @PluginBuilderFactory diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/NestedJarResourceSet.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/NestedJarResourceSet.java index 0f1be9560a92..77cee0c6a6e1 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/NestedJarResourceSet.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/NestedJarResourceSet.java @@ -124,7 +124,7 @@ protected boolean isMultiRelease() { // JarFile.isMultiRelease() is final so we must go to the manifest Manifest manifest = getManifest(); Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null; - this.multiRelease = (attributes != null) ? attributes.containsKey(MULTI_RELEASE) : false; + this.multiRelease = attributes != null && attributes.containsKey(MULTI_RELEASE); } } } From ddb769bf7f144fa365c46533ca58c12bbab94042 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 23 Jan 2024 08:54:52 -0800 Subject: [PATCH 1115/1215] Polish 'Simplify conditionals' See gh-39259 --- .../endpoint/invoke/reflect/OperationMethodParameter.java | 5 ++++- .../devtools/restart/classloader/ClassLoaderFile.java | 8 ++++++-- ...OverrideAutoConfigurationContextCustomizerFactory.java | 2 +- .../boot/loader/net/protocol/jar/JarUrlConnection.java | 6 +++--- .../properties/bind/DefaultBindConstructorProvider.java | 2 +- .../boot/logging/log4j2/SpringProfileArbiter.java | 2 +- .../boot/web/embedded/tomcat/NestedJarResourceSet.java | 2 +- 7 files changed, 17 insertions(+), 10 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameter.java index fdda353e3145..6b3e038859b0 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameter.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/reflect/OperationMethodParameter.java @@ -68,7 +68,10 @@ public boolean isMandatory() { if (!ObjectUtils.isEmpty(this.parameter.getAnnotationsByType(Nullable.class))) { return false; } - return !jsr305Present || new Jsr305().isMandatory(this.parameter); + if (jsr305Present) { + return new Jsr305().isMandatory(this.parameter); + } + return true; } @Override diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/classloader/ClassLoaderFile.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/classloader/ClassLoaderFile.java index 3073b528f472..9972ee764604 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/classloader/ClassLoaderFile.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/classloader/ClassLoaderFile.java @@ -55,8 +55,12 @@ public ClassLoaderFile(Kind kind, byte[] contents) { */ public ClassLoaderFile(Kind kind, long lastModified, byte[] contents) { Assert.notNull(kind, "Kind must not be null"); - Assert.isTrue((kind != Kind.DELETED) == (contents != null), - () -> "Contents must " + ((kind != Kind.DELETED) ? "not " : "") + "be null"); + if (kind == Kind.DELETED) { + Assert.isTrue(contents == null, "Contents must be null"); + } + else { + Assert.isTrue(contents != null, "Contents must not be null"); + } this.kind = kind; this.lastModified = lastModified; this.contents = contents; diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/OverrideAutoConfigurationContextCustomizerFactory.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/OverrideAutoConfigurationContextCustomizerFactory.java index 06817411d5a3..5b7ab2976b28 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/OverrideAutoConfigurationContextCustomizerFactory.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/OverrideAutoConfigurationContextCustomizerFactory.java @@ -44,7 +44,7 @@ public ContextCustomizer createContextCustomizer(Class testClass, } OverrideAutoConfiguration overrideAutoConfiguration = TestContextAnnotationUtils.findMergedAnnotation(testClass, OverrideAutoConfiguration.class); - boolean enabled = overrideAutoConfiguration == null || overrideAutoConfiguration.enabled(); + boolean enabled = (overrideAutoConfiguration == null) || overrideAutoConfiguration.enabled(); return !enabled ? new DisableAutoConfigurationContextCustomizer() : null; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java index 2177dae7f160..edc18cd44eb1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/jar/JarUrlConnection.java @@ -207,7 +207,7 @@ public InputStream getInputStream() throws IOException { @Override public boolean getAllowUserInteraction() { - return this.jarFileConnection != null && this.jarFileConnection.getAllowUserInteraction(); + return (this.jarFileConnection != null) && this.jarFileConnection.getAllowUserInteraction(); } @Override @@ -219,7 +219,7 @@ public void setAllowUserInteraction(boolean allowuserinteraction) { @Override public boolean getUseCaches() { - return this.jarFileConnection == null || this.jarFileConnection.getUseCaches(); + return (this.jarFileConnection == null) || this.jarFileConnection.getUseCaches(); } @Override @@ -231,7 +231,7 @@ public void setUseCaches(boolean usecaches) { @Override public boolean getDefaultUseCaches() { - return this.jarFileConnection == null || this.jarFileConnection.getDefaultUseCaches(); + return (this.jarFileConnection == null) || this.jarFileConnection.getDefaultUseCaches(); } @Override diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProvider.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProvider.java index a81d38cfd638..33da909ac8a2 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProvider.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProvider.java @@ -127,7 +127,7 @@ private static boolean isAutowiredPresent(Class type) { return true; } Class userClass = ClassUtils.getUserClass(type); - return userClass != type && isAutowiredPresent(userClass); + return (userClass != type) && isAutowiredPresent(userClass); } private static Constructor[] getCandidateConstructors(Class type) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringProfileArbiter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringProfileArbiter.java index 461f529f0f87..65dcd3e2d535 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringProfileArbiter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringProfileArbiter.java @@ -53,7 +53,7 @@ private SpringProfileArbiter(Environment environment, String[] profiles) { @Override public boolean isCondition() { - return this.environment != null && this.environment.acceptsProfiles(this.profiles); + return (this.environment != null) && this.environment.acceptsProfiles(this.profiles); } @PluginBuilderFactory diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/NestedJarResourceSet.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/NestedJarResourceSet.java index 77cee0c6a6e1..a0a003d2acab 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/NestedJarResourceSet.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/NestedJarResourceSet.java @@ -124,7 +124,7 @@ protected boolean isMultiRelease() { // JarFile.isMultiRelease() is final so we must go to the manifest Manifest manifest = getManifest(); Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null; - this.multiRelease = attributes != null && attributes.containsKey(MULTI_RELEASE); + this.multiRelease = (attributes != null) && attributes.containsKey(MULTI_RELEASE); } } } From fe38cb3b4a4cff997c7e70b4063b51da0f2a019d Mon Sep 17 00:00:00 2001 From: Tobias Lippert Date: Sun, 21 Jan 2024 18:45:31 +0100 Subject: [PATCH 1116/1215] Use string.repeat() See gh-39259 --- .../org/springframework/boot/loader/zip/ZipStringTests.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipStringTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipStringTests.java index 0716d65aac97..0bcdc5f956b4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipStringTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipStringTests.java @@ -168,9 +168,7 @@ void startsWithWhenDoesNotStartWith() throws Exception { @Test void zipStringWhenMultiCodePointAtBufferBoundary() throws Exception { StringBuilder source = new StringBuilder(); - for (int i = 0; i < ZipString.BUFFER_SIZE - 1; i++) { - source.append("A"); - } + source.append("A".repeat(ZipString.BUFFER_SIZE - 1)); source.append("\u1EFF"); String charSequence = source.toString(); source.append("suffix"); From 06265ee617dbbfa3e3ebe2cec7233258bc3644f4 Mon Sep 17 00:00:00 2001 From: Tobias Lippert Date: Sun, 21 Jan 2024 18:47:43 +0100 Subject: [PATCH 1117/1215] Use pattern variables See gh-39259 --- .../bom/bomr/version/ReleaseTrainDependencyVersion.java | 6 ++---- .../properties/migrator/PropertiesMigrationReporter.java | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/ReleaseTrainDependencyVersion.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/ReleaseTrainDependencyVersion.java index 12328ee323d2..fc999a252674 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/ReleaseTrainDependencyVersion.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/ReleaseTrainDependencyVersion.java @@ -64,10 +64,9 @@ public int compareTo(DependencyVersion other) { @Override public boolean isUpgrade(DependencyVersion candidate, boolean movingToSnapshots) { - if (!(candidate instanceof ReleaseTrainDependencyVersion)) { + if (!(candidate instanceof ReleaseTrainDependencyVersion candidateReleaseTrain)) { return true; } - ReleaseTrainDependencyVersion candidateReleaseTrain = (ReleaseTrainDependencyVersion) candidate; int comparison = this.releaseTrain.compareTo(candidateReleaseTrain.releaseTrain); if (comparison != 0) { return comparison < 0; @@ -88,10 +87,9 @@ private boolean isSnapshot() { @Override public boolean isSnapshotFor(DependencyVersion candidate) { - if (!isSnapshot() || !(candidate instanceof ReleaseTrainDependencyVersion)) { + if (!isSnapshot() || !(candidate instanceof ReleaseTrainDependencyVersion candidateReleaseTrain)) { return false; } - ReleaseTrainDependencyVersion candidateReleaseTrain = (ReleaseTrainDependencyVersion) candidate; return this.releaseTrain.equals(candidateReleaseTrain.releaseTrain); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReporter.java b/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReporter.java index 85603a7bb0c5..9872c6ecd36c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReporter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReporter.java @@ -127,8 +127,7 @@ private Map> getMatchingProperties( new PropertyMigration(match, metadata, determineReplacementMetadata(metadata), false)); } // Prefix match for maps - if (isMapType(metadata) && propertySource instanceof IterableConfigurationPropertySource) { - IterableConfigurationPropertySource iterableSource = (IterableConfigurationPropertySource) propertySource; + if (isMapType(metadata) && propertySource instanceof IterableConfigurationPropertySource iterableSource) { iterableSource.stream() .filter(metadataName::isAncestorOf) .map(propertySource::getConfigurationProperty) From e40f49d173fedc1f3309da0075ab6a5a64b84dfc Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 23 Jan 2024 09:01:43 -0800 Subject: [PATCH 1118/1215] Polish 'Use pattern variables' See gh-39259 --- .../version/ReleaseTrainDependencyVersion.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/ReleaseTrainDependencyVersion.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/ReleaseTrainDependencyVersion.java index fc999a252674..6c1e5f3d1715 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/ReleaseTrainDependencyVersion.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/ReleaseTrainDependencyVersion.java @@ -64,21 +64,25 @@ public int compareTo(DependencyVersion other) { @Override public boolean isUpgrade(DependencyVersion candidate, boolean movingToSnapshots) { - if (!(candidate instanceof ReleaseTrainDependencyVersion candidateReleaseTrain)) { - return true; + if (candidate instanceof ReleaseTrainDependencyVersion candidateReleaseTrain) { + return isUpgrade(candidateReleaseTrain, movingToSnapshots); } - int comparison = this.releaseTrain.compareTo(candidateReleaseTrain.releaseTrain); + return true; + } + + private boolean isUpgrade(ReleaseTrainDependencyVersion candidate, boolean movingToSnapshots) { + int comparison = this.releaseTrain.compareTo(candidate.releaseTrain); if (comparison != 0) { return comparison < 0; } - if (movingToSnapshots && !isSnapshot() && candidateReleaseTrain.isSnapshot()) { + if (movingToSnapshots && !isSnapshot() && candidate.isSnapshot()) { return true; } - comparison = this.type.compareTo(candidateReleaseTrain.type); + comparison = this.type.compareTo(candidate.type); if (comparison != 0) { return comparison < 0; } - return Integer.compare(this.version, candidateReleaseTrain.version) < 0; + return Integer.compare(this.version, candidate.version) < 0; } private boolean isSnapshot() { From 74a7fbea9d8d1633e7ff4b39d994f27039a697f7 Mon Sep 17 00:00:00 2001 From: Tobias Lippert Date: Sun, 21 Jan 2024 18:49:51 +0100 Subject: [PATCH 1119/1215] Remove redundant boxing See gh-39259 --- .../main/java/smoketest/websocket/jetty/snake/Snake.java | 6 +++--- .../java/smoketest/websocket/jetty/snake/SnakeTimer.java | 4 ++-- .../websocket/jetty/snake/SnakeWebSocketHandler.java | 4 ++-- .../main/java/smoketest/websocket/tomcat/snake/Snake.java | 6 +++--- .../java/smoketest/websocket/tomcat/snake/SnakeTimer.java | 4 ++-- .../websocket/tomcat/snake/SnakeWebSocketHandler.java | 4 ++-- .../main/java/smoketest/websocket/undertow/snake/Snake.java | 6 +++--- .../java/smoketest/websocket/undertow/snake/SnakeTimer.java | 4 ++-- .../websocket/undertow/snake/SnakeWebSocketHandler.java | 4 ++-- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/Snake.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/Snake.java index 0206f06e94ee..82372c4d2468 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/Snake.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/Snake.java @@ -136,12 +136,12 @@ public void setDirection(Direction direction) { public String getLocationsJson() { synchronized (this.monitor) { StringBuilder sb = new StringBuilder(); - sb.append(String.format("{x: %d, y: %d}", Integer.valueOf(this.head.x), Integer.valueOf(this.head.y))); + sb.append(String.format("{x: %d, y: %d}", this.head.x, this.head.y)); for (Location location : this.tail) { sb.append(','); - sb.append(String.format("{x: %d, y: %d}", Integer.valueOf(location.x), Integer.valueOf(location.y))); + sb.append(String.format("{x: %d, y: %d}", location.x, location.y)); } - return String.format("{'id':%d,'body':[%s]}", Integer.valueOf(this.id), sb); + return String.format("{'id':%d,'body':[%s]}", this.id, sb); } } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/SnakeTimer.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/SnakeTimer.java index 8a50a492cefd..f7a8871e3735 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/SnakeTimer.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/SnakeTimer.java @@ -50,7 +50,7 @@ public static void addSnake(Snake snake) { if (snakes.isEmpty()) { startTimer(); } - snakes.put(Integer.valueOf(snake.getId()), snake); + snakes.put(snake.getId(), snake); } } @@ -60,7 +60,7 @@ public static Collection getSnakes() { public static void removeSnake(Snake snake) { synchronized (MONITOR) { - snakes.remove(Integer.valueOf(snake.getId())); + snakes.remove(snake.getId()); if (snakes.isEmpty()) { stopTimer(); } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/SnakeWebSocketHandler.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/SnakeWebSocketHandler.java index dc5ba84e841f..515e71b13338 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/SnakeWebSocketHandler.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/SnakeWebSocketHandler.java @@ -69,7 +69,7 @@ public void afterConnectionEstablished(WebSocketSession session) throws Exceptio StringBuilder sb = new StringBuilder(); for (Iterator iterator = SnakeTimer.getSnakes().iterator(); iterator.hasNext();) { Snake snake = iterator.next(); - sb.append(String.format("{id: %d, color: '%s'}", Integer.valueOf(snake.getId()), snake.getHexColor())); + sb.append(String.format("{id: %d, color: '%s'}", snake.getId(), snake.getHexColor())); if (iterator.hasNext()) { sb.append(','); } @@ -97,7 +97,7 @@ else if ("south".equals(payload)) { @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { SnakeTimer.removeSnake(this.snake); - SnakeTimer.broadcast(String.format("{'type': 'leave', 'id': %d}", Integer.valueOf(this.id))); + SnakeTimer.broadcast(String.format("{'type': 'leave', 'id': %d}", this.id)); } } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/Snake.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/Snake.java index e69ff6918d90..dbedf531683e 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/Snake.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/Snake.java @@ -136,12 +136,12 @@ public void setDirection(Direction direction) { public String getLocationsJson() { synchronized (this.monitor) { StringBuilder sb = new StringBuilder(); - sb.append(String.format("{x: %d, y: %d}", Integer.valueOf(this.head.x), Integer.valueOf(this.head.y))); + sb.append(String.format("{x: %d, y: %d}", this.head.x, this.head.y)); for (Location location : this.tail) { sb.append(','); - sb.append(String.format("{x: %d, y: %d}", Integer.valueOf(location.x), Integer.valueOf(location.y))); + sb.append(String.format("{x: %d, y: %d}", location.x, location.y)); } - return String.format("{'id':%d,'body':[%s]}", Integer.valueOf(this.id), sb); + return String.format("{'id':%d,'body':[%s]}", this.id, sb); } } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/SnakeTimer.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/SnakeTimer.java index 0d95ec921de8..c50bb9fc292b 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/SnakeTimer.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/SnakeTimer.java @@ -50,7 +50,7 @@ public static void addSnake(Snake snake) { if (snakes.isEmpty()) { startTimer(); } - snakes.put(Integer.valueOf(snake.getId()), snake); + snakes.put(snake.getId(), snake); } } @@ -60,7 +60,7 @@ public static Collection getSnakes() { public static void removeSnake(Snake snake) { synchronized (MONITOR) { - snakes.remove(Integer.valueOf(snake.getId())); + snakes.remove(snake.getId()); if (snakes.isEmpty()) { stopTimer(); } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/SnakeWebSocketHandler.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/SnakeWebSocketHandler.java index e69a489bc3ab..a5dd6a1aabe9 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/SnakeWebSocketHandler.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/SnakeWebSocketHandler.java @@ -69,7 +69,7 @@ public void afterConnectionEstablished(WebSocketSession session) throws Exceptio StringBuilder sb = new StringBuilder(); for (Iterator iterator = SnakeTimer.getSnakes().iterator(); iterator.hasNext();) { Snake snake = iterator.next(); - sb.append(String.format("{id: %d, color: '%s'}", Integer.valueOf(snake.getId()), snake.getHexColor())); + sb.append(String.format("{id: %d, color: '%s'}", snake.getId(), snake.getHexColor())); if (iterator.hasNext()) { sb.append(','); } @@ -97,7 +97,7 @@ else if ("south".equals(payload)) { @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { SnakeTimer.removeSnake(this.snake); - SnakeTimer.broadcast(String.format("{'type': 'leave', 'id': %d}", Integer.valueOf(this.id))); + SnakeTimer.broadcast(String.format("{'type': 'leave', 'id': %d}", this.id)); } } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/Snake.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/Snake.java index a9718ead230d..55eb35d38dca 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/Snake.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/Snake.java @@ -136,12 +136,12 @@ public void setDirection(Direction direction) { public String getLocationsJson() { synchronized (this.monitor) { StringBuilder sb = new StringBuilder(); - sb.append(String.format("{x: %d, y: %d}", Integer.valueOf(this.head.x), Integer.valueOf(this.head.y))); + sb.append(String.format("{x: %d, y: %d}", this.head.x, this.head.y)); for (Location location : this.tail) { sb.append(','); - sb.append(String.format("{x: %d, y: %d}", Integer.valueOf(location.x), Integer.valueOf(location.y))); + sb.append(String.format("{x: %d, y: %d}", location.x, location.y)); } - return String.format("{'id':%d,'body':[%s]}", Integer.valueOf(this.id), sb); + return String.format("{'id':%d,'body':[%s]}", this.id, sb); } } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/SnakeTimer.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/SnakeTimer.java index bb6314e0da48..520c485329cc 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/SnakeTimer.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/SnakeTimer.java @@ -50,7 +50,7 @@ public static void addSnake(Snake snake) { if (snakes.isEmpty()) { startTimer(); } - snakes.put(Integer.valueOf(snake.getId()), snake); + snakes.put(snake.getId(), snake); } } @@ -60,7 +60,7 @@ public static Collection getSnakes() { public static void removeSnake(Snake snake) { synchronized (MONITOR) { - snakes.remove(Integer.valueOf(snake.getId())); + snakes.remove(snake.getId()); if (snakes.isEmpty()) { stopTimer(); } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/SnakeWebSocketHandler.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/SnakeWebSocketHandler.java index da2a32658484..6d2553da595d 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/SnakeWebSocketHandler.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/SnakeWebSocketHandler.java @@ -69,7 +69,7 @@ public void afterConnectionEstablished(WebSocketSession session) throws Exceptio StringBuilder sb = new StringBuilder(); for (Iterator iterator = SnakeTimer.getSnakes().iterator(); iterator.hasNext();) { Snake snake = iterator.next(); - sb.append(String.format("{id: %d, color: '%s'}", Integer.valueOf(snake.getId()), snake.getHexColor())); + sb.append(String.format("{id: %d, color: '%s'}", snake.getId(), snake.getHexColor())); if (iterator.hasNext()) { sb.append(','); } @@ -97,7 +97,7 @@ else if ("south".equals(payload)) { @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { SnakeTimer.removeSnake(this.snake); - SnakeTimer.broadcast(String.format("{'type': 'leave', 'id': %d}", Integer.valueOf(this.id))); + SnakeTimer.broadcast(String.format("{'type': 'leave', 'id': %d}", this.id)); } } From ac5a08a49bc62ca079ced99bb97ad76116db38fb Mon Sep 17 00:00:00 2001 From: Tobias Lippert Date: Sun, 21 Jan 2024 18:51:26 +0100 Subject: [PATCH 1120/1215] Avoid redundant boxing See gh-39259 --- .../boot/actuate/autoconfigure/metrics/MeterValue.java | 2 +- ...nfigurationPropertiesCharSequenceToObjectConverterTests.java | 2 +- .../boot/convert/CharSequenceToObjectConverterTests.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterValue.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterValue.java index d475f8c5d685..3b00fd6be427 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterValue.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterValue.java @@ -90,7 +90,7 @@ public static MeterValue valueOf(String value) { if (duration != null) { return new MeterValue(duration); } - return new MeterValue(Double.valueOf(value)); + return new MeterValue(Double.parseDouble(value)); } /** diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesCharSequenceToObjectConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesCharSequenceToObjectConverterTests.java index 3a81830df698..3b9c12c26be1 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesCharSequenceToObjectConverterTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesCharSequenceToObjectConverterTests.java @@ -108,7 +108,7 @@ static class CharSequenceToLongConverter implements Converter Date: Sun, 21 Jan 2024 18:53:14 +0100 Subject: [PATCH 1121/1215] Replace explicit type with diamond operator See gh-39259 --- .../boot/gradle/plugin/ApplicationPluginAction.java | 2 +- .../boot/web/servlet/DynamicRegistrationBeanTests.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ApplicationPluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ApplicationPluginAction.java index a4df28854a7a..3ba91dda77af 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ApplicationPluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ApplicationPluginAction.java @@ -130,7 +130,7 @@ private void configureFilePermissions(CopySpec copySpec, int mode) { if (GradleVersion.current().compareTo(GradleVersion.version("8.3")) >= 0) { try { Method filePermissions = copySpec.getClass().getMethod("filePermissions", Action.class); - filePermissions.invoke(copySpec, new Action() { + filePermissions.invoke(copySpec, new Action<>() { @Override public void execute(Object filePermissions) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/DynamicRegistrationBeanTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/DynamicRegistrationBeanTests.java index 70d598c95ea8..37f91191e840 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/DynamicRegistrationBeanTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/DynamicRegistrationBeanTests.java @@ -50,7 +50,7 @@ void shouldUseConventionBasedNameIfNoNameOrBeanNameIsSet() { } private static DynamicRegistrationBean createBean() { - return new DynamicRegistrationBean() { + return new DynamicRegistrationBean<>() { @Override protected Dynamic addRegistration(String description, ServletContext servletContext) { return null; From 316b415e95fed05c20eb33879151f7eb7f8781a9 Mon Sep 17 00:00:00 2001 From: Tobias Lippert Date: Sun, 21 Jan 2024 18:54:01 +0100 Subject: [PATCH 1122/1215] Use try with resources instead of try-finally See gh-39259 --- .../springframework/boot/loader/jar/NestedJarFileTests.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java index 40a59c7baae3..bcbd8a47f581 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java @@ -413,7 +413,7 @@ void getCommentAlignsWithJdkJar() throws Exception { } private List collectComments(JarFile jarFile) throws IOException { - try { + try (jarFile) { List comments = new ArrayList<>(); Enumeration entries = jarFile.entries(); while (entries.hasMoreElements()) { @@ -424,9 +424,6 @@ private List collectComments(JarFile jarFile) throws IOException { } return comments; } - finally { - jarFile.close(); - } } } From 0613034e195ce55f684af11b3e4b71d62bf9b0bf Mon Sep 17 00:00:00 2001 From: Tobias Lippert Date: Sun, 21 Jan 2024 20:03:16 +0100 Subject: [PATCH 1123/1215] Replace multiple ifs with switch See gh-39259 --- .../jetty/snake/SnakeWebSocketHandler.java | 16 +++++----------- .../tomcat/snake/SnakeWebSocketHandler.java | 16 +++++----------- .../undertow/snake/SnakeWebSocketHandler.java | 16 +++++----------- 3 files changed, 15 insertions(+), 33 deletions(-) diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/SnakeWebSocketHandler.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/SnakeWebSocketHandler.java index 515e71b13338..e79eb2bdb4f2 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/SnakeWebSocketHandler.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/SnakeWebSocketHandler.java @@ -80,17 +80,11 @@ public void afterConnectionEstablished(WebSocketSession session) throws Exceptio @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String payload = message.getPayload(); - if ("west".equals(payload)) { - this.snake.setDirection(Direction.WEST); - } - else if ("north".equals(payload)) { - this.snake.setDirection(Direction.NORTH); - } - else if ("east".equals(payload)) { - this.snake.setDirection(Direction.EAST); - } - else if ("south".equals(payload)) { - this.snake.setDirection(Direction.SOUTH); + switch (payload) { + case "west" -> this.snake.setDirection(Direction.WEST); + case "north" -> this.snake.setDirection(Direction.NORTH); + case "east" -> this.snake.setDirection(Direction.EAST); + case "south" -> this.snake.setDirection(Direction.SOUTH); } } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/SnakeWebSocketHandler.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/SnakeWebSocketHandler.java index a5dd6a1aabe9..70c6162d79d9 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/SnakeWebSocketHandler.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/SnakeWebSocketHandler.java @@ -80,17 +80,11 @@ public void afterConnectionEstablished(WebSocketSession session) throws Exceptio @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String payload = message.getPayload(); - if ("west".equals(payload)) { - this.snake.setDirection(Direction.WEST); - } - else if ("north".equals(payload)) { - this.snake.setDirection(Direction.NORTH); - } - else if ("east".equals(payload)) { - this.snake.setDirection(Direction.EAST); - } - else if ("south".equals(payload)) { - this.snake.setDirection(Direction.SOUTH); + switch (payload) { + case "west" -> this.snake.setDirection(Direction.WEST); + case "north" -> this.snake.setDirection(Direction.NORTH); + case "east" -> this.snake.setDirection(Direction.EAST); + case "south" -> this.snake.setDirection(Direction.SOUTH); } } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/SnakeWebSocketHandler.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/SnakeWebSocketHandler.java index 6d2553da595d..fbe606e4110a 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/SnakeWebSocketHandler.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/SnakeWebSocketHandler.java @@ -80,17 +80,11 @@ public void afterConnectionEstablished(WebSocketSession session) throws Exceptio @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String payload = message.getPayload(); - if ("west".equals(payload)) { - this.snake.setDirection(Direction.WEST); - } - else if ("north".equals(payload)) { - this.snake.setDirection(Direction.NORTH); - } - else if ("east".equals(payload)) { - this.snake.setDirection(Direction.EAST); - } - else if ("south".equals(payload)) { - this.snake.setDirection(Direction.SOUTH); + switch (payload) { + case "west" -> this.snake.setDirection(Direction.WEST); + case "north" -> this.snake.setDirection(Direction.NORTH); + case "east" -> this.snake.setDirection(Direction.EAST); + case "south" -> this.snake.setDirection(Direction.SOUTH); } } From d3c97adf7913a3b9e36f18466521bcf3fde40005 Mon Sep 17 00:00:00 2001 From: Tobias Lippert Date: Sun, 21 Jan 2024 20:05:06 +0100 Subject: [PATCH 1124/1215] Remove redundant array creation See gh-39259 --- .../boot/buildpack/platform/socket/BsdDomainSocket.java | 2 +- .../boot/buildpack/platform/socket/LinuxDomainSocket.java | 2 +- .../boot/jarmode/layertools/ExtractCommandTests.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/BsdDomainSocket.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/BsdDomainSocket.java index 4fdfeea64200..6105373c99ee 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/BsdDomainSocket.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/BsdDomainSocket.java @@ -75,7 +75,7 @@ private SockaddrUn(byte sunFamily, byte[] path) { @Override protected List getFieldOrder() { - return Arrays.asList(new String[] { "sunLen", "sunFamily", "sunPath" }); + return Arrays.asList("sunLen", "sunFamily", "sunPath"); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/LinuxDomainSocket.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/LinuxDomainSocket.java index 24950d6c9fc1..09bd1549fc79 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/LinuxDomainSocket.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/LinuxDomainSocket.java @@ -72,7 +72,7 @@ private SockaddrUn(byte sunFamily, byte[] path) { @Override protected List getFieldOrder() { - return Arrays.asList(new String[] { "sunFamily", "sunPath" }); + return Arrays.asList("sunFamily", "sunPath"); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java index 1ec5a39214bf..b4e418a6792d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java @@ -103,7 +103,7 @@ void runExtractsLayers() { private void timeAttributes(File file) { try { BasicFileAttributes basicAttributes = Files - .getFileAttributeView(file.toPath(), BasicFileAttributeView.class, new LinkOption[0]) + .getFileAttributeView(file.toPath(), BasicFileAttributeView.class) .readAttributes(); assertThat(basicAttributes.lastModifiedTime().to(TimeUnit.SECONDS)) .isEqualTo(LAST_MODIFIED_TIME.to(TimeUnit.SECONDS)); From 15f1e8536bd1ddc7780a4a2c5a5be00cf0fa0a4a Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 23 Jan 2024 09:37:19 -0800 Subject: [PATCH 1125/1215] Polish 'Remove redundant array creation' See gh-39259 --- .../boot/jarmode/layertools/ExtractCommandTests.java | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java index b4e418a6792d..249cfaef8cef 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java @@ -22,7 +22,6 @@ import java.io.IOException; import java.io.InputStreamReader; import java.nio.file.Files; -import java.nio.file.LinkOption; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; From 7f4aaacf42a157cee1e16cf77fa6cd5da3265103 Mon Sep 17 00:00:00 2001 From: Tobias Lippert Date: Sun, 21 Jan 2024 20:06:20 +0100 Subject: [PATCH 1126/1215] Simplify stream chain operations See gh-39259 --- .../boot/build/bom/bomr/UpgradeDependencies.java | 4 ++-- .../boot/loader/tools/AbstractPackagerTests.java | 2 +- .../boot/testsupport/junit/DisabledOnOsCondition.java | 2 +- .../properties/IncompatibleConfigurationFailureAnalyzer.java | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeDependencies.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeDependencies.java index d85817e36ddd..13ba197ccf04 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeDependencies.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeDependencies.java @@ -242,9 +242,9 @@ private boolean isAnUpgrade(Library library, DependencyVersion candidate) { } private boolean isNotProhibited(Library library, DependencyVersion candidate) { - return !library.getProhibitedVersions() + return library.getProhibitedVersions() .stream() - .anyMatch((prohibited) -> prohibited.isProhibited(candidate.toString())); + .noneMatch((prohibited) -> prohibited.isProhibited(candidate.toString())); } private List matchingLibraries() { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java index 1193ce35959b..25baeca88737 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java @@ -651,7 +651,7 @@ void nativeImageArgFileWithExcludesIsWritten() throws Exception { expected.add("\\Q" + libraryTwo.getName() + "\\E"); expected.add("^/META-INF/native-image/.*"); assertThat(getPackagedEntryContent("META-INF/native-image/argfile")) - .isEqualTo(expected.stream().collect(Collectors.joining("\n")) + "\n"); + .isEqualTo(String.join("\n", expected) + "\n"); } private File createLibraryJar() throws IOException { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/DisabledOnOsCondition.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/DisabledOnOsCondition.java index 32a08b0e88e8..542bca5d5b79 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/DisabledOnOsCondition.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/DisabledOnOsCondition.java @@ -53,7 +53,7 @@ private ConditionEvaluationResult evaluate(DisabledOnOs annotation) { String architecture = System.getProperty("os.arch"); String os = System.getProperty("os.name"); boolean onDisabledOs = Arrays.stream(annotation.os()).anyMatch(OS::isCurrentOs); - boolean onDisabledArchitecture = Arrays.stream(annotation.architecture()).anyMatch(architecture::equals); + boolean onDisabledArchitecture = Arrays.asList(annotation.architecture()).contains(architecture); if (onDisabledOs && onDisabledArchitecture) { String reason = annotation.disabledReason().isEmpty() ? String.format("Disabled on OS = %s, architecture = %s", os, architecture) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/IncompatibleConfigurationFailureAnalyzer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/IncompatibleConfigurationFailureAnalyzer.java index 8df1d1e2378d..b00ada869193 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/IncompatibleConfigurationFailureAnalyzer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/IncompatibleConfigurationFailureAnalyzer.java @@ -32,7 +32,7 @@ class IncompatibleConfigurationFailureAnalyzer extends AbstractFailureAnalyzer Date: Tue, 23 Jan 2024 09:40:26 -0800 Subject: [PATCH 1127/1215] Polish 'Simplify stream chain operations' See gh-39259 --- .../properties/IncompatibleConfigurationFailureAnalyzer.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/IncompatibleConfigurationFailureAnalyzer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/IncompatibleConfigurationFailureAnalyzer.java index b00ada869193..11128e184488 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/IncompatibleConfigurationFailureAnalyzer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/IncompatibleConfigurationFailureAnalyzer.java @@ -16,8 +16,6 @@ package org.springframework.boot.context.properties; -import java.util.stream.Collectors; - import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; import org.springframework.boot.diagnostics.FailureAnalysis; From 24e0864105d5d051afc29e0238595bda66a5e876 Mon Sep 17 00:00:00 2001 From: Tobias Lippert Date: Sun, 21 Jan 2024 20:14:48 +0100 Subject: [PATCH 1128/1215] Replace !Optional.isPresent with Optional.isEmpty See gh-39259 --- .../boot/build/bom/bomr/UpgradeDependencies.java | 2 +- .../boot/testsupport/junit/DisabledOnOsCondition.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeDependencies.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeDependencies.java index 13ba197ccf04..19e897cb823d 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeDependencies.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeDependencies.java @@ -194,7 +194,7 @@ private Milestone determineMilestone(GitHubRepository repository) { java.util.Optional matchingMilestone = milestones.stream() .filter((milestone) -> milestone.getName().equals(getMilestone().get())) .findFirst(); - if (!matchingMilestone.isPresent()) { + if (matchingMilestone.isEmpty()) { throw new InvalidUserDataException("Unknown milestone: " + getMilestone().get()); } return matchingMilestone.get(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/DisabledOnOsCondition.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/DisabledOnOsCondition.java index 542bca5d5b79..c3cad58fc308 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/DisabledOnOsCondition.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/DisabledOnOsCondition.java @@ -37,7 +37,7 @@ class DisabledOnOsCondition implements ExecutionCondition { @Override public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { - if (!context.getElement().isPresent()) { + if (context.getElement().isEmpty()) { return ConditionEvaluationResult.enabled("No element for @DisabledOnOs found"); } MergedAnnotation annotation = MergedAnnotations From 8f1a330dd5f256a7588a96fb1814fe59d3471e17 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 23 Jan 2024 09:10:52 -0800 Subject: [PATCH 1129/1215] Update copyright header of cleaned up code See gh-39259 --- .../main/java/org/springframework/boot/build/bom/BomPlugin.java | 2 +- .../boot/build/bom/bomr/version/AbstractDependencyVersion.java | 2 +- .../build/bom/bomr/version/ReleaseTrainDependencyVersion.java | 2 +- .../classpath/CheckClasspathForProhibitedDependencies.java | 2 +- .../cloudfoundry/CloudFoundryWebEndpointDiscoverer.java | 2 +- .../actuate/autoconfigure/info/InfoContributorFallback.java | 2 +- .../boot/actuate/autoconfigure/metrics/MeterValue.java | 2 +- .../metrics/export/signalfx/SignalFxProperties.java | 2 +- .../autoconfigure/security/reactive/EndpointRequest.java | 2 +- .../boot/autoconfigure/cassandra/CassandraProperties.java | 2 +- .../boot/autoconfigure/jackson/JacksonProperties.java | 2 +- .../boot/devtools/restart/classloader/ClassLoaderFile.java | 2 +- .../autoconfigure/web/servlet/WebDriverContextCustomizer.java | 2 +- .../org/springframework/boot/test/context/SpringBootTest.java | 2 +- .../boot/test/mock/mockito/ResetMocksTestExecutionListener.java | 2 +- .../boot/buildpack/platform/socket/BsdDomainSocket.java | 2 +- .../boot/buildpack/platform/socket/LinuxDomainSocket.java | 2 +- .../org/springframework/boot/cli/command/core/HelpCommand.java | 2 +- .../boot/configurationprocessor/metadata/Metadata.java | 2 +- .../boot/gradle/plugin/ApplicationPluginAction.java | 2 +- .../boot/loader/tools/AbstractPackagerTests.java | 2 +- .../org/springframework/boot/loader/zip/ZipStringTests.java | 2 +- .../properties/migrator/PropertiesMigrationReporter.java | 2 +- .../boot/context/properties/migrator/PropertyMigration.java | 2 +- .../boot/testsupport/junit/DisabledOnOsCondition.java | 2 +- .../org/springframework/boot/context/config/ConfigData.java | 2 +- .../boot/context/config/ConfigDataEnvironmentContributor.java | 2 +- .../boot/context/config/ConfigDataEnvironmentContributors.java | 2 +- .../properties/IncompatibleConfigurationFailureAnalyzer.java | 2 +- .../properties/NotConstructorBoundInjectionFailureAnalyzer.java | 2 +- .../boot/context/properties/bind/BindMethod.java | 2 +- .../context/properties/bind/DefaultBindConstructorProvider.java | 2 +- .../boot/logging/log4j2/SpringProfileArbiter.java | 2 +- .../boot/web/embedded/tomcat/NestedJarResourceSet.java | 2 +- .../springframework/boot/web/server/GracefulShutdownResult.java | 2 +- .../main/java/org/springframework/boot/web/server/Shutdown.java | 2 +- .../boot/convert/CharSequenceToObjectConverterTests.java | 2 +- .../boot/web/servlet/DynamicRegistrationBeanTests.java | 2 +- .../boot/web/servlet/server/StaticResourceJarsTests.java | 2 +- .../EmbeddedServerContainerInvocationContextProvider.java | 2 +- .../smoketest/liquibase/SampleLiquibaseApplicationTests.java | 2 +- .../src/main/java/smoketest/websocket/jetty/snake/Location.java | 2 +- .../src/main/java/smoketest/websocket/jetty/snake/Snake.java | 2 +- .../main/java/smoketest/websocket/jetty/snake/SnakeTimer.java | 2 +- .../smoketest/websocket/jetty/snake/SnakeWebSocketHandler.java | 2 +- .../main/java/smoketest/websocket/tomcat/snake/Location.java | 2 +- .../src/main/java/smoketest/websocket/tomcat/snake/Snake.java | 2 +- .../main/java/smoketest/websocket/tomcat/snake/SnakeTimer.java | 2 +- .../smoketest/websocket/tomcat/snake/SnakeWebSocketHandler.java | 2 +- .../main/java/smoketest/websocket/undertow/snake/Location.java | 2 +- .../src/main/java/smoketest/websocket/undertow/snake/Snake.java | 2 +- .../java/smoketest/websocket/undertow/snake/SnakeTimer.java | 2 +- .../websocket/undertow/snake/SnakeWebSocketHandler.java | 2 +- 53 files changed, 53 insertions(+), 53 deletions(-) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java index f4361dc3b7bd..0cc7b4189068 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/AbstractDependencyVersion.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/AbstractDependencyVersion.java index a595718ac7e8..d0c74aa2da4e 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/AbstractDependencyVersion.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/AbstractDependencyVersion.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/ReleaseTrainDependencyVersion.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/ReleaseTrainDependencyVersion.java index 6c1e5f3d1715..e43c1b05d9de 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/ReleaseTrainDependencyVersion.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/ReleaseTrainDependencyVersion.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/buildSrc/src/main/java/org/springframework/boot/build/classpath/CheckClasspathForProhibitedDependencies.java b/buildSrc/src/main/java/org/springframework/boot/build/classpath/CheckClasspathForProhibitedDependencies.java index dc976b2eefdb..abc1098e8d37 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/classpath/CheckClasspathForProhibitedDependencies.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/classpath/CheckClasspathForProhibitedDependencies.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscoverer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscoverer.java index 5bb1638333b5..c401f5cf7801 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscoverer.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cloudfoundry/CloudFoundryWebEndpointDiscoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/info/InfoContributorFallback.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/info/InfoContributorFallback.java index b93e148b4d73..ce03c3f76a44 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/info/InfoContributorFallback.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/info/InfoContributorFallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterValue.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterValue.java index 3b00fd6be427..fa429ca765b1 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterValue.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MeterValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/signalfx/SignalFxProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/signalfx/SignalFxProperties.java index d1afe6fd8a9c..0b9555b9aecb 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/signalfx/SignalFxProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/export/signalfx/SignalFxProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequest.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequest.java index 4efc65d05c05..bed3b8f5c068 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequest.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/security/reactive/EndpointRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java index dcc1b07a1c7f..f2f8cae7e39c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java index 31e3da6200f0..46162768d4f5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/classloader/ClassLoaderFile.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/classloader/ClassLoaderFile.java index 9972ee764604..46df7df0d325 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/classloader/ClassLoaderFile.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/restart/classloader/ClassLoaderFile.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverContextCustomizer.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverContextCustomizer.java index d79d79d4b806..e0c178db3e2b 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverContextCustomizer.java +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/WebDriverContextCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTest.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTest.java index 7a5cffa0387c..41cb55b1557f 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTest.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/ResetMocksTestExecutionListener.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/ResetMocksTestExecutionListener.java index 1c2957d72a30..4aedbd3e48d4 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/ResetMocksTestExecutionListener.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/mock/mockito/ResetMocksTestExecutionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/BsdDomainSocket.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/BsdDomainSocket.java index 6105373c99ee..37b597250d29 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/BsdDomainSocket.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/BsdDomainSocket.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/LinuxDomainSocket.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/LinuxDomainSocket.java index 09bd1549fc79..13490f17fa60 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/LinuxDomainSocket.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/socket/LinuxDomainSocket.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/core/HelpCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/core/HelpCommand.java index 4f4b513b6ffd..6cae752f3f55 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/core/HelpCommand.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/core/HelpCommand.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/Metadata.java b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/Metadata.java index d65b8dd4ec41..0f4fcb10fd8c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/Metadata.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/Metadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ApplicationPluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ApplicationPluginAction.java index 3ba91dda77af..7b29a32685a9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ApplicationPluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ApplicationPluginAction.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java index 25baeca88737..7dce78edafc1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/AbstractPackagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipStringTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipStringTests.java index 0bcdc5f956b4..1f35622d820c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipStringTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/ZipStringTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReporter.java b/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReporter.java index 9872c6ecd36c..c1f7e61796d5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReporter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertiesMigrationReporter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertyMigration.java b/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertyMigration.java index 24f9410052db..ab69d8213072 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertyMigration.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-properties-migrator/src/main/java/org/springframework/boot/context/properties/migrator/PropertyMigration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/DisabledOnOsCondition.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/DisabledOnOsCondition.java index c3cad58fc308..3d1f6dc6b34a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/DisabledOnOsCondition.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/junit/DisabledOnOsCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigData.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigData.java index 1c68b25c4306..1a9dcf068c66 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigData.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigData.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributor.java index d5ffe3083447..dc2dcd45aa4a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributor.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributors.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributors.java index 1e399785946f..ca6bf96784c3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributors.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributors.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/IncompatibleConfigurationFailureAnalyzer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/IncompatibleConfigurationFailureAnalyzer.java index 11128e184488..fad96a58123e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/IncompatibleConfigurationFailureAnalyzer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/IncompatibleConfigurationFailureAnalyzer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NotConstructorBoundInjectionFailureAnalyzer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NotConstructorBoundInjectionFailureAnalyzer.java index 548eb266bae1..c7c48fe4ca2d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NotConstructorBoundInjectionFailureAnalyzer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/NotConstructorBoundInjectionFailureAnalyzer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindMethod.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindMethod.java index fe3664449cb5..0277907965eb 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindMethod.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProvider.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProvider.java index 33da909ac8a2..de23e928a2fa 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProvider.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DefaultBindConstructorProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringProfileArbiter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringProfileArbiter.java index 65dcd3e2d535..03e9e2494796 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringProfileArbiter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringProfileArbiter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/NestedJarResourceSet.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/NestedJarResourceSet.java index a0a003d2acab..47a9f5b5711e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/NestedJarResourceSet.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/NestedJarResourceSet.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/GracefulShutdownResult.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/GracefulShutdownResult.java index 4cc7eaa1f668..67b2c5e61fdb 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/GracefulShutdownResult.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/GracefulShutdownResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Shutdown.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Shutdown.java index 739234260d62..caa22eac98dc 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Shutdown.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Shutdown.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/CharSequenceToObjectConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/CharSequenceToObjectConverterTests.java index 73b434865570..10382dd4e01c 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/CharSequenceToObjectConverterTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/CharSequenceToObjectConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/DynamicRegistrationBeanTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/DynamicRegistrationBeanTests.java index 37f91191e840..033792e4d9e5 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/DynamicRegistrationBeanTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/DynamicRegistrationBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/StaticResourceJarsTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/StaticResourceJarsTests.java index 8643e81a54c8..050cbb447728 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/StaticResourceJarsTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/server/StaticResourceJarsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/intTest/java/org/springframework/boot/context/embedded/EmbeddedServerContainerInvocationContextProvider.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/intTest/java/org/springframework/boot/context/embedded/EmbeddedServerContainerInvocationContextProvider.java index 135b677d42dc..4ed867e3ee03 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/intTest/java/org/springframework/boot/context/embedded/EmbeddedServerContainerInvocationContextProvider.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-server-tests/src/intTest/java/org/springframework/boot/context/embedded/EmbeddedServerContainerInvocationContextProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-liquibase/src/test/java/smoketest/liquibase/SampleLiquibaseApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-liquibase/src/test/java/smoketest/liquibase/SampleLiquibaseApplicationTests.java index b0df8bb338c2..72038526d3c2 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-liquibase/src/test/java/smoketest/liquibase/SampleLiquibaseApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-liquibase/src/test/java/smoketest/liquibase/SampleLiquibaseApplicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/Location.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/Location.java index 02390e3a1107..563235703d60 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/Location.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/Location.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/Snake.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/Snake.java index 82372c4d2468..2376d9028b3a 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/Snake.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/Snake.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/SnakeTimer.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/SnakeTimer.java index f7a8871e3735..8620bb6d0236 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/SnakeTimer.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/SnakeTimer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/SnakeWebSocketHandler.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/SnakeWebSocketHandler.java index e79eb2bdb4f2..625a5c7dfe6b 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/SnakeWebSocketHandler.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-jetty/src/main/java/smoketest/websocket/jetty/snake/SnakeWebSocketHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/Location.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/Location.java index 21550e2ba8c1..62aca98c75b0 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/Location.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/Location.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/Snake.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/Snake.java index dbedf531683e..e20044600cda 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/Snake.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/Snake.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/SnakeTimer.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/SnakeTimer.java index c50bb9fc292b..7eef15019034 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/SnakeTimer.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/SnakeTimer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/SnakeWebSocketHandler.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/SnakeWebSocketHandler.java index 70c6162d79d9..2080c9d23a1f 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/SnakeWebSocketHandler.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-tomcat/src/main/java/smoketest/websocket/tomcat/snake/SnakeWebSocketHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/Location.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/Location.java index 16050be98e79..d7b3e06c8ad6 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/Location.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/Location.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/Snake.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/Snake.java index 55eb35d38dca..f0d2f297520a 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/Snake.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/Snake.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/SnakeTimer.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/SnakeTimer.java index 520c485329cc..d04b82c8e252 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/SnakeTimer.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/SnakeTimer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/SnakeWebSocketHandler.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/SnakeWebSocketHandler.java index fbe606e4110a..18b1216cb6af 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/SnakeWebSocketHandler.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-websocket-undertow/src/main/java/smoketest/websocket/undertow/snake/SnakeWebSocketHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. From d702c2f8606ad7f0881b6586179c000ecd8bd89e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 24 Jan 2024 13:59:43 +0000 Subject: [PATCH 1130/1215] Permit upgrades to Jetty Reactive HTTPClient 4.0.2 Closes gh-39288 --- spring-boot-project/spring-boot-dependencies/build.gradle | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 63c2c287ab98..37e4674049e8 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -672,10 +672,6 @@ bom { } } library("Jetty Reactive HTTPClient", "4.0.1") { - prohibit { - versionRange "[4.0.2]" - because "it causes problems in Spring Framework (https://github.com/spring-projects/spring-framework/issues/31931#issue-2061468092)" - } group("org.eclipse.jetty") { modules = [ "jetty-reactive-httpclient" From 1247f8920923c7c0a173516de3a192a962951a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edd=C3=BA=20Mel=C3=A9ndez?= Date: Thu, 25 Jan 2024 17:22:19 -0500 Subject: [PATCH 1131/1215] Polish See gh-39312 --- .../ActiveMQDockerComposeConnectionDetailsFactory.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQDockerComposeConnectionDetailsFactory.java index ac3809d8da21..30597c45a8dd 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQDockerComposeConnectionDetailsFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -16,7 +16,6 @@ package org.springframework.boot.docker.compose.service.connection.activemq; -import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails; import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQConnectionDetails; import org.springframework.boot.docker.compose.core.RunningService; import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; @@ -43,7 +42,7 @@ protected ActiveMQConnectionDetails getDockerComposeConnectionDetails(DockerComp } /** - * {@link RabbitConnectionDetails} backed by a {@code rabbitmq} + * {@link ActiveMQConnectionDetails} backed by a {@code activemq} * {@link RunningService}. */ static class ActiveMQDockerComposeConnectionDetails extends DockerComposeConnectionDetails From a85e99790b3f02b8b60180e9b3935901bf01045d Mon Sep 17 00:00:00 2001 From: Piyal Ahmed Date: Wed, 24 Jan 2024 16:34:16 +0600 Subject: [PATCH 1132/1215] Fix NestedJarFile constructor javadoc See gh-39285 --- .../boot/loader/jar/NestedJarFile.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java index 1c2fea69a403..6168202950ea 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/NestedJarFile.java @@ -101,8 +101,10 @@ public class NestedJarFile extends JarFile { * Creates a new {@link NestedJarFile} instance to read from the specific * {@code File}. * @param file the jar file to be opened for reading - * @param nestedEntryName the nested entry name to open or {@code null} + * @param nestedEntryName the nested entry name to open * @throws IOException on I/O error + * @throws IllegalArgumentException if {@code nestedEntryName} is {@code null} or + * empty */ public NestedJarFile(File file, String nestedEntryName) throws IOException { this(file, nestedEntryName, null, true, Cleaner.instance); @@ -112,9 +114,11 @@ public NestedJarFile(File file, String nestedEntryName) throws IOException { * Creates a new {@link NestedJarFile} instance to read from the specific * {@code File}. * @param file the jar file to be opened for reading - * @param nestedEntryName the nested entry name to open or {@code null} + * @param nestedEntryName the nested entry name to open * @param version the release version to use when opening a multi-release jar * @throws IOException on I/O error + * @throws IllegalArgumentException if {@code nestedEntryName} is {@code null} or + * empty */ public NestedJarFile(File file, String nestedEntryName, Runtime.Version version) throws IOException { this(file, nestedEntryName, version, true, Cleaner.instance); @@ -124,11 +128,13 @@ public NestedJarFile(File file, String nestedEntryName, Runtime.Version version) * Creates a new {@link NestedJarFile} instance to read from the specific * {@code File}. * @param file the jar file to be opened for reading - * @param nestedEntryName the nested entry name to open or {@code null} + * @param nestedEntryName the nested entry name to open * @param version the release version to use when opening a multi-release jar * @param onlyNestedJars if only nested jars should be opened * @param cleaner the cleaner used to release resources * @throws IOException on I/O error + * @throws IllegalArgumentException if {@code nestedEntryName} is {@code null} or + * empty */ NestedJarFile(File file, String nestedEntryName, Runtime.Version version, boolean onlyNestedJars, Cleaner cleaner) throws IOException { From dae395214469ca23c78c05ec25451cff83a788e0 Mon Sep 17 00:00:00 2001 From: Wzy19930507 <1208931582@qq.com> Date: Sun, 21 Jan 2024 22:28:55 +0800 Subject: [PATCH 1133/1215] Include the environment default profiles in the env endpoint's response See gh-39257 --- ...EnvironmentEndpointDocumentationTests.java | 7 ++++-- .../boot/actuate/env/EnvironmentEndpoint.java | 24 +++++++++++++++---- .../EnvironmentEndpointWebExtensionTests.java | 3 ++- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/EnvironmentEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/EnvironmentEndpointDocumentationTests.java index 061f6e8eaf8e..55703f33da21 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/EnvironmentEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/EnvironmentEndpointDocumentationTests.java @@ -64,6 +64,9 @@ class EnvironmentEndpointDocumentationTests extends MockMvcEndpointDocumentation private static final FieldDescriptor activeProfiles = fieldWithPath("activeProfiles") .description("Names of the active profiles, if any."); + private static final FieldDescriptor defaultProfiles = fieldWithPath("defaultProfiles") + .description("Names of the default profiles, if any."); + private static final FieldDescriptor propertySources = fieldWithPath("propertySources") .description("Property sources in order of precedence."); @@ -79,7 +82,7 @@ void env() throws Exception { replacePattern(Pattern.compile( "org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/"), ""), filterProperties()), - responseFields(activeProfiles, propertySources, propertySourceName, + responseFields(activeProfiles, defaultProfiles, propertySources, propertySourceName, fieldWithPath("propertySources.[].properties") .description("Properties in the property source keyed by property name."), fieldWithPath("propertySources.[].properties.*.value") @@ -101,7 +104,7 @@ void singlePropertyFromEnv() throws Exception { .optional(), fieldWithPath("property.source").description("Name of the source of the property."), fieldWithPath("property.value").description("Value of the property."), activeProfiles, - propertySources, propertySourceName, + defaultProfiles, propertySources, propertySourceName, fieldWithPath("propertySources.[].property") .description("Property in the property source, if any.") .optional(), diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java index 1358f6f0636e..0abdb53fc4d6 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/env/EnvironmentEndpoint.java @@ -101,7 +101,8 @@ private EnvironmentDescriptor getEnvironmentDescriptor(Predicate propert propertyNamePredicate, showUnsanitized)); } }); - return new EnvironmentDescriptor(Arrays.asList(this.environment.getActiveProfiles()), propertySources); + return new EnvironmentDescriptor(Arrays.asList(this.environment.getActiveProfiles()), + Arrays.asList(this.environment.getDefaultProfiles()), propertySources); } @ReadOperation @@ -114,7 +115,7 @@ EnvironmentEntryDescriptor getEnvironmentEntryDescriptor(String propertyName, bo Map descriptors = getPropertySourceDescriptors(propertyName, showUnsanitized); PropertySummaryDescriptor summary = getPropertySummaryDescriptor(descriptors); return new EnvironmentEntryDescriptor(summary, Arrays.asList(this.environment.getActiveProfiles()), - toPropertySourceDescriptors(descriptors)); + Arrays.asList(this.environment.getDefaultProfiles()), toPropertySourceDescriptors(descriptors)); } private List toPropertySourceDescriptors( @@ -209,10 +210,14 @@ public static final class EnvironmentDescriptor implements OperationResponseBody private final List activeProfiles; + private final List defaultProfiles; + private final List propertySources; - private EnvironmentDescriptor(List activeProfiles, List propertySources) { + private EnvironmentDescriptor(List activeProfiles, List defaultProfiles, + List propertySources) { this.activeProfiles = activeProfiles; + this.defaultProfiles = defaultProfiles; this.propertySources = propertySources; } @@ -220,6 +225,10 @@ public List getActiveProfiles() { return this.activeProfiles; } + public List getDefaultProfiles() { + return this.defaultProfiles; + } + public List getPropertySources() { return this.propertySources; } @@ -236,12 +245,15 @@ public static final class EnvironmentEntryDescriptor { private final List activeProfiles; + private final List defaultProfiles; + private final List propertySources; EnvironmentEntryDescriptor(PropertySummaryDescriptor property, List activeProfiles, - List propertySources) { + List defaultProfiles, List propertySources) { this.property = property; this.activeProfiles = activeProfiles; + this.defaultProfiles = defaultProfiles; this.propertySources = propertySources; } @@ -253,6 +265,10 @@ public List getActiveProfiles() { return this.activeProfiles; } + public List getDefaultProfiles() { + return this.defaultProfiles; + } + public List getPropertySources() { return this.propertySources; } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebExtensionTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebExtensionTests.java index febf1ee346fd..fa00f3cd0e0c 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebExtensionTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/env/EnvironmentEndpointWebExtensionTests.java @@ -86,7 +86,8 @@ void whenShowValuesIsWhenAuthorizedAndSecurityContextIsNotAuthorized() { private void verifyPrefixed(SecurityContext securityContext, boolean showUnsanitized) { given(this.delegate.getEnvironmentEntryDescriptor("test", showUnsanitized)) - .willReturn(new EnvironmentEntryDescriptor(null, Collections.emptyList(), Collections.emptyList())); + .willReturn(new EnvironmentEntryDescriptor(null, Collections.emptyList(), Collections.emptyList(), + Collections.emptyList())); this.webExtension.environmentEntry(securityContext, "test"); then(this.delegate).should().getEnvironmentEntryDescriptor("test", showUnsanitized); } From d31b02c38d3d6ccbc17a468c210c774edc33595d Mon Sep 17 00:00:00 2001 From: Claudio Nave Date: Wed, 17 Jan 2024 23:04:06 +0100 Subject: [PATCH 1134/1215] Add liquibase ui-service property See gh-39227 --- .../liquibase/LiquibaseAutoConfiguration.java | 6 ++- .../liquibase/LiquibaseProperties.java | 37 ++++++++++++++++++- ...itional-spring-configuration-metadata.json | 4 ++ .../LiquibaseAutoConfigurationTests.java | 14 ++++++- .../liquibase/LiquibasePropertiesTests.java | 9 ++++- 5 files changed, 66 insertions(+), 4 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java index 979053511255..089cd79c404f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -22,6 +22,7 @@ import liquibase.UpdateSummaryOutputEnum; import liquibase.change.DatabaseChange; import liquibase.integration.spring.SpringLiquibase; +import liquibase.ui.UIServiceEnum; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; @@ -122,6 +123,9 @@ public SpringLiquibase liquibase(ObjectProvider dataSource, liquibase .setShowSummaryOutput(UpdateSummaryOutputEnum.valueOf(properties.getShowSummaryOutput().name())); } + if (properties.getUiService() != null) { + liquibase.setUiService(UIServiceEnum.valueOf(properties.getUiService().name())); + } return liquibase; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java index 7b32afdadfde..cac5d3b33657 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -22,6 +22,7 @@ import liquibase.UpdateSummaryEnum; import liquibase.UpdateSummaryOutputEnum; import liquibase.integration.spring.SpringLiquibase; +import liquibase.ui.UIServiceEnum; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.util.Assert; @@ -147,6 +148,11 @@ public class LiquibaseProperties { */ private ShowSummaryOutput showSummaryOutput; + /** + * Which UIService to use. + */ + private UIService uiService; + public String getChangeLog() { return this.changeLog; } @@ -316,6 +322,14 @@ public void setShowSummaryOutput(ShowSummaryOutput showSummaryOutput) { this.showSummaryOutput = showSummaryOutput; } + public UIService getUiService() { + return this.uiService; + } + + public void setUiService(UIService uiService) { + this.uiService = uiService; + } + /** * Enumeration of types of summary to show. Values are the same as those on * {@link UpdateSummaryEnum}. To maximize backwards compatibility, the Liquibase enum @@ -368,4 +382,25 @@ public enum ShowSummaryOutput { } + /** + * Enumeration of types of UIService. Values are the same as those on + * {@link UIServiceEnum}. To maximize backwards compatibility, the Liquibase enum is + * not used directly. + * + * @since 3.3.0 + */ + public enum UIService { + + /** + * Console-based UIService. + */ + CONSOLE, + + /** + * Logging-based UIService. + */ + LOGGER + + } + } 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 c61b327b6ee0..d2e8c582597b 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 @@ -1959,6 +1959,10 @@ "name": "spring.liquibase.show-summary-output", "defaultValue": "log" }, + { + "name": "spring.liquibase.ui-service", + "defaultValue": "logger" + }, { "name": "spring.mail.test-connection", "description": "Whether to test that the mail server is available on startup.", diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java index ac15c4880e33..7b7456fc2053 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -33,6 +33,7 @@ import liquibase.UpdateSummaryOutputEnum; import liquibase.command.core.helpers.ShowSummaryArgument; import liquibase.integration.spring.SpringLiquibase; +import liquibase.ui.UIServiceEnum; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; @@ -226,6 +227,7 @@ void defaultValues() { assertThat(liquibase).extracting("showSummary").isNull(); assertThat(ShowSummaryArgument.SHOW_SUMMARY.getDefaultValue()).isEqualTo(UpdateSummaryEnum.SUMMARY); assertThat(liquibase).extracting("showSummaryOutput").isEqualTo(UpdateSummaryOutputEnum.LOG); + assertThat(liquibase).extracting("uiService").isEqualTo(UIServiceEnum.LOGGER); })); } @@ -411,6 +413,16 @@ void overrideShowSummaryOutput() { })); } + @Test + void overrideUiService() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) + .withPropertyValues("spring.liquibase.ui-service=console") + .run(assertLiquibase((liquibase) -> { + UIServiceEnum uiService = (UIServiceEnum) ReflectionTestUtils.getField(liquibase, "uiService"); + assertThat(uiService).isEqualTo(UIServiceEnum.CONSOLE); + })); + } + @Test @SuppressWarnings("unchecked") void testOverrideParameters() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibasePropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibasePropertiesTests.java index 57f6025f46a8..8c3f8260baf1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibasePropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibasePropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -21,10 +21,12 @@ import liquibase.UpdateSummaryEnum; import liquibase.UpdateSummaryOutputEnum; +import liquibase.ui.UIServiceEnum; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties.ShowSummary; import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties.ShowSummaryOutput; +import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties.UIService; import static org.assertj.core.api.Assertions.assertThat; @@ -45,6 +47,11 @@ void valuesOfShowSummaryOutputMatchValuesOfUpdateSummaryOutputEnum() { assertThat(namesOf(ShowSummaryOutput.values())).isEqualTo(namesOf(UpdateSummaryOutputEnum.values())); } + @Test + void valuesOfUiServiceMatchValuesOfUiServiceEnum() { + assertThat(namesOf(UIService.values())).isEqualTo(namesOf(UIServiceEnum.values())); + } + private List namesOf(Enum[] input) { return Stream.of(input).map(Enum::name).toList(); } From 50c44e301a03e10bf509b173733ea23e3d456454 Mon Sep 17 00:00:00 2001 From: tish Date: Thu, 25 Jan 2024 07:59:49 +0400 Subject: [PATCH 1135/1215] Prevent double registration of event publisher registrar See gh-39297 --- .../testcontainers/properties/TestcontainersPropertySource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java index 1994303645ef..d52cc0c8d51b 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java @@ -115,7 +115,7 @@ private static DynamicPropertyRegistry attach(Environment environment, Applicati if (eventPublisher != null) { propertySource.addEventPublisher(eventPublisher); } - else if (registry != null) { + else if (registry != null && !registry.containsBeanDefinition(EventPublisherRegistrar.NAME)) { registry.registerBeanDefinition(EventPublisherRegistrar.NAME, new RootBeanDefinition( EventPublisherRegistrar.class, () -> new EventPublisherRegistrar(environment))); } From 61ca87f7a4960353c1e7699a9567c666fc6c2231 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 30 Jan 2024 16:02:38 +0000 Subject: [PATCH 1136/1215] Polish "Prevent double registration of event publisher registrar" See gh-39297 --- .../TestcontainersPropertySource.java | 2 +- .../TestcontainersPropertySourceTests.java | 31 +++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java index d52cc0c8d51b..10bcb2a9864c 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java @@ -138,7 +138,7 @@ static TestcontainersPropertySource getOrAdd(ConfigurableEnvironment environment * to the {@link TestcontainersPropertySource}. This class is a * {@link BeanFactoryPostProcessor} so that it is initialized as early as possible. */ - private static class EventPublisherRegistrar implements BeanFactoryPostProcessor, ApplicationEventPublisherAware { + static class EventPublisherRegistrar implements BeanFactoryPostProcessor, ApplicationEventPublisherAware { static final String NAME = EventPublisherRegistrar.class.getName(); diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceTests.java index 4dce61ffc942..e11488b349c3 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceTests.java +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySourceTests.java @@ -23,6 +23,8 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.boot.testcontainers.properties.TestcontainersPropertySource.EventPublisherRegistrar; import org.springframework.context.ApplicationEvent; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.env.EnumerablePropertySource; @@ -42,6 +44,13 @@ class TestcontainersPropertySourceTests { private MockEnvironment environment = new MockEnvironment(); + private GenericApplicationContext context = new GenericApplicationContext(); + + TestcontainersPropertySourceTests() { + ((DefaultListableBeanFactory) this.context.getBeanFactory()).setAllowBeanDefinitionOverriding(false); + this.context.setEnvironment(this.environment); + } + @Test void getPropertyWhenHasValueSupplierReturnsSuppliedValue() { DynamicPropertyRegistry registry = TestcontainersPropertySource.attach(this.environment); @@ -90,14 +99,14 @@ void getSourceReturnsImmutableSource() { } @Test - void attachWhenNotAttachedAttaches() { + void attachToEnvironmentWhenNotAttachedAttaches() { TestcontainersPropertySource.attach(this.environment); PropertySource propertySource = this.environment.getPropertySources().get(TestcontainersPropertySource.NAME); assertThat(propertySource).isNotNull(); } @Test - void attachWhenAlreadyAttachedReturnsExisting() { + void attachToEnvironmentWhenAlreadyAttachedReturnsExisting() { DynamicPropertyRegistry r1 = TestcontainersPropertySource.attach(this.environment); PropertySource p1 = this.environment.getPropertySources().get(TestcontainersPropertySource.NAME); DynamicPropertyRegistry r2 = TestcontainersPropertySource.attach(this.environment); @@ -106,6 +115,24 @@ void attachWhenAlreadyAttachedReturnsExisting() { assertThat(p1).isSameAs(p2); } + @Test + void attachToEnvironmentAndContextWhenNotAttachedAttaches() { + TestcontainersPropertySource.attach(this.environment, this.context); + PropertySource propertySource = this.environment.getPropertySources().get(TestcontainersPropertySource.NAME); + assertThat(propertySource).isNotNull(); + assertThat(this.context.containsBean(EventPublisherRegistrar.NAME)); + } + + @Test + void attachToEnvironmentAndContextWhenAlreadyAttachedReturnsExisting() { + DynamicPropertyRegistry r1 = TestcontainersPropertySource.attach(this.environment, this.context); + PropertySource p1 = this.environment.getPropertySources().get(TestcontainersPropertySource.NAME); + DynamicPropertyRegistry r2 = TestcontainersPropertySource.attach(this.environment, this.context); + PropertySource p2 = this.environment.getPropertySources().get(TestcontainersPropertySource.NAME); + assertThat(r1).isSameAs(r2); + assertThat(p1).isSameAs(p2); + } + @Test void getPropertyPublishesEvent() { try (GenericApplicationContext applicationContext = new GenericApplicationContext()) { From 231aa014fa2413415c173cdc9d63dc1158e8bb6e Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 31 Jan 2024 08:35:11 +0100 Subject: [PATCH 1137/1215] Upgrade to Testcontainers 1.19.4 Closes gh-39353 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 37e4674049e8..1c3596392674 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1641,7 +1641,7 @@ bom { ] } } - library("Testcontainers", "1.19.3") { + library("Testcontainers", "1.19.4") { group("org.testcontainers") { imports = [ "testcontainers-bom" From f15cd93a35199ee658a3febff889ec2e20d2e10c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edd=C3=BA=20Mel=C3=A9ndez?= Date: Thu, 25 Jan 2024 17:07:08 -0500 Subject: [PATCH 1138/1215] Add service connection for Docker Compose and Testcontainers Artemis See gh-39311 --- .../jms/artemis/ArtemisAutoConfiguration.java | 42 ++++++++- .../jms/artemis/ArtemisConnectionDetails.java | 37 ++++++++ ...ArtemisConnectionFactoryConfiguration.java | 22 +++-- .../ArtemisConnectionFactoryFactory.java | 19 ++-- ...temisXAConnectionFactoryConfiguration.java | 15 +-- .../ArtemisAutoConfigurationTests.java | 56 +++++++++++- ...DockerComposeConnectionDetailsFactory.java | 84 +++++++++++++++++ .../activemq/ArtemisEnvironment.java | 45 +++++++++ .../main/resources/META-INF/spring.factories | 1 + ...nectionDetailsFactoryIntegrationTests.java | 48 ++++++++++ .../activemq/ArtemisEnvironmentTests.java | 57 ++++++++++++ .../connection/activemq/artemis-compose.yaml | 8 ++ .../src/docs/asciidoc/features/testing.adoc | 3 + .../spring-boot-testcontainers/build.gradle | 4 + ...emisContainerConnectionDetailsFactory.java | 71 +++++++++++++++ .../main/resources/META-INF/spring.factories | 1 + ...nectionDetailsFactoryIntegrationTests.java | 91 +++++++++++++++++++ .../testcontainers/DockerImageNames.java | 10 ++ 18 files changed, 589 insertions(+), 25 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionDetails.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisEnvironment.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisEnvironmentTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/activemq/artemis-compose.yaml create mode 100644 spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/activemq/ArtemisContainerConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/activemq/ArtemisContainerConnectionDetailsFactoryIntegrationTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisAutoConfiguration.java index 1087934873b3..a4732f0dd581 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -27,6 +27,7 @@ import org.springframework.boot.autoconfigure.jms.JmsProperties; import org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; /** @@ -48,4 +49,43 @@ ArtemisConnectionFactoryConfiguration.class }) public class ArtemisAutoConfiguration { + @Bean + @ConditionalOnMissingBean(ArtemisConnectionDetails.class) + ArtemisConnectionDetails artemisConnectionDetails(ArtemisProperties properties) { + return new PropertiesArtemisConnectionDetails(properties); + } + + /** + * Adapts {@link ArtemisProperties} to {@link ArtemisConnectionDetails}. + */ + static class PropertiesArtemisConnectionDetails implements ArtemisConnectionDetails { + + private final ArtemisProperties properties; + + PropertiesArtemisConnectionDetails(ArtemisProperties properties) { + this.properties = properties; + } + + @Override + public ArtemisMode getMode() { + return this.properties.getMode(); + } + + @Override + public String getBrokerUrl() { + return this.properties.getBrokerUrl(); + } + + @Override + public String getUser() { + return this.properties.getUser(); + } + + @Override + public String getPassword() { + return this.properties.getPassword(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionDetails.java new file mode 100644 index 000000000000..94db35414404 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionDetails.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.jms.artemis; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to an Artemis service. + * + * @author Eddú Meléndez + * @since 3.3.0 + */ +public interface ArtemisConnectionDetails extends ConnectionDetails { + + ArtemisMode getMode(); + + String getBrokerUrl(); + + String getUser(); + + String getPassword(); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryConfiguration.java index 98a305be8f38..5f4f879431d6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -49,13 +49,14 @@ static class SimpleConnectionFactoryConfiguration { @Bean(name = "jmsConnectionFactory") @ConditionalOnProperty(prefix = "spring.jms.cache", name = "enabled", havingValue = "false") - ActiveMQConnectionFactory jmsConnectionFactory(ArtemisProperties properties, ListableBeanFactory beanFactory) { - return createJmsConnectionFactory(properties, beanFactory); + ActiveMQConnectionFactory jmsConnectionFactory(ArtemisProperties properties, ListableBeanFactory beanFactory, + ArtemisConnectionDetails connectionDetails) { + return createJmsConnectionFactory(properties, connectionDetails, beanFactory); } private static ActiveMQConnectionFactory createJmsConnectionFactory(ArtemisProperties properties, - ListableBeanFactory beanFactory) { - return new ArtemisConnectionFactoryFactory(beanFactory, properties) + ArtemisConnectionDetails connectionDetails, ListableBeanFactory beanFactory) { + return new ArtemisConnectionFactoryFactory(beanFactory, properties, connectionDetails) .createConnectionFactory(ActiveMQConnectionFactory.class); } @@ -67,10 +68,11 @@ static class CachingConnectionFactoryConfiguration { @Bean(name = "jmsConnectionFactory") CachingConnectionFactory cachingJmsConnectionFactory(JmsProperties jmsProperties, - ArtemisProperties properties, ListableBeanFactory beanFactory) { + ArtemisProperties properties, ArtemisConnectionDetails connectionDetails, + ListableBeanFactory beanFactory) { JmsProperties.Cache cacheProperties = jmsProperties.getCache(); CachingConnectionFactory connectionFactory = new CachingConnectionFactory( - createJmsConnectionFactory(properties, beanFactory)); + createJmsConnectionFactory(properties, connectionDetails, beanFactory)); connectionFactory.setCacheConsumers(cacheProperties.isConsumers()); connectionFactory.setCacheProducers(cacheProperties.isProducers()); connectionFactory.setSessionCacheSize(cacheProperties.getSessionCacheSize()); @@ -87,8 +89,10 @@ CachingConnectionFactory cachingJmsConnectionFactory(JmsProperties jmsProperties static class PooledConnectionFactoryConfiguration { @Bean(destroyMethod = "stop") - JmsPoolConnectionFactory jmsConnectionFactory(ListableBeanFactory beanFactory, ArtemisProperties properties) { - ActiveMQConnectionFactory connectionFactory = new ArtemisConnectionFactoryFactory(beanFactory, properties) + JmsPoolConnectionFactory jmsConnectionFactory(ListableBeanFactory beanFactory, ArtemisProperties properties, + ArtemisConnectionDetails connectionDetails) { + ActiveMQConnectionFactory connectionFactory = new ArtemisConnectionFactoryFactory(beanFactory, properties, + connectionDetails) .createConnectionFactory(ActiveMQConnectionFactory.class); return new JmsPoolConnectionFactoryFactory(properties.getPool()) .createPooledConnectionFactory(connectionFactory); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryFactory.java index 344f8ace981c..70e74e4b3e44 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryFactory.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionFactoryFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -47,13 +47,18 @@ class ArtemisConnectionFactoryFactory { private final ArtemisProperties properties; + private final ArtemisConnectionDetails connectionDetails; + private final ListableBeanFactory beanFactory; - ArtemisConnectionFactoryFactory(ListableBeanFactory beanFactory, ArtemisProperties properties) { + ArtemisConnectionFactoryFactory(ListableBeanFactory beanFactory, ArtemisProperties properties, + ArtemisConnectionDetails connectionDetails) { Assert.notNull(beanFactory, "BeanFactory must not be null"); Assert.notNull(properties, "Properties must not be null"); + Assert.notNull(connectionDetails, "ConnectionDetails must not be null"); this.beanFactory = beanFactory; this.properties = properties; + this.connectionDetails = connectionDetails; } T createConnectionFactory(Class factoryClass) { @@ -80,7 +85,7 @@ private void startEmbeddedJms() { } private T doCreateConnectionFactory(Class factoryClass) throws Exception { - ArtemisMode mode = this.properties.getMode(); + ArtemisMode mode = this.connectionDetails.getMode(); if (mode == null) { mode = deduceMode(); } @@ -127,17 +132,17 @@ private T createEmbeddedConnectionFactory( private T createNativeConnectionFactory(Class factoryClass) throws Exception { T connectionFactory = newNativeConnectionFactory(factoryClass); - String user = this.properties.getUser(); + String user = this.connectionDetails.getUser(); if (StringUtils.hasText(user)) { connectionFactory.setUser(user); - connectionFactory.setPassword(this.properties.getPassword()); + connectionFactory.setPassword(this.connectionDetails.getPassword()); } return connectionFactory; } private T newNativeConnectionFactory(Class factoryClass) throws Exception { - String brokerUrl = StringUtils.hasText(this.properties.getBrokerUrl()) ? this.properties.getBrokerUrl() - : DEFAULT_BROKER_URL; + String brokerUrl = StringUtils.hasText(this.connectionDetails.getBrokerUrl()) + ? this.connectionDetails.getBrokerUrl() : DEFAULT_BROKER_URL; Constructor constructor = factoryClass.getConstructor(String.class); return constructor.newInstance(brokerUrl); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisXAConnectionFactoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisXAConnectionFactoryConfiguration.java index b296ae14b892..89e7823ca226 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisXAConnectionFactoryConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisXAConnectionFactoryConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -44,15 +44,16 @@ class ArtemisXAConnectionFactoryConfiguration { @Primary @Bean(name = { "jmsConnectionFactory", "xaJmsConnectionFactory" }) ConnectionFactory jmsConnectionFactory(ListableBeanFactory beanFactory, ArtemisProperties properties, - XAConnectionFactoryWrapper wrapper) throws Exception { - return wrapper.wrapConnectionFactory(new ArtemisConnectionFactoryFactory(beanFactory, properties) - .createConnectionFactory(ActiveMQXAConnectionFactory.class)); + ArtemisConnectionDetails connectionDetails, XAConnectionFactoryWrapper wrapper) throws Exception { + return wrapper + .wrapConnectionFactory(new ArtemisConnectionFactoryFactory(beanFactory, properties, connectionDetails) + .createConnectionFactory(ActiveMQXAConnectionFactory.class)); } @Bean - ActiveMQXAConnectionFactory nonXaJmsConnectionFactory(ListableBeanFactory beanFactory, - ArtemisProperties properties) { - return new ArtemisConnectionFactoryFactory(beanFactory, properties) + ActiveMQXAConnectionFactory nonXaJmsConnectionFactory(ListableBeanFactory beanFactory, ArtemisProperties properties, + ArtemisConnectionDetails connectionDetails) { + return new ArtemisConnectionFactoryFactory(beanFactory, properties, connectionDetails) .createConnectionFactory(ActiveMQXAConnectionFactory.class); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisAutoConfigurationTests.java index ec0c32031e73..0d1ed76b217b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -46,6 +46,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration; +import org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration.PropertiesArtemisConnectionDetails; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -371,6 +372,27 @@ void cachingConnectionFactoryNotOnTheClasspathAndCacheEnabledThenSimpleConnectio .run((context) -> assertThat(context).doesNotHaveBean(ActiveMQConnectionFactory.class)); } + @Test + void definesPropertiesBasedConnectionDetailsByDefault() { + this.contextRunner + .run((context) -> assertThat(context).hasSingleBean(PropertiesArtemisConnectionDetails.class)); + } + + @Test + void testConnectionFactoryWithOverridesWhenUsingCustomConnectionDetails() { + this.contextRunner.withClassLoader(new FilteredClassLoader(CachingConnectionFactory.class)) + .withPropertyValues("spring.artemis.pool.enabled=false", "spring.jms.cache.enabled=false") + .withUserConfiguration(TestConnectionDetailsConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(ArtemisConnectionDetails.class) + .doesNotHaveBean(PropertiesArtemisConnectionDetails.class); + ActiveMQConnectionFactory connectionFactory = context.getBean(ActiveMQConnectionFactory.class); + assertThat(connectionFactory.toURI().toString()).startsWith("tcp://localhost:12345"); + assertThat(connectionFactory.getUser()).isEqualTo("springuser"); + assertThat(connectionFactory.getPassword()).isEqualTo("spring"); + }); + } + private ConnectionFactory getConnectionFactory(AssertableApplicationContext context) { assertThat(context).hasSingleBean(ConnectionFactory.class).hasBean("jmsConnectionFactory"); ConnectionFactory connectionFactory = context.getBean(ConnectionFactory.class); @@ -496,4 +518,36 @@ ArtemisConfigurationCustomizer myArtemisCustomize() { } + @Configuration(proxyBeanMethods = false) + static class TestConnectionDetailsConfiguration { + + @Bean + ArtemisConnectionDetails activemqConnectionDetails() { + return new ArtemisConnectionDetails() { + + @Override + public ArtemisMode getMode() { + return ArtemisMode.NATIVE; + } + + @Override + public String getBrokerUrl() { + return "tcp://localhost:12345"; + } + + @Override + public String getUser() { + return "springuser"; + } + + @Override + public String getPassword() { + return "spring"; + } + + }; + } + + } + } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisDockerComposeConnectionDetailsFactory.java new file mode 100644 index 000000000000..c2ceeb0c6436 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.activemq; + +import org.springframework.boot.autoconfigure.jms.artemis.ArtemisConnectionDetails; +import org.springframework.boot.autoconfigure.jms.artemis.ArtemisMode; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create + * {@link ArtemisConnectionDetails} for an {@code artemis} service. + * + * @author Eddú Meléndez + */ +class ArtemisDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + private static final int ACTIVEMQ_PORT = 61616; + + protected ArtemisDockerComposeConnectionDetailsFactory() { + super("apache/activemq-classic"); + } + + @Override + protected ArtemisConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new ArtemisDockerComposeConnectionDetails(source.getRunningService()); + } + + /** + * {@link ArtemisConnectionDetails} backed by a {@code artemis} + * {@link RunningService}. + */ + static class ArtemisDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements ArtemisConnectionDetails { + + private final ArtemisEnvironment environment; + + private final String brokerUrl; + + protected ArtemisDockerComposeConnectionDetails(RunningService service) { + super(service); + this.environment = new ArtemisEnvironment(service.env()); + this.brokerUrl = "tcp://" + service.host() + ":" + service.ports().get(ACTIVEMQ_PORT); + } + + @Override + public ArtemisMode getMode() { + return ArtemisMode.NATIVE; + } + + @Override + public String getBrokerUrl() { + return this.brokerUrl; + } + + @Override + public String getUser() { + return this.environment.getUser(); + } + + @Override + public String getPassword() { + return this.environment.getPassword(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisEnvironment.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisEnvironment.java new file mode 100644 index 000000000000..078f216c2589 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisEnvironment.java @@ -0,0 +1,45 @@ +/* + * 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.docker.compose.service.connection.activemq; + +import java.util.Map; + +/** + * Artemis environment details. + * + * @author Eddú Meléndez + */ +class ArtemisEnvironment { + + private final String user; + + private final String password; + + ArtemisEnvironment(Map env) { + this.user = env.get("ACTIVEMQ_CONNECTION_USER"); + this.password = env.get("ACTIVEMQ_CONNECTION_PASSWORD"); + } + + String getUser() { + return this.user; + } + + String getPassword() { + return this.password; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories index 3ffd311e3631..fffc5722e0cc 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories @@ -6,6 +6,7 @@ org.springframework.boot.docker.compose.service.connection.DockerComposeServiceC # Connection Details Factories org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ org.springframework.boot.docker.compose.service.connection.activemq.ActiveMQDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.activemq.ArtemisDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.cassandra.CassandraDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.elasticsearch.ElasticsearchDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.flyway.JdbcAdaptingFlywayConnectionDetailsFactory,\ diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..2dd9413b7dcc --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,48 @@ +/* + * 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.docker.compose.service.connection.activemq; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.jms.artemis.ArtemisConnectionDetails; +import org.springframework.boot.autoconfigure.jms.artemis.ArtemisMode; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link ArtemisDockerComposeConnectionDetailsFactory}. + * + * @author Eddú Meléndez + */ +class ArtemisDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { + + ArtemisDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("artemis-compose.yaml", DockerImageNames.artemis()); + } + + @Test + void runCreatesConnectionDetails() { + ArtemisConnectionDetails connectionDetails = run(ArtemisConnectionDetails.class); + assertThat(connectionDetails.getMode()).isEqualTo(ArtemisMode.NATIVE); + assertThat(connectionDetails.getBrokerUrl()).isNotNull().startsWith("tcp://"); + assertThat(connectionDetails.getUser()).isEqualTo("root"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisEnvironmentTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisEnvironmentTests.java new file mode 100644 index 000000000000..d3781014b0fa --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisEnvironmentTests.java @@ -0,0 +1,57 @@ +/* + * 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.docker.compose.service.connection.activemq; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ArtemisEnvironment}. + * + * @author Eddú Meléndez + */ +class ArtemisEnvironmentTests { + + @Test + void getUserWhenHasNoActiveMqUser() { + ArtemisEnvironment environment = new ArtemisEnvironment(Collections.emptyMap()); + assertThat(environment.getUser()).isNull(); + } + + @Test + void getUserWhenHasActiveMqUser() { + ArtemisEnvironment environment = new ArtemisEnvironment(Map.of("ACTIVEMQ_CONNECTION_USER", "me")); + assertThat(environment.getUser()).isEqualTo("me"); + } + + @Test + void getPasswordWhenHasNoActiveMqPassword() { + ArtemisEnvironment environment = new ArtemisEnvironment(Collections.emptyMap()); + assertThat(environment.getPassword()).isNull(); + } + + @Test + void getPasswordWhenHasActiveMqPassword() { + ArtemisEnvironment environment = new ArtemisEnvironment(Map.of("ACTIVEMQ_CONNECTION_PASSWORD", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/activemq/artemis-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/activemq/artemis-compose.yaml new file mode 100644 index 000000000000..ffdd8c6b78ae --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/activemq/artemis-compose.yaml @@ -0,0 +1,8 @@ +services: + artemis: + image: '{imageName}' + ports: + - '61616' + environment: + ACTIVEMQ_CONNECTION_USER: 'root' + ACTIVEMQ_CONNECTION_PASSWORD: 'secret' diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc index 24bcf0d1ee44..c86faa72b03c 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc @@ -983,6 +983,9 @@ The following service connection factories are provided in the `spring-boot-test | `ActiveMQConnectionDetails` | Containers named "symptoma/activemq" +| `ArtemisConnectionDetails` +| Containers of type `ArtemisContainer` + | `CassandraConnectionDetails` | Containers of type `CassandraContainer` diff --git a/spring-boot-project/spring-boot-testcontainers/build.gradle b/spring-boot-project/spring-boot-testcontainers/build.gradle index 28b62213219a..f719fd8b5c29 100644 --- a/spring-boot-project/spring-boot-testcontainers/build.gradle +++ b/spring-boot-project/spring-boot-testcontainers/build.gradle @@ -17,6 +17,7 @@ dependencies { optional("org.springframework:spring-test") optional("org.springframework.data:spring-data-mongodb") optional("org.springframework.data:spring-data-neo4j") + optional("org.testcontainers:activemq") optional("org.testcontainers:cassandra") optional("org.testcontainers:couchbase") optional("org.testcontainers:elasticsearch") @@ -44,6 +45,9 @@ dependencies { exclude group: "commons-logging", module: "commons-logging" } testImplementation("org.apache.activemq:activemq-client-jakarta") + testImplementation("org.apache.activemq:artemis-jakarta-client") { + exclude group: "commons-logging", module: "commons-logging" + } testImplementation("org.assertj:assertj-core") testImplementation("org.awaitility:awaitility") testImplementation("org.influxdb:influxdb-java") diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/activemq/ArtemisContainerConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/activemq/ArtemisContainerConnectionDetailsFactory.java new file mode 100644 index 000000000000..798ff7d97f85 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/activemq/ArtemisContainerConnectionDetailsFactory.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.testcontainers.service.connection.activemq; + +import org.testcontainers.activemq.ArtemisContainer; + +import org.springframework.boot.autoconfigure.jms.artemis.ArtemisConnectionDetails; +import org.springframework.boot.autoconfigure.jms.artemis.ArtemisMode; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; + +/** + * {@link ContainerConnectionDetailsFactory} to create {@link ArtemisConnectionDetails} + * from a {@link ServiceConnection @ServiceConnection}-annotated {@link ArtemisContainer}. + * + * @author Eddú Meléndez + */ +class ArtemisContainerConnectionDetailsFactory + extends ContainerConnectionDetailsFactory { + + @Override + protected ArtemisConnectionDetails getContainerConnectionDetails( + ContainerConnectionSource source) { + return new ArtemisContainerConnectionDetails(source); + } + + private static final class ArtemisContainerConnectionDetails extends ContainerConnectionDetails + implements ArtemisConnectionDetails { + + private ArtemisContainerConnectionDetails(ContainerConnectionSource source) { + super(source); + } + + @Override + public ArtemisMode getMode() { + return ArtemisMode.NATIVE; + } + + @Override + public String getBrokerUrl() { + return getContainer().getBrokerUrl(); + } + + @Override + public String getUser() { + return getContainer().getUser(); + } + + @Override + public String getPassword() { + return getContainer().getPassword(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories index 5b8bda347fcb..71721a3f32d0 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories @@ -9,6 +9,7 @@ org.springframework.boot.testcontainers.service.connection.ServiceConnectionCont # Connection Details Factories org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ org.springframework.boot.testcontainers.service.connection.activemq.ActiveMQContainerConnectionDetailsFactory,\ +org.springframework.boot.testcontainers.service.connection.activemq.ArtemisContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.amqp.RabbitContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.cassandra.CassandraContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.couchbase.CouchbaseContainerConnectionDetailsFactory,\ diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/activemq/ArtemisContainerConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/activemq/ArtemisContainerConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..4c695936bbd4 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/activemq/ArtemisContainerConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,91 @@ +/* + * 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.testcontainers.service.connection.activemq; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.testcontainers.activemq.ArtemisContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration; +import org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.jms.core.JmsMessagingTemplate; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ArtemisContainerConnectionDetailsFactory}. + * + * @author Eddú Meléndez + */ +@SpringJUnitConfig +@Testcontainers(disabledWithoutDocker = true) +class ArtemisContainerConnectionDetailsFactoryIntegrationTests { + + @Container + @ServiceConnection + static final ArtemisContainer artemis = new ArtemisContainer(DockerImageNames.artemis()); + + @Autowired + private JmsMessagingTemplate jmsTemplate; + + @Autowired + private TestListener listener; + + @Test + void connectionCanBeMadeToActiveMQContainer() { + this.jmsTemplate.convertAndSend("sample.queue", "message"); + Awaitility.waitAtMost(Duration.ofMinutes(1)) + .untilAsserted(() -> assertThat(this.listener.messages).containsExactly("message")); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration({ ArtemisAutoConfiguration.class, JmsAutoConfiguration.class }) + static class TestConfiguration { + + @Bean + TestListener testListener() { + return new TestListener(); + } + + } + + static class TestListener { + + private final List messages = new ArrayList<>(); + + @JmsListener(destination = "sample.queue") + void processMessage(String message) { + this.messages.add(message); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java index 81a5673d8fec..7f9d598095c4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java @@ -30,6 +30,8 @@ public final class DockerImageNames { private static final String ACTIVE_MQ_VERSION = "5.18.0"; + private static final String ARTEMIS_VERSION = "2.31.2"; + private static final String CASSANDRA_VERSION = "3.11.10"; private static final String COUCHBASE_VERSION = "7.1.4"; @@ -81,6 +83,14 @@ public static DockerImageName activeMq() { return DockerImageName.parse("symptoma/activemq").withTag(ACTIVE_MQ_VERSION); } + /** + * Return a {@link DockerImageName} suitable for running Artemis. + * @return a docker image name for running artemis + */ + public static DockerImageName artemis() { + return DockerImageName.parse("apache/activemq-artemis").withTag(ARTEMIS_VERSION); + } + /** * Return a {@link DockerImageName} suitable for running Cassandra. * @return a docker image name for running cassandra From 1f321c4421f5ec3ee42c5ebba8acdc9c537da29c Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 30 Jan 2024 15:50:55 +0100 Subject: [PATCH 1139/1215] Polish "Add service connection for Docker Compose and Testcontainers Artemis" See gh-39311 --- .../jms/artemis/ArtemisConnectionDetails.java | 16 ++++++++++++++++ ...misDockerComposeConnectionDetailsFactory.java | 3 ++- .../connection/activemq/ArtemisEnvironment.java | 5 +++-- .../activemq/ArtemisEnvironmentTests.java | 4 ++-- .../connection/activemq/artemis-compose.yaml | 4 ++-- .../docs/asciidoc/features/docker-compose.adoc | 3 +++ 6 files changed, 28 insertions(+), 7 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionDetails.java index 94db35414404..dea123a3c186 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionDetails.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jms/artemis/ArtemisConnectionDetails.java @@ -26,12 +26,28 @@ */ public interface ArtemisConnectionDetails extends ConnectionDetails { + /** + * Artemis deployment mode, auto-detected by default. + * @return the Artemis deployment mode, auto-detected by default + */ ArtemisMode getMode(); + /** + * Artemis broker url. + * @return the Artemis broker url + */ String getBrokerUrl(); + /** + * Login user of the broker. + * @return the login user of the broker + */ String getUser(); + /** + * Login password of the broker. + * @return the login password of the broker + */ String getPassword(); } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisDockerComposeConnectionDetailsFactory.java index c2ceeb0c6436..4995ef24ca41 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisDockerComposeConnectionDetailsFactory.java @@ -27,6 +27,7 @@ * {@link ArtemisConnectionDetails} for an {@code artemis} service. * * @author Eddú Meléndez + * @author Moritz Halbritter */ class ArtemisDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { @@ -34,7 +35,7 @@ class ArtemisDockerComposeConnectionDetailsFactory private static final int ACTIVEMQ_PORT = 61616; protected ArtemisDockerComposeConnectionDetailsFactory() { - super("apache/activemq-classic"); + super("apache/activemq-artemis"); } @Override diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisEnvironment.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisEnvironment.java index 078f216c2589..a44dc69aa6da 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisEnvironment.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisEnvironment.java @@ -22,6 +22,7 @@ * Artemis environment details. * * @author Eddú Meléndez + * @author Moritz Halbritter */ class ArtemisEnvironment { @@ -30,8 +31,8 @@ class ArtemisEnvironment { private final String password; ArtemisEnvironment(Map env) { - this.user = env.get("ACTIVEMQ_CONNECTION_USER"); - this.password = env.get("ACTIVEMQ_CONNECTION_PASSWORD"); + this.user = env.get("ARTEMIS_USER"); + this.password = env.get("ARTEMIS_PASSWORD"); } String getUser() { diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisEnvironmentTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisEnvironmentTests.java index d3781014b0fa..76a980a249ae 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisEnvironmentTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ArtemisEnvironmentTests.java @@ -38,7 +38,7 @@ void getUserWhenHasNoActiveMqUser() { @Test void getUserWhenHasActiveMqUser() { - ArtemisEnvironment environment = new ArtemisEnvironment(Map.of("ACTIVEMQ_CONNECTION_USER", "me")); + ArtemisEnvironment environment = new ArtemisEnvironment(Map.of("ARTEMIS_USER", "me")); assertThat(environment.getUser()).isEqualTo("me"); } @@ -50,7 +50,7 @@ void getPasswordWhenHasNoActiveMqPassword() { @Test void getPasswordWhenHasActiveMqPassword() { - ArtemisEnvironment environment = new ArtemisEnvironment(Map.of("ACTIVEMQ_CONNECTION_PASSWORD", "secret")); + ArtemisEnvironment environment = new ArtemisEnvironment(Map.of("ARTEMIS_PASSWORD", "secret")); assertThat(environment.getPassword()).isEqualTo("secret"); } diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/activemq/artemis-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/activemq/artemis-compose.yaml index ffdd8c6b78ae..c9ea82fbadd4 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/activemq/artemis-compose.yaml +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/activemq/artemis-compose.yaml @@ -4,5 +4,5 @@ services: ports: - '61616' environment: - ACTIVEMQ_CONNECTION_USER: 'root' - ACTIVEMQ_CONNECTION_PASSWORD: 'secret' + ARTEMIS_USER: 'root' + ARTEMIS_PASSWORD: 'secret' diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc index 6b8c5688e232..4fa42013c0a0 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc @@ -72,6 +72,9 @@ The following service connections are currently supported: | `ActiveMQConnectionDetails` | Containers named "symptoma/activemq" +| `ArtemisConnectionDetails` +| Containers named "apache/activemq-artemis" + | `CassandraConnectionDetails` | Containers named "cassandra" From f5c9d34a5df880dc16192f08c76ff71a86b6b7fe Mon Sep 17 00:00:00 2001 From: Wzy19930507 <1208931582@qq.com> Date: Tue, 23 Jan 2024 11:23:07 +0800 Subject: [PATCH 1140/1215] Unify 'observation-enabled' property defaults Change spring.pulsar.listener.observation-enabled and spring.pulsar.template.observations-enabled to false See gh-39276 --- .../boot/autoconfigure/pulsar/PulsarProperties.java | 4 ++-- .../pulsar/PulsarAutoConfigurationTests.java | 4 ++-- .../autoconfigure/pulsar/PulsarPropertiesMapperTests.java | 4 ++-- .../boot/autoconfigure/pulsar/PulsarPropertiesTests.java | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java index 37fe9a3b4a1f..ef2dd6d2e58d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarProperties.java @@ -761,7 +761,7 @@ public static class Listener { * Whether to record observations for when the Observations API is available and * the client supports it. */ - private boolean observationEnabled = true; + private boolean observationEnabled; public SchemaType getSchemaType() { return this.schemaType; @@ -856,7 +856,7 @@ public static class Template { /** * Whether to record observations for when the Observations API is available. */ - private boolean observationsEnabled = true; + private boolean observationsEnabled; public boolean isObservationsEnabled() { return this.observationsEnabled; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java index 9cc201bbbdab..be1efb36cb2b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java @@ -304,7 +304,7 @@ void whenHasUseDefinedProducerInterceptorsInjectsBeansInCorrectOrder() { @Test void whenNoPropertiesEnablesObservation() { this.contextRunner.run((context) -> assertThat(context).getBean(PulsarTemplate.class) - .hasFieldOrPropertyWithValue("observationEnabled", true)); + .hasFieldOrPropertyWithValue("observationEnabled", false)); } @Test @@ -451,7 +451,7 @@ void whenHasCustomProperties() { void whenNoPropertiesEnablesObservation() { this.contextRunner .run((context) -> assertThat(context).getBean(ConcurrentPulsarListenerContainerFactory.class) - .hasFieldOrPropertyWithValue("containerProperties.observationEnabled", true)); + .hasFieldOrPropertyWithValue("containerProperties.observationEnabled", false)); } @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java index e26b6ac35ce0..2739529790f6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapperTests.java @@ -219,12 +219,12 @@ void customizeContainerProperties() { PulsarProperties properties = new PulsarProperties(); properties.getConsumer().getSubscription().setType(SubscriptionType.Shared); properties.getListener().setSchemaType(SchemaType.AVRO); - properties.getListener().setObservationEnabled(false); + properties.getListener().setObservationEnabled(true); PulsarContainerProperties containerProperties = new PulsarContainerProperties("my-topic-pattern"); new PulsarPropertiesMapper(properties).customizeContainerProperties(containerProperties); assertThat(containerProperties.getSubscriptionType()).isEqualTo(SubscriptionType.Shared); assertThat(containerProperties.getSchemaType()).isEqualTo(SchemaType.AVRO); - assertThat(containerProperties.isObservationEnabled()).isFalse(); + assertThat(containerProperties.isObservationEnabled()).isTrue(); } @Test diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesTests.java index 204676956152..bcf91314f465 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesTests.java @@ -354,10 +354,10 @@ class ListenerProperties { void bind() { Map map = new HashMap<>(); map.put("spring.pulsar.listener.schema-type", "avro"); - map.put("spring.pulsar.listener.observation-enabled", "false"); + map.put("spring.pulsar.listener.observation-enabled", "true"); PulsarProperties.Listener properties = bindPropeties(map).getListener(); assertThat(properties.getSchemaType()).isEqualTo(SchemaType.AVRO); - assertThat(properties.isObservationEnabled()).isFalse(); + assertThat(properties.isObservationEnabled()).isTrue(); } } @@ -389,9 +389,9 @@ class TemplateProperties { @Test void bind() { Map map = new HashMap<>(); - map.put("spring.pulsar.template.observations-enabled", "false"); + map.put("spring.pulsar.template.observations-enabled", "true"); PulsarProperties.Template properties = bindPropeties(map).getTemplate(); - assertThat(properties.isObservationsEnabled()).isFalse(); + assertThat(properties.isObservationsEnabled()).isTrue(); } } From c02dd14c66b4004d0e1db65855c364ac3e2f5a6b Mon Sep 17 00:00:00 2001 From: JonasG Date: Thu, 25 Jan 2024 15:56:08 +0100 Subject: [PATCH 1141/1215] Use generic wildcard for Pulsar beans See gh-39308 --- .../pulsar/PulsarAutoConfiguration.java | 5 +++-- .../pulsar/PulsarAutoConfigurationTests.java | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfiguration.java index c60565b5918f..fbe4f3fcc5a5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfiguration.java @@ -65,6 +65,7 @@ * @author Soby Chacko * @author Alexander Preuß * @author Phillip Webb + * @author Jonas Geiregat * @since 3.2.0 */ @AutoConfiguration @@ -131,7 +132,7 @@ PulsarTemplate pulsarTemplate(PulsarProducerFactory pulsarProducerFactory, @Bean @ConditionalOnMissingBean(PulsarConsumerFactory.class) - DefaultPulsarConsumerFactory pulsarConsumerFactory(PulsarClient pulsarClient, + DefaultPulsarConsumerFactory pulsarConsumerFactory(PulsarClient pulsarClient, ObjectProvider> customizersProvider) { List> customizers = new ArrayList<>(); customizers.add(this.propertiesMapper::customizeConsumerBuilder); @@ -150,7 +151,7 @@ private void applyConsumerBuilderCustomizers(List> @Bean @ConditionalOnMissingBean(name = "pulsarListenerContainerFactory") - ConcurrentPulsarListenerContainerFactory pulsarListenerContainerFactory( + ConcurrentPulsarListenerContainerFactory pulsarListenerContainerFactory( PulsarConsumerFactory pulsarConsumerFactory, SchemaResolver schemaResolver, TopicResolver topicResolver, Environment environment) { PulsarContainerProperties containerProperties = new PulsarContainerProperties(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java index 7e56f4129ead..b4691ecbfbc6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java @@ -34,6 +34,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.TestConfiguration; @@ -373,6 +374,14 @@ void whenHasUserDefinedCustomizersAppliesInCorrectOrder() { }); } + @Test + void injectsExpectedBeanWithExplicitGenericType() { + this.contextRunner.withBean(ExplicitGenericTypeConfig.class) + .run((context) -> assertThat(context).getBean(ExplicitGenericTypeConfig.class) + .hasFieldOrPropertyWithValue("consumerFactory", context.getBean(PulsarConsumerFactory.class)) + .hasFieldOrPropertyWithValue("containerFactory", context.getBean(ConcurrentPulsarListenerContainerFactory.class))); + } + @TestConfiguration(proxyBeanMethods = false) static class ConsumerBuilderCustomizersConfig { @@ -390,6 +399,16 @@ ConsumerBuilderCustomizer customizerBar() { } + static class ExplicitGenericTypeConfig { + @Autowired + PulsarConsumerFactory consumerFactory; + + @Autowired + ConcurrentPulsarListenerContainerFactory containerFactory; + + static class TestType {} + } + } @Nested From cee249197fbf8eaebbbe91f9f25dcba33ee8e920 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Wed, 31 Jan 2024 09:54:18 +0100 Subject: [PATCH 1142/1215] Polish "Use generic wildcard for Pulsar beans" See gh-39308 --- .../pulsar/PulsarAutoConfigurationTests.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java index b4691ecbfbc6..bade49b3b286 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/pulsar/PulsarAutoConfigurationTests.java @@ -377,9 +377,10 @@ void whenHasUserDefinedCustomizersAppliesInCorrectOrder() { @Test void injectsExpectedBeanWithExplicitGenericType() { this.contextRunner.withBean(ExplicitGenericTypeConfig.class) - .run((context) -> assertThat(context).getBean(ExplicitGenericTypeConfig.class) - .hasFieldOrPropertyWithValue("consumerFactory", context.getBean(PulsarConsumerFactory.class)) - .hasFieldOrPropertyWithValue("containerFactory", context.getBean(ConcurrentPulsarListenerContainerFactory.class))); + .run((context) -> assertThat(context).getBean(ExplicitGenericTypeConfig.class) + .hasFieldOrPropertyWithValue("consumerFactory", context.getBean(PulsarConsumerFactory.class)) + .hasFieldOrPropertyWithValue("containerFactory", + context.getBean(ConcurrentPulsarListenerContainerFactory.class))); } @TestConfiguration(proxyBeanMethods = false) @@ -400,13 +401,17 @@ ConsumerBuilderCustomizer customizerBar() { } static class ExplicitGenericTypeConfig { + @Autowired PulsarConsumerFactory consumerFactory; @Autowired ConcurrentPulsarListenerContainerFactory containerFactory; - static class TestType {} + static class TestType { + + } + } } From 0df3ec2ef37d0c382376839ceef5911ca116a4af Mon Sep 17 00:00:00 2001 From: Onur Kagan Ozcan Date: Wed, 31 Jan 2024 16:49:13 +0300 Subject: [PATCH 1143/1215] Remove System.out usage from Jetty GracefulShutdown See gh-39360 --- .../boot/web/embedded/jetty/GracefulShutdown.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/GracefulShutdown.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/GracefulShutdown.java index 8ccd5d26e56c..987583bc2128 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/GracefulShutdown.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/GracefulShutdown.java @@ -36,6 +36,7 @@ * Handles Jetty graceful shutdown. * * @author Andy Wilkinson + * @author Onur Kagan Ozcan */ final class GracefulShutdown { @@ -99,7 +100,6 @@ private void awaitShutdown(GracefulShutdownCallback callback) { while (this.shuttingDown && this.activeRequests.get() > 0) { sleep(100); } - System.out.println(this.activeRequests.get()); this.shuttingDown = false; long activeRequests = this.activeRequests.get(); if (activeRequests == 0) { From f3e7325064ee985252b5b1a2b357ba4c7e461f2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edd=C3=BA=20Mel=C3=A9ndez?= Date: Wed, 31 Jan 2024 12:35:13 -0500 Subject: [PATCH 1144/1215] Add service connection for Docker Compose and Testcontainers ActiveMQ See gh-39363 --- ...DockerComposeConnectionDetailsFactory.java | 79 ++++++++++++++++ .../activemq/ActiveMQClassicEnvironment.java | 46 ++++++++++ .../main/resources/META-INF/spring.factories | 1 + ...nectionDetailsFactoryIntegrationTests.java | 48 ++++++++++ .../ActiveMQClassicEnvironmentTests.java | 60 ++++++++++++ .../activemq/activemq-classic-compose.yaml | 8 ++ .../asciidoc/features/docker-compose.adoc | 2 +- .../src/docs/asciidoc/features/testing.adoc | 2 +- ...ssicContainerConnectionDetailsFactory.java | 66 ++++++++++++++ .../main/resources/META-INF/spring.factories | 1 + ...nectionDetailsFactoryIntegrationTests.java | 91 +++++++++++++++++++ .../testcontainers/DockerImageNames.java | 10 +- .../build.gradle | 1 + .../activemq/SampleActiveMqTests.java | 7 +- 14 files changed, 416 insertions(+), 6 deletions(-) create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQClassicDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQClassicEnvironment.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQClassicDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQClassicEnvironmentTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/activemq/activemq-classic-compose.yaml create mode 100644 spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/activemq/ActiveMQClassicContainerConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/activemq/ActiveMQClassicContainerConnectionDetailsFactoryIntegrationTests.java diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQClassicDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQClassicDockerComposeConnectionDetailsFactory.java new file mode 100644 index 000000000000..3442420bb62b --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQClassicDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,79 @@ +/* + * 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.docker.compose.service.connection.activemq; + +import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create + * {@link ActiveMQConnectionDetails} for an {@code activemq} service. + * + * @author Stephane Nicoll + * @author Eddú Meléndez + */ +class ActiveMQClassicDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + private static final int ACTIVEMQ_PORT = 61616; + + protected ActiveMQClassicDockerComposeConnectionDetailsFactory() { + super("apache/activemq-classic"); + } + + @Override + protected ActiveMQConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new ActiveMQDockerComposeConnectionDetails(source.getRunningService()); + } + + /** + * {@link ActiveMQConnectionDetails} backed by a {@code activemq} + * {@link RunningService}. + */ + static class ActiveMQDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements ActiveMQConnectionDetails { + + private final ActiveMQClassicEnvironment environment; + + private final String brokerUrl; + + protected ActiveMQDockerComposeConnectionDetails(RunningService service) { + super(service); + this.environment = new ActiveMQClassicEnvironment(service.env()); + this.brokerUrl = "tcp://" + service.host() + ":" + service.ports().get(ACTIVEMQ_PORT); + } + + @Override + public String getBrokerUrl() { + return this.brokerUrl; + } + + @Override + public String getUser() { + return this.environment.getUser(); + } + + @Override + public String getPassword() { + return this.environment.getPassword(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQClassicEnvironment.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQClassicEnvironment.java new file mode 100644 index 000000000000..dffede29238e --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQClassicEnvironment.java @@ -0,0 +1,46 @@ +/* + * 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.docker.compose.service.connection.activemq; + +import java.util.Map; + +/** + * ActiveMQ environment details. + * + * @author Stephane Nicoll + * @author Eddú Meléndez + */ +class ActiveMQClassicEnvironment { + + private final String user; + + private final String password; + + ActiveMQClassicEnvironment(Map env) { + this.user = env.get("ACTIVEMQ_CONNECTION_USER"); + this.password = env.get("ACTIVEMQ_CONNECTION_PASSWORD"); + } + + String getUser() { + return this.user; + } + + String getPassword() { + return this.password; + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories index fffc5722e0cc..d5b70a5f7cc3 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories @@ -5,6 +5,7 @@ org.springframework.boot.docker.compose.service.connection.DockerComposeServiceC # Connection Details Factories org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ +org.springframework.boot.docker.compose.service.connection.activemq.ActiveMQClassicDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.activemq.ActiveMQDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.activemq.ArtemisDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.cassandra.CassandraDockerComposeConnectionDetailsFactory,\ diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQClassicDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQClassicDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..34075742f7eb --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQClassicDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,48 @@ +/* + * 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.docker.compose.service.connection.activemq; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link ActiveMQClassicDockerComposeConnectionDetailsFactory}. + * + * @author Stephane Nicoll + * @author Eddú Meléndez + */ +class ActiveMQClassicDockerComposeConnectionDetailsFactoryIntegrationTests + extends AbstractDockerComposeIntegrationTests { + + ActiveMQClassicDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("activemq-classic-compose.yaml", DockerImageNames.activeMqClassic()); + } + + @Test + void runCreatesConnectionDetails() { + ActiveMQConnectionDetails connectionDetails = run(ActiveMQConnectionDetails.class); + assertThat(connectionDetails.getBrokerUrl()).isNotNull().startsWith("tcp://"); + assertThat(connectionDetails.getUser()).isEqualTo("root"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQClassicEnvironmentTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQClassicEnvironmentTests.java new file mode 100644 index 000000000000..8ef192c78877 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/activemq/ActiveMQClassicEnvironmentTests.java @@ -0,0 +1,60 @@ +/* + * 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.docker.compose.service.connection.activemq; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ActiveMQClassicEnvironment}. + * + * @author Stephane Nicoll + * @author Eddú Meléndez + */ +class ActiveMQClassicEnvironmentTests { + + @Test + void getUserWhenHasNoActiveMqUser() { + ActiveMQClassicEnvironment environment = new ActiveMQClassicEnvironment(Collections.emptyMap()); + assertThat(environment.getUser()).isNull(); + } + + @Test + void getUserWhenHasActiveMqUser() { + ActiveMQClassicEnvironment environment = new ActiveMQClassicEnvironment( + Map.of("ACTIVEMQ_CONNECTION_USER", "me")); + assertThat(environment.getUser()).isEqualTo("me"); + } + + @Test + void getPasswordWhenHasNoActiveMqPassword() { + ActiveMQClassicEnvironment environment = new ActiveMQClassicEnvironment(Collections.emptyMap()); + assertThat(environment.getPassword()).isNull(); + } + + @Test + void getPasswordWhenHasActiveMqPassword() { + ActiveMQClassicEnvironment environment = new ActiveMQClassicEnvironment( + Map.of("ACTIVEMQ_CONNECTION_PASSWORD", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/activemq/activemq-classic-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/activemq/activemq-classic-compose.yaml new file mode 100644 index 000000000000..2bdef98e5aa7 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/activemq/activemq-classic-compose.yaml @@ -0,0 +1,8 @@ +services: + activemq: + image: '{imageName}' + ports: + - '61616' + environment: + ACTIVEMQ_CONNECTION_USER: 'root' + ACTIVEMQ_CONNECTION_PASSWORD: 'secret' diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc index 4fa42013c0a0..9b6f4688d199 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc @@ -70,7 +70,7 @@ The following service connections are currently supported: | Connection Details | Matched on | `ActiveMQConnectionDetails` -| Containers named "symptoma/activemq" +| Containers named "symptoma/activemq", "apache/activemq-classic" | `ArtemisConnectionDetails` | Containers named "apache/activemq-artemis" diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc index c86faa72b03c..19fab2a23691 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc @@ -981,7 +981,7 @@ The following service connection factories are provided in the `spring-boot-test | Connection Details | Matched on | `ActiveMQConnectionDetails` -| Containers named "symptoma/activemq" +| Containers named "symptoma/activemq" or `ActiveMQContainer` | `ArtemisConnectionDetails` | Containers of type `ArtemisContainer` diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/activemq/ActiveMQClassicContainerConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/activemq/ActiveMQClassicContainerConnectionDetailsFactory.java new file mode 100644 index 000000000000..8c9904bc5d09 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/activemq/ActiveMQClassicContainerConnectionDetailsFactory.java @@ -0,0 +1,66 @@ +/* + * 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.testcontainers.service.connection.activemq; + +import org.testcontainers.activemq.ActiveMQContainer; + +import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQConnectionDetails; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; + +/** + * {@link ContainerConnectionDetailsFactory} to create {@link ActiveMQConnectionDetails} * + * from a {@link ServiceConnection @ServiceConnection}-annotated + * {@link ActiveMQContainer}. + * + * @author Eddú Meléndez + */ +class ActiveMQClassicContainerConnectionDetailsFactory + extends ContainerConnectionDetailsFactory { + + @Override + protected ActiveMQConnectionDetails getContainerConnectionDetails( + ContainerConnectionSource source) { + return new ActiveMQContainerConnectionDetails(source); + } + + private static final class ActiveMQContainerConnectionDetails extends ContainerConnectionDetails + implements ActiveMQConnectionDetails { + + private ActiveMQContainerConnectionDetails(ContainerConnectionSource source) { + super(source); + } + + @Override + public String getBrokerUrl() { + return getContainer().getBrokerUrl(); + } + + @Override + public String getUser() { + return getContainer().getUser(); + } + + @Override + public String getPassword() { + return getContainer().getPassword(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories index 71721a3f32d0..ac9e6aeae26b 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories @@ -8,6 +8,7 @@ org.springframework.boot.testcontainers.service.connection.ServiceConnectionCont # Connection Details Factories org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ +org.springframework.boot.testcontainers.service.connection.activemq.ActiveMQClassicContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.activemq.ActiveMQContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.activemq.ArtemisContainerConnectionDetailsFactory,\ org.springframework.boot.testcontainers.service.connection.amqp.RabbitContainerConnectionDetailsFactory,\ diff --git a/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/activemq/ActiveMQClassicContainerConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/activemq/ActiveMQClassicContainerConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..f203d6ef6346 --- /dev/null +++ b/spring-boot-project/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/service/connection/activemq/ActiveMQClassicContainerConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,91 @@ +/* + * 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.testcontainers.service.connection.activemq; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.testcontainers.activemq.ActiveMQContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration; +import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.jms.core.JmsMessagingTemplate; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ActiveMQClassicContainerConnectionDetailsFactory}. + * + * @author Eddú Meléndez + */ +@SpringJUnitConfig +@Testcontainers(disabledWithoutDocker = true) +class ActiveMQClassicContainerConnectionDetailsFactoryIntegrationTests { + + @Container + @ServiceConnection + static final ActiveMQContainer activemq = new ActiveMQContainer(DockerImageNames.activeMqClassic()); + + @Autowired + private JmsMessagingTemplate jmsTemplate; + + @Autowired + private TestListener listener; + + @Test + void connectionCanBeMadeToActiveMQContainer() { + this.jmsTemplate.convertAndSend("sample.queue", "message"); + Awaitility.waitAtMost(Duration.ofMinutes(1)) + .untilAsserted(() -> assertThat(this.listener.messages).containsExactly("message")); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration({ ActiveMQAutoConfiguration.class, JmsAutoConfiguration.class }) + static class TestConfiguration { + + @Bean + TestListener testListener() { + return new TestListener(); + } + + } + + static class TestListener { + + private final List messages = new ArrayList<>(); + + @JmsListener(destination = "sample.queue") + void processMessage(String message) { + this.messages.add(message); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java index 7f9d598095c4..84cacff6b4f8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java @@ -28,7 +28,7 @@ */ public final class DockerImageNames { - private static final String ACTIVE_MQ_VERSION = "5.18.0"; + private static final String ACTIVE_MQ_VERSION = "5.18.3"; private static final String ARTEMIS_VERSION = "2.31.2"; @@ -83,6 +83,14 @@ public static DockerImageName activeMq() { return DockerImageName.parse("symptoma/activemq").withTag(ACTIVE_MQ_VERSION); } + /** + * Return a {@link DockerImageName} suitable for running ActiveMQ. + * @return a docker image name for running activeMq + */ + public static DockerImageName activeMqClassic() { + return DockerImageName.parse("apache/activemq-classic").withTag(ACTIVE_MQ_VERSION); + } + /** * Return a {@link DockerImageName} suitable for running Artemis. * @return a docker image name for running artemis diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-activemq/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-activemq/build.gradle index 963f63814ac7..ecdad02c7f28 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-activemq/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-activemq/build.gradle @@ -10,6 +10,7 @@ dependencies { testImplementation("org.awaitility:awaitility") testImplementation("org.testcontainers:junit-jupiter") + testImplementation("org.testcontainers:activemq") testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) testImplementation(project(":spring-boot-project:spring-boot-testcontainers")) testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-activemq/src/test/java/smoketest/activemq/SampleActiveMqTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-activemq/src/test/java/smoketest/activemq/SampleActiveMqTests.java index 9d68eda78d2b..5a584c02faff 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-activemq/src/test/java/smoketest/activemq/SampleActiveMqTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-activemq/src/test/java/smoketest/activemq/SampleActiveMqTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -21,6 +21,7 @@ import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.testcontainers.activemq.ActiveMQContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -29,7 +30,7 @@ import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.springframework.boot.testsupport.testcontainers.ActiveMQContainer; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; import static org.assertj.core.api.Assertions.assertThat; @@ -46,7 +47,7 @@ class SampleActiveMqTests { @Container @ServiceConnection - private static final ActiveMQContainer container = new ActiveMQContainer(); + private static final ActiveMQContainer container = new ActiveMQContainer(DockerImageNames.activeMqClassic()); @Autowired private Producer producer; From 8aef70749f3839b3942c49bb54b8c8a827de0c2d Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Mon, 5 Feb 2024 08:37:21 +0100 Subject: [PATCH 1145/1215] Upgrade to Native Build Tools Plugin 0.10.0 Closes gh-39398 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d28745da06d8..467a13983f52 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ jacksonVersion=2.16.1 junitJupiterVersion=5.10.1 kotlinVersion=1.9.22 mavenVersion=3.9.4 -nativeBuildToolsVersion=0.9.28 +nativeBuildToolsVersion=0.10.0 springFrameworkVersion=6.1.3 tomcatVersion=10.1.18 From 07ee7254a6905f6c346ea00592c0147a210b9c74 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Mon, 5 Feb 2024 08:42:30 +0100 Subject: [PATCH 1146/1215] Align to Native Build Tools metadata repository default Closes gh-39068 --- .../spring-boot-starter-parent/build.gradle | 6 ------ .../boot/gradle/plugin/NativeImagePluginAction.java | 11 +---------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle index 32d88db76916..14974ef32076 100644 --- a/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-parent/build.gradle @@ -269,9 +269,6 @@ publishing.publications.withType(MavenPublication) { delegate.artifactId('native-maven-plugin') configuration { delegate.classesDirectory('${project.build.outputDirectory}') - metadataRepository { - delegate.enabled('true') - } delegate.requiredVersion('22.3') } executions { @@ -315,9 +312,6 @@ publishing.publications.withType(MavenPublication) { delegate.artifactId('native-maven-plugin') configuration { delegate.classesDirectory('${project.build.outputDirectory}') - metadataRepository { - delegate.enabled('true') - } delegate.requiredVersion('22.3') } executions { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java index c4689c80b242..c41b71f70b2a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/NativeImagePluginAction.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -22,14 +22,12 @@ import org.graalvm.buildtools.gradle.NativeImagePlugin; import org.graalvm.buildtools.gradle.dsl.GraalVMExtension; -import org.graalvm.buildtools.gradle.dsl.GraalVMReachabilityMetadataRepositoryExtension; import org.gradle.api.Action; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.artifacts.Configuration; import org.gradle.api.file.FileCollection; import org.gradle.api.java.archives.Manifest; -import org.gradle.api.plugins.ExtensionAware; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.tasks.SourceSetContainer; @@ -60,7 +58,6 @@ public void execute(Project project) { GraalVMExtension graalVmExtension = configureGraalVmExtension(project); configureMainNativeBinaryClasspath(project, sourceSets, graalVmExtension); configureTestNativeBinaryClasspath(sourceSets, graalVmExtension); - configureGraalVmReachabilityExtension(graalVmExtension); copyReachabilityMetadataToBootJar(project); configureBootBuildImageToProduceANativeImage(project); configureJarManifestNativeAttribute(project); @@ -99,12 +96,6 @@ private GraalVMExtension configureGraalVmExtension(Project project) { return extension; } - private void configureGraalVmReachabilityExtension(GraalVMExtension graalVmExtension) { - GraalVMReachabilityMetadataRepositoryExtension extension = ((ExtensionAware) graalVmExtension).getExtensions() - .getByType(GraalVMReachabilityMetadataRepositoryExtension.class); - extension.getEnabled().set(true); - } - private void copyReachabilityMetadataToBootJar(Project project) { project.getTasks() .named(SpringBootPlugin.BOOT_JAR_TASK_NAME, BootJar.class) From be851aaee0a99ea9cfb64698b0831b267883f963 Mon Sep 17 00:00:00 2001 From: Ramil Sayetov Date: Tue, 6 Feb 2024 13:31:36 +0300 Subject: [PATCH 1147/1215] Fix awaitility link See gh-39415 --- .../spring-boot-docs/src/docs/asciidoc/features/testing.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc index 24bcf0d1ee44..7e24391e1375 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc @@ -41,7 +41,7 @@ The `spring-boot-starter-test` "`Starter`" (in the `test` `scope`) contains the * https://site.mockito.org/[Mockito]: A Java mocking framework. * https://github.com/skyscreamer/JSONassert[JSONassert]: An assertion library for JSON. * https://github.com/jayway/JsonPath[JsonPath]: XPath for JSON. -* https://https://github.com/awaitility/awaitility[Awaitility]: A library for testing asynchronous systems. +* https://github.com/awaitility/awaitility[Awaitility]: A library for testing asynchronous systems. We generally find these common libraries to be useful when writing tests. If these libraries do not suit your needs, you can add additional test dependencies of your own. From 41ed4d6cf4c5e316f236c99ce59cf7ddfa48ee89 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Sat, 3 Feb 2024 00:29:45 -0600 Subject: [PATCH 1148/1215] Remove use of Pulsar ObjectMapperFactory This commit removes the use of the Pulsar ObjectMapperFactory when converting the authentication config props map to a JSON string. The Pulsar factory operates on a shaded returned value of Jackson ObjectMapper which may not exist when users are using the non-shaded version of the Pulsar client lib. See https://github.com/spring-projects/spring-pulsar/issues/562 See gh-39389 --- .../boot/autoconfigure/pulsar/PulsarPropertiesMapper.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java index ed9411512eb0..7b76c555022b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/pulsar/PulsarPropertiesMapper.java @@ -23,6 +23,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.stream.Collectors; import org.apache.pulsar.client.admin.PulsarAdminBuilder; import org.apache.pulsar.client.api.ClientBuilder; @@ -30,7 +31,6 @@ import org.apache.pulsar.client.api.ProducerBuilder; import org.apache.pulsar.client.api.PulsarClientException.UnsupportedAuthenticationException; import org.apache.pulsar.client.api.ReaderBuilder; -import org.apache.pulsar.common.util.ObjectMapperFactory; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.pulsar.listener.PulsarContainerProperties; @@ -87,7 +87,10 @@ private void customizeAuthentication(AuthenticationConsumer authentication, private String getAuthenticationParamsJson(Map params) { Map sortedParams = new TreeMap<>(params); try { - return ObjectMapperFactory.create().writeValueAsString(sortedParams); + return sortedParams.entrySet() + .stream() + .map((e) -> "\"%s\":\"%s\"".formatted(e.getKey(), e.getValue())) + .collect(Collectors.joining(",", "{", "}")); } catch (Exception ex) { throw new IllegalStateException("Could not convert auth parameters to encoded string", ex); From 8e75817d6a15f26807dc9d4bc73e8d37871c4dde Mon Sep 17 00:00:00 2001 From: BenchmarkingBuffalo <46448799+benchmarkingbuffalo@users.noreply.github.com> Date: Sun, 4 Feb 2024 18:20:05 +0100 Subject: [PATCH 1149/1215] Add nameIdFormat to Properties Add the new property nameIdFormat to the Saml2RelyingPartyProperties and the corresponding mapping to the Saml2RelyingPartyRegistrationConfiguration. See gh-39395 --- .../saml2/Saml2RelyingPartyProperties.java | 15 +++++++++++++-- ...aml2RelyingPartyRegistrationConfiguration.java | 2 ++ .../saml2/Saml2RelyingPartyPropertiesTests.java | 8 ++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyProperties.java index 8898587a46b1..ca747354f17b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyProperties.java @@ -31,6 +31,7 @@ * @author Madhura Bhave * @author Phillip Webb * @author Moritz Halbritter + * @author Lasse Wulff * @since 2.2.0 */ @ConfigurationProperties("spring.security.saml2.relyingparty") @@ -72,6 +73,8 @@ public static class Registration { */ private final AssertingParty assertingparty = new AssertingParty(); + private String nameIdFormat; + public String getEntityId() { return this.entityId; } @@ -92,12 +95,20 @@ public Decryption getDecryption() { return this.decryption; } + public Singlelogout getSinglelogout() { + return this.singlelogout; + } + public AssertingParty getAssertingparty() { return this.assertingparty; } - public Singlelogout getSinglelogout() { - return this.singlelogout; + public String getNameIdFormat() { + return this.nameIdFormat; + } + + public void setNameIdFormat(String nameIdFormat) { + this.nameIdFormat = nameIdFormat; } public static class Acs { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyRegistrationConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyRegistrationConfiguration.java index 830077fae5b9..7dee3c397dd8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyRegistrationConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyRegistrationConfiguration.java @@ -56,6 +56,7 @@ * @author Phillip Webb * @author Moritz Halbritter * @author Lasse Lindqvist + * @author Lasse Wulff */ @Configuration(proxyBeanMethods = false) @Conditional(RegistrationConfiguredCondition.class) @@ -104,6 +105,7 @@ private RelyingPartyRegistration asRegistration(String id, Registration properti builder.singleLogoutServiceResponseLocation(properties.getSinglelogout().getResponseUrl()); builder.singleLogoutServiceBinding(properties.getSinglelogout().getBinding()); builder.entityId(properties.getEntityId()); + builder.nameIdFormat(properties.getNameIdFormat()); RelyingPartyRegistration registration = builder.build(); boolean signRequest = registration.getAssertingPartyDetails().getWantAuthnRequestsSigned(); validateSigningCredentials(properties, signRequest); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyPropertiesTests.java index ff0e8106209f..ab959705a866 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/saml2/Saml2RelyingPartyPropertiesTests.java @@ -34,6 +34,7 @@ * Tests for {@link Saml2RelyingPartyProperties}. * * @author Madhura Bhave + * @author Lasse Wulff */ class Saml2RelyingPartyPropertiesTests { @@ -102,6 +103,13 @@ void customizeSsoSignRequestsIsNullByDefault() { .getSignRequest()).isNull(); } + @Test + void customizeNameIdFormat() { + bind("spring.security.saml2.relyingparty.registration.simplesamlphp.name-id-format", "sampleNameIdFormat"); + assertThat(this.properties.getRegistration().get("simplesamlphp").getNameIdFormat()) + .isEqualTo("sampleNameIdFormat"); + } + private void bind(String name, String value) { bind(Collections.singletonMap(name, value)); } From ff8089de067893b55c57b0fd72fd7e8c74c31d91 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Mon, 5 Feb 2024 13:14:04 -0600 Subject: [PATCH 1150/1215] Update to Pulsar 3.2.0 and use Pulsar BOM This commit updates Pulsar to 3.2.0 and leverages the newly added Pulsar BOM in order to ease dependency management. See gh-39408 --- .../spring-boot-dependencies/build.gradle | 78 +------------------ .../testcontainers/DockerImageNames.java | 2 +- 2 files changed, 4 insertions(+), 76 deletions(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e222707924ed..34a40aa769b9 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1138,82 +1138,10 @@ bom { ] } } - library("Pulsar", "3.1.2") { + library("Pulsar", "3.2.0") { group("org.apache.pulsar") { - modules = [ - "bouncy-castle-bc", - "bouncy-castle-bcfips", - "pulsar-client-1x-base", - "pulsar-client-1x", - "pulsar-client-2x-shaded", - "pulsar-client-admin-api", - "pulsar-client-admin-original", - "pulsar-client-admin", - "pulsar-client-all", - "pulsar-client-api", - "pulsar-client-auth-athenz", - "pulsar-client-auth-sasl", - "pulsar-client-messagecrypto-bc", - "pulsar-client-original", - "pulsar-client-tools-api", - "pulsar-client-tools", - "pulsar-client", - "pulsar-common", - "pulsar-config-validation", - "pulsar-functions-api", - "pulsar-functions-proto", - "pulsar-functions-utils", - "pulsar-io-aerospike", - "pulsar-io-alluxio", - "pulsar-io-aws", - "pulsar-io-batch-data-generator", - "pulsar-io-batch-discovery-triggerers", - "pulsar-io-canal", - "pulsar-io-cassandra", - "pulsar-io-common", - "pulsar-io-core", - "pulsar-io-data-generator", - "pulsar-io-debezium-core", - "pulsar-io-debezium-mongodb", - "pulsar-io-debezium-mssql", - "pulsar-io-debezium-mysql", - "pulsar-io-debezium-oracle", - "pulsar-io-debezium-postgres", - "pulsar-io-debezium", - "pulsar-io-dynamodb", - "pulsar-io-elastic-search", - "pulsar-io-file", - "pulsar-io-flume", - "pulsar-io-hbase", - "pulsar-io-hdfs2", - "pulsar-io-hdfs3", - "pulsar-io-http", - "pulsar-io-influxdb", - "pulsar-io-jdbc-clickhouse", - "pulsar-io-jdbc-core", - "pulsar-io-jdbc-mariadb", - "pulsar-io-jdbc-openmldb", - "pulsar-io-jdbc-postgres", - "pulsar-io-jdbc-sqlite", - "pulsar-io-jdbc", - "pulsar-io-kafka-connect-adaptor-nar", - "pulsar-io-kafka-connect-adaptor", - "pulsar-io-kafka", - "pulsar-io-kinesis", - "pulsar-io-mongo", - "pulsar-io-netty", - "pulsar-io-nsq", - "pulsar-io-rabbitmq", - "pulsar-io-redis", - "pulsar-io-solr", - "pulsar-io-twitter", - "pulsar-io", - "pulsar-metadata", - "pulsar-presto-connector-original", - "pulsar-presto-connector", - "pulsar-sql", - "pulsar-transaction-common", - "pulsar-websocket" + imports = [ + "pulsar-bom" ] } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java index 84cacff6b4f8..da27927f0704 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java @@ -58,7 +58,7 @@ public final class DockerImageNames { private static final String OPENTELEMETRY_VERSION = "0.75.0"; - private static final String PULSAR_VERSION = "3.1.0"; + private static final String PULSAR_VERSION = "3.2.0"; private static final String POSTGRESQL_VERSION = "14.0"; From 5ae533a00d853ed4b9a7683fa582f99cf01c53c7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 6 Feb 2024 15:08:15 +0000 Subject: [PATCH 1151/1215] Minimize scope of version management for commons-compress See gh-39368 --- gradle.properties | 1 + spring-boot-project/spring-boot-parent/build.gradle | 7 ------- .../spring-boot-buildpack-platform/build.gradle | 13 ++++++++++++- .../spring-boot-gradle-plugin/build.gradle | 13 ++++++++++++- .../spring-boot-gradle-test-support/build.gradle | 2 +- .../spring-boot-loader-tools/build.gradle | 8 +++++++- .../boot/image/assertions/ImageAssert.java | 8 ++++---- 7 files changed, 37 insertions(+), 15 deletions(-) diff --git a/gradle.properties b/gradle.properties index 22d7baf4a925..28236114ec4a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,6 +6,7 @@ org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 assertjVersion=3.24.2 commonsCodecVersion=1.16.0 +commonsCompressVersion=1.21 hamcrestVersion=2.2 jacksonVersion=2.15.3 junitJupiterVersion=5.10.1 diff --git a/spring-boot-project/spring-boot-parent/build.gradle b/spring-boot-project/spring-boot-parent/build.gradle index 52b0104b582e..8a2bf315f0ee 100644 --- a/spring-boot-project/spring-boot-parent/build.gradle +++ b/spring-boot-project/spring-boot-parent/build.gradle @@ -34,13 +34,6 @@ bom { ] } } - library("Commons Compress", "1.21") { - group("org.apache.commons") { - modules = [ - "commons-compress" - ] - } - } library("Commons FileUpload", "1.5") { group("commons-fileupload") { modules = [ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/build.gradle index b99a4c61c3e4..f3886f7b8471 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/build.gradle @@ -19,6 +19,17 @@ configurations.all { if (dependency.requested.group.equals("org.springframework")) { dependency.useVersion("6.0.10") } + // We manage the version of commons-compress here rather than + // in spring-boot-parent to minimize conflicts with Testcontainers + if (dependency.requested.group.equals("org.apache.commons") + && dependency.requested.name.equals("commons-compress")) { + dependency.useVersion("$commonsCompressVersion") + } + // Downgrade Testcontainers for compatibility with the managed + // version of Commons Compress. + if (dependency.requested.group.equals("org.testcontainers")) { + dependency.useVersion("1.19.3") + } } } } @@ -27,7 +38,7 @@ dependencies { api("com.fasterxml.jackson.core:jackson-databind") api("com.fasterxml.jackson.module:jackson-module-parameter-names") api("net.java.dev.jna:jna-platform") - api("org.apache.commons:commons-compress") + api("org.apache.commons:commons-compress:$commonsCompressVersion") api("org.apache.httpcomponents.client5:httpclient5") api("org.springframework:spring-core") api("org.tomlj:tomlj:1.0.0") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle index 35cd303513d7..36a2c3b8f6e1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle @@ -28,6 +28,17 @@ configurations { if (dependency.requested.group.equals("org.springframework")) { dependency.useVersion("6.0.10") } + // We manage the version of commons-compress here rather than + // in spring-boot-parent to minimize conflicts with Testcontainers + if (dependency.requested.group.equals("org.apache.commons") + && dependency.requested.name.equals("commons-compress")) { + dependency.useVersion("$commonsCompressVersion") + } + // Downgrade Testcontainers for compatibility with the managed + // version of Commons Compress. + if (dependency.requested.group.equals("org.testcontainers")) { + dependency.useVersion("1.19.3") + } } } } @@ -39,7 +50,7 @@ dependencies { implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-buildpack-platform")) implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-tools")) implementation("io.spring.gradle:dependency-management-plugin") - implementation("org.apache.commons:commons-compress") + implementation("org.apache.commons:commons-compress:$commonsCompressVersion") implementation("org.springframework:spring-core") optional("org.graalvm.buildtools:native-gradle-plugin") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-test-support/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-test-support/build.gradle index d3df269b1b2c..c71fca96a3b8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-test-support/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-test-support/build.gradle @@ -17,7 +17,7 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlinVersion") implementation("org.jetbrains.kotlin:kotlin-compiler-runner:$kotlinVersion") implementation("org.jetbrains.kotlin:kotlin-daemon-client:$kotlinVersion") - implementation("org.apache.commons:commons-compress") + implementation("org.apache.commons:commons-compress:$commonsCompressVersion") implementation("org.assertj:assertj-core") } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle index f7968f659d51..7c2053406537 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/build.gradle @@ -29,13 +29,19 @@ configurations { if (dependency.requested.group.equals("org.springframework")) { dependency.useVersion("6.0.10") } + // We manage the version of commons-compress here rather than + // in spring-boot-parent to minimize conflicts with Testcontainers + if (dependency.requested.group.equals("org.apache.commons") + && dependency.requested.name.equals("commons-compress")) { + dependency.useVersion("$commonsCompressVersion") + } } } } } dependencies { - api("org.apache.commons:commons-compress") + api("org.apache.commons:commons-compress:$commonsCompressVersion") api("org.springframework:spring-core") compileOnly("ch.qos.logback:logback-classic") diff --git a/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/assertions/ImageAssert.java b/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/assertions/ImageAssert.java index ce326c1702ba..fc5f9509412b 100644 --- a/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/assertions/ImageAssert.java +++ b/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/assertions/ImageAssert.java @@ -80,12 +80,12 @@ public ListAssert entries() { this.actual.writeTo(out); try (TarArchiveInputStream in = new TarArchiveInputStream( new ByteArrayInputStream(out.toByteArray()))) { - TarArchiveEntry entry = in.getNextTarEntry(); + TarArchiveEntry entry = in.getNextEntry(); while (entry != null) { if (!entry.isDirectory()) { entryNames.add(entry.getName().replaceFirst("^/workspace/", "")); } - entry = in.getNextTarEntry(); + entry = in.getNextEntry(); } } } @@ -101,7 +101,7 @@ public void jsonEntry(String name, Consumer assertConsumer) { this.actual.writeTo(out); try (TarArchiveInputStream in = new TarArchiveInputStream( new ByteArrayInputStream(out.toByteArray()))) { - TarArchiveEntry entry = in.getNextTarEntry(); + TarArchiveEntry entry = in.getNextEntry(); while (entry != null) { if (entry.getName().equals(name)) { ByteArrayOutputStream entryOut = new ByteArrayOutputStream(); @@ -109,7 +109,7 @@ public void jsonEntry(String name, Consumer assertConsumer) { assertConsumer.accept(new JsonContentAssert(LayerContentAssert.class, entryOut.toString())); return; } - entry = in.getNextTarEntry(); + entry = in.getNextEntry(); } } failWithMessage("Expected JSON entry '%s' in layer with digest '%s'", name, this.actual.getId()); From 038ea2cb9a5ca22c61355e025c96587bc7890685 Mon Sep 17 00:00:00 2001 From: BenchmarkingBuffalo <46448799+benchmarkingbuffalo@users.noreply.github.com> Date: Wed, 7 Feb 2024 10:39:49 +0100 Subject: [PATCH 1152/1215] Add possibility for custom MimeMappings Add a new property called 'mime-mappings' under the 'server' property. This is a key-value-map, which is added to the default MimeMappings. See gh-39430 --- .../autoconfigure/web/ServerProperties.java | 12 ++++++++++++ .../ServletWebServerFactoryCustomizer.java | 2 ++ .../web/ServerPropertiesTests.java | 18 ++++++++++++++++++ ...ServletWebServerFactoryCustomizerTests.java | 8 ++++++++ 4 files changed, 40 insertions(+) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index d0be8c72f65d..7704eb283a41 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -37,6 +37,7 @@ import org.springframework.boot.web.server.Compression; import org.springframework.boot.web.server.Cookie; import org.springframework.boot.web.server.Http2; +import org.springframework.boot.web.server.MimeMappings; import org.springframework.boot.web.server.Shutdown; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.servlet.server.Encoding; @@ -71,6 +72,7 @@ * @author Parviz Rozikov * @author Florian Storz * @author Michael Weidmann + * @author Lasse Wulff * @since 1.0.0 */ @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true) @@ -115,6 +117,8 @@ public class ServerProperties { @NestedConfigurationProperty private final Compression compression = new Compression(); + private final MimeMappings mimeMappings = MimeMappings.lazyCopy(MimeMappings.DEFAULT); + @NestedConfigurationProperty private final Http2 http2 = new Http2(); @@ -186,6 +190,14 @@ public Compression getCompression() { return this.compression; } + public MimeMappings getMimeMappings() { + return this.mimeMappings; + } + + public void setMimeMappings(Map customMappings) { + customMappings.forEach(this.mimeMappings::add); + } + public Http2 getHttp2() { return this.http2; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizer.java index 70706f939749..66c68a2d4503 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizer.java @@ -38,6 +38,7 @@ * @author Olivier Lamy * @author Yunkun Huang * @author Scott Frederick + * @author Lasse Wulff * @since 2.0.0 */ public class ServletWebServerFactoryCustomizer @@ -94,6 +95,7 @@ public void customize(ConfigurableServletWebServerFactory factory) { map.from(() -> this.cookieSameSiteSuppliers) .whenNot(CollectionUtils::isEmpty) .to(factory::setCookieSameSiteSuppliers); + map.from(this.serverProperties::getMimeMappings).to(factory::setMimeMappings); this.webListenerRegistrars.forEach((registrar) -> registrar.register(factory)); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java index ff8b377d2d95..c5f580ff7ad3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java @@ -46,6 +46,8 @@ import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; import org.springframework.boot.web.embedded.jetty.JettyWebServer; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.server.MimeMappings; +import org.springframework.boot.web.server.MimeMappings.Mapping; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.unit.DataSize; @@ -66,6 +68,7 @@ * @author Rafiullah Hamedy * @author Chris Bono * @author Parviz Rozikov + * @author Lasse Wulff */ @DirtiesUrlFactories class ServerPropertiesTests { @@ -182,6 +185,21 @@ void testContextPathWithLeadingAndTrailingWhitespaceAndContextWithSpace() { assertThat(this.properties.getServlet().getContextPath()).isEqualTo("/assets /copy"); } + @Test + void testDefaultMimeMapping() { + assertThat(this.properties.getMimeMappings()) + .containsExactly(MimeMappings.DEFAULT.getAll().toArray(new Mapping[0])); + } + + @Test + void testCustomizedMimeMapping() { + MimeMappings expectedMappings = MimeMappings.lazyCopy(MimeMappings.DEFAULT); + expectedMappings.add("mjs", "text/javascript"); + bind("server.mime-mappings.mjs", "text/javascript"); + assertThat(this.properties.getMimeMappings()) + .containsExactly(expectedMappings.getAll().toArray(new Mapping[0])); + } + @Test void testCustomizeUriEncoding() { bind("server.tomcat.uri-encoding", "US-ASCII"); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizerTests.java index c428ff17819f..cc77a1a8c2c8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryCustomizerTests.java @@ -45,6 +45,7 @@ * * @author Brian Clozel * @author Yunkun Huang + * @author Lasse Wulff */ class ServletWebServerFactoryCustomizerTests { @@ -72,6 +73,13 @@ void testCustomizeDisplayName() { then(factory).should().setDisplayName("TestName"); } + @Test + void testCustomMimeMappings() { + ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class); + this.customizer.customize(factory); + then(factory).should().setMimeMappings(this.properties.getMimeMappings()); + } + @Test void testCustomizeDefaultServlet() { ConfigurableServletWebServerFactory factory = mock(ConfigurableServletWebServerFactory.class); From 0a11cdcc338ac26d21d85e0fdac63a56a6b67010 Mon Sep 17 00:00:00 2001 From: BenchmarkingBuffalo <46448799+benchmarkingbuffalo@users.noreply.github.com> Date: Thu, 8 Feb 2024 15:03:32 +0100 Subject: [PATCH 1153/1215] Add customizer callback for WebHttpHandlerBuilder Add a new interface for customizing the WebHttpHandlerBuilder before the HttpHandler is built from it. See gh-39467 --- .../HttpHandlerAutoConfiguration.java | 10 ++++-- .../WebHttpHandlerBuilderCustomizer.java | 36 +++++++++++++++++++ .../HttpHandlerAutoConfigurationTests.java | 36 ++++++++++++++++++- .../src/docs/asciidoc/web/reactive.adoc | 2 ++ 4 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebHttpHandlerBuilderCustomizer.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/HttpHandlerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/HttpHandlerAutoConfiguration.java index 7a6c32c0933f..b59914875f79 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/HttpHandlerAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/HttpHandlerAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -41,6 +41,7 @@ * * @author Brian Clozel * @author Stephane Nicoll + * @author Lasse Wulff * @since 2.0.0 */ @AutoConfiguration(after = { WebFluxAutoConfiguration.class }) @@ -60,8 +61,11 @@ public AnnotationConfig(ApplicationContext applicationContext) { } @Bean - public HttpHandler httpHandler(ObjectProvider propsProvider) { - HttpHandler httpHandler = WebHttpHandlerBuilder.applicationContext(this.applicationContext).build(); + public HttpHandler httpHandler(ObjectProvider propsProvider, + ObjectProvider handlerBuilderCustomizers) { + WebHttpHandlerBuilder handlerBuilder = WebHttpHandlerBuilder.applicationContext(this.applicationContext); + handlerBuilderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(handlerBuilder)); + HttpHandler httpHandler = handlerBuilder.build(); WebFluxProperties properties = propsProvider.getIfAvailable(); if (properties != null && StringUtils.hasText(properties.getBasePath())) { Map handlersMap = Collections.singletonMap(properties.getBasePath(), httpHandler); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebHttpHandlerBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebHttpHandlerBuilderCustomizer.java new file mode 100644 index 000000000000..fcc755d8b181 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebHttpHandlerBuilderCustomizer.java @@ -0,0 +1,36 @@ +/* + * 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.web.reactive; + +import org.springframework.web.server.adapter.WebHttpHandlerBuilder; + +/** + * Callback interface used to customize a {@link WebHttpHandlerBuilder}. + * + * @author Lasse Wulff + * @since 3.3.0 + */ +@FunctionalInterface +public interface WebHttpHandlerBuilderCustomizer { + + /** + * Callback to customize a {@link WebHttpHandlerBuilder} instance. + * @param webHttpHandlerBuilder the handlerBuilder to customize + */ + void customize(WebHttpHandlerBuilder webHttpHandlerBuilder); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/HttpHandlerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/HttpHandlerAutoConfigurationTests.java index 70df1d0c2e23..1537780e8155 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/HttpHandlerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/HttpHandlerAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -23,8 +23,13 @@ import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ContextPathCompositeHandler; import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.http.server.reactive.MockServerHttpResponse; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerResponse; @@ -40,6 +45,7 @@ * @author Brian Clozel * @author Stephane Nicoll * @author Andy Wilkinson + * @author Lasse Wulff */ class HttpHandlerAutoConfigurationTests { @@ -66,6 +72,20 @@ void shouldConfigureHttpHandlerWithoutWebFluxAutoConfiguration() { .run((context) -> assertThat(context).hasSingleBean(HttpHandler.class)); } + @Test + void customizersAreCalled() { + this.contextRunner.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class)) + .withUserConfiguration(WebHttpHandlerBuilderCustomizers.class) + .run((context) -> { + assertThat(context).hasSingleBean(HttpHandler.class); + HttpHandler httpHandler = context.getBean(HttpHandler.class); + ServerHttpRequest request = MockServerHttpRequest.get("").build(); + ServerHttpResponse response = new MockServerHttpResponse(); + httpHandler.handle(request, response).block(); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.I_AM_A_TEAPOT); + }); + } + @Test void shouldConfigureBasePathCompositeHandler() { this.contextRunner.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class)) @@ -104,4 +124,18 @@ WebHandler webHandler() { } + @Configuration(proxyBeanMethods = false) + static class WebHttpHandlerBuilderCustomizers { + + @Bean + WebHttpHandlerBuilderCustomizer customizerDecorator() { + return (webHttpHandlerBuilder) -> webHttpHandlerBuilder + .httpHandlerDecorator(((httpHandler) -> (request, response) -> { + response.setStatusCode(HttpStatus.I_AM_A_TEAPOT); + return response.setComplete(); + })); + } + + } + } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc index 6e5a2d1b60bc..3c33bd6e6253 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/web/reactive.adoc @@ -44,6 +44,8 @@ The auto-configuration adds the following features on top of Spring's defaults: If you want to keep Spring Boot WebFlux features and you want to add additional {spring-framework-docs}/web/webflux/config.html[WebFlux configuration], you can add your own `@Configuration` class of type `WebFluxConfigurer` but *without* `@EnableWebFlux`. +If you want to add additional customization to the auto-configured `HttpHandler`, you can define beans of type `WebHttpHandlerBuilderCustomizer` and use them to modify the `WebHttpHandlerBuilder`. + If you want to take complete control of Spring WebFlux, you can add your own `@Configuration` annotated with `@EnableWebFlux`. From f26ab78ed789d0797a7dd2742a2cc88bc4eadf4e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 09:57:56 +0000 Subject: [PATCH 1154/1215] Start building against Micrometer 1.12.3 snapshots See gh-39474 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 9bc2ed236f20..cf39b7be4964 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -995,7 +995,7 @@ bom { ] } } - library("Micrometer", "1.12.2") { + library("Micrometer", "1.12.3-SNAPSHOT") { considerSnapshots() group("io.micrometer") { modules = [ From 3f02b632f8aeeec5b95c32c8841a51f56bdd7c93 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 09:58:01 +0000 Subject: [PATCH 1155/1215] Start building against Micrometer Tracing 1.2.3 snapshots See gh-39475 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index cf39b7be4964..25a5ac4eaea6 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1008,7 +1008,7 @@ bom { ] } } - library("Micrometer Tracing", "1.2.2") { + library("Micrometer Tracing", "1.2.3-SNAPSHOT") { considerSnapshots() calendarName = "Tracing" group("io.micrometer") { From 316151bbf57996abf937561e620b75a37d331df7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 09:58:05 +0000 Subject: [PATCH 1156/1215] Start building against Reactor Bom 2023.0.3 snapshots See gh-39476 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 25a5ac4eaea6..d3368ce0da29 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1327,7 +1327,7 @@ bom { ] } } - library("Reactor Bom", "2023.0.2") { + library("Reactor Bom", "2023.0.3-SNAPSHOT") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From 29659738ce85d84e8a9cf06c20e199b09bfbbc87 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 09:58:10 +0000 Subject: [PATCH 1157/1215] Start building against Spring AMQP 3.1.2 snapshots See gh-39477 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d3368ce0da29..d8afa04ea812 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1495,7 +1495,7 @@ bom { ] } } - library("Spring AMQP", "3.1.1") { + library("Spring AMQP", "3.1.2-SNAPSHOT") { considerSnapshots() group("org.springframework.amqp") { imports = [ From e90e4c51e7403157f0982ee5057e956b1c3cf349 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 09:58:15 +0000 Subject: [PATCH 1158/1215] Start building against Spring Authorization Server 1.2.2 snapshots See gh-39478 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index d8afa04ea812..9ceaf00fa525 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1503,7 +1503,7 @@ bom { ] } } - library("Spring Authorization Server", "1.2.1") { + library("Spring Authorization Server", "1.2.2-SNAPSHOT") { considerSnapshots() group("org.springframework.security") { modules = [ From f527e9a6fcb04e6946be94ae141160f91d69d87c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 09:58:19 +0000 Subject: [PATCH 1159/1215] Start building against Spring Batch 5.1.1 snapshots See gh-39479 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 9ceaf00fa525..edae1dc62abd 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1511,7 +1511,7 @@ bom { ] } } - library("Spring Batch", "5.1.0") { + library("Spring Batch", "5.1.1-SNAPSHOT") { considerSnapshots() group("org.springframework.batch") { imports = [ From c58406c7a5de0c2d7228305f8afca0ccb092eb04 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 09:58:24 +0000 Subject: [PATCH 1160/1215] Start building against Spring Data Bom 2023.1.3 snapshots See gh-39480 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index edae1dc62abd..32e85d92ff4a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1519,7 +1519,7 @@ bom { ] } } - library("Spring Data Bom", "2023.1.2") { + library("Spring Data Bom", "2023.1.3-SNAPSHOT") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From 4b37228cfaa5eaf638faeac306046839621e076c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 09:58:29 +0000 Subject: [PATCH 1161/1215] Start building against Spring Framework 6.1.4 snapshots See gh-39481 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 28236114ec4a..d1786b540b68 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ junitJupiterVersion=5.10.1 kotlinVersion=1.9.22 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.28 -springFrameworkVersion=6.1.3 +springFrameworkVersion=6.1.4-SNAPSHOT tomcatVersion=10.1.18 kotlin.stdlib.default.dependency=false From dada1378bd847ab29492827d26c206b054c413ba Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 09:58:34 +0000 Subject: [PATCH 1162/1215] Start building against Spring GraphQL 1.2.5 snapshots See gh-39482 --- ...raphQlWebMvcSecurityAutoConfigurationTests.java | 12 +++--------- .../GraphQlWebMvcAutoConfigurationTests.java | 14 ++++---------- .../spring-boot-dependencies/build.gradle | 2 +- 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebMvcSecurityAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebMvcSecurityAutoConfigurationTests.java index a7deb9a3afdb..45ea3fdcc818 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebMvcSecurityAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebMvcSecurityAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-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. @@ -46,14 +46,12 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -84,8 +82,7 @@ void contributesSecurityComponents() { void anonymousUserShouldBeUnauthorized() { testWith((mockMvc) -> { String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author }}"; - MvcResult result = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")).andReturn(); - mockMvc.perform(asyncDispatch(result)) + mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("data.bookById.name").doesNotExist()) @@ -97,10 +94,7 @@ void anonymousUserShouldBeUnauthorized() { void authenticatedUserShouldGetData() { testWith((mockMvc) -> { String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author }}"; - MvcResult result = mockMvc - .perform(post("/graphql").content("{\"query\": \"" + query + "\"}").with(user("rob"))) - .andReturn(); - mockMvc.perform(asyncDispatch(result)) + mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}").with(user("rob"))) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("data.bookById.name").value("GraphQL for beginners")) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfigurationTests.java index 6df0fdff742a..cd082e39ac6b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -43,7 +43,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.function.RouterFunction; @@ -52,7 +51,6 @@ import org.springframework.web.socket.server.support.WebSocketHandlerMapping; import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; @@ -88,8 +86,7 @@ void shouldContributeDefaultBeans() { void simpleQueryShouldWork() { testWith((mockMvc) -> { String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }"; - MvcResult result = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")).andReturn(); - mockMvc.perform(asyncDispatch(result)) + mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_GRAPHQL_RESPONSE)) .andExpect(jsonPath("data.bookById.name").value("GraphQL for beginners")); @@ -120,8 +117,7 @@ void shouldRejectQueryWithInvalidJson() { void shouldConfigureWebInterceptors() { testWith((mockMvc) -> { String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }"; - MvcResult result = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")).andReturn(); - mockMvc.perform(asyncDispatch(result)) + mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")) .andExpect(status().isOk()) .andExpect(header().string("X-Custom-Header", "42")); }); @@ -152,12 +148,10 @@ void shouldSupportCors() { testWith((mockMvc) -> { String query = "{" + " bookById(id: \\\"book-1\\\"){ " + " id" + " name" + " pageCount" + " author" + " }" + "}"; - MvcResult result = mockMvc + mockMvc .perform(post("/graphql").header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST") .header(HttpHeaders.ORIGIN, "https://example.com") .content("{\"query\": \"" + query + "\"}")) - .andReturn(); - mockMvc.perform(asyncDispatch(result)) .andExpect(status().isOk()) .andExpect(header().stringValues(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "https://example.com")) .andExpect(header().stringValues(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true")); diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 32e85d92ff4a..e16409cb8337 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1537,7 +1537,7 @@ bom { ] } } - library("Spring GraphQL", "1.2.4") { + library("Spring GraphQL", "1.2.5-SNAPSHOT") { considerSnapshots() group("org.springframework.graphql") { modules = [ From 65a96b270d023f2f82f504d2d035ee644cdaeb05 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 09:58:38 +0000 Subject: [PATCH 1163/1215] Start building against Spring Integration 6.2.2 snapshots See gh-39483 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e16409cb8337..386bfb1f3663 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1554,7 +1554,7 @@ bom { ] } } - library("Spring Integration", "6.2.1") { + library("Spring Integration", "6.2.2-SNAPSHOT") { considerSnapshots() group("org.springframework.integration") { imports = [ From b1d841dab83e78870fa5ea1acf2956b7b5c759d8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 09:58:43 +0000 Subject: [PATCH 1164/1215] Start building against Spring Kafka 3.1.2 snapshots See gh-39484 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 386bfb1f3663..006bb194f6e3 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1562,7 +1562,7 @@ bom { ] } } - library("Spring Kafka", "3.1.1") { + library("Spring Kafka", "3.1.2-SNAPSHOT") { considerSnapshots() group("org.springframework.kafka") { modules = [ From 3ab24c06631aacbd2e3dafdc7ef6b5216dd52d63 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 09:58:48 +0000 Subject: [PATCH 1165/1215] Start building against Spring LDAP 3.2.2 snapshots See gh-39485 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 006bb194f6e3..76089d58cbe2 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1571,7 +1571,7 @@ bom { ] } } - library("Spring LDAP", "3.2.1") { + library("Spring LDAP", "3.2.2-SNAPSHOT") { considerSnapshots() group("org.springframework.ldap") { modules = [ From 54201108145d36b9928efd31c6c6c21bec4dab82 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 09:58:53 +0000 Subject: [PATCH 1166/1215] Start building against Spring Pulsar 1.0.3 snapshots See gh-39486 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 76089d58cbe2..efdd56d57225 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1582,7 +1582,7 @@ bom { ] } } - library("Spring Pulsar", "1.0.2") { + library("Spring Pulsar", "1.0.3-SNAPSHOT") { considerSnapshots() group("org.springframework.pulsar") { imports = [ From aa061696b1a825268fa713e824d1e63ff7aa25ad Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 09:58:57 +0000 Subject: [PATCH 1167/1215] Start building against Spring Security 6.2.2 snapshots See gh-39487 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index efdd56d57225..3a02e99f5d40 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1606,7 +1606,7 @@ bom { ] } } - library("Spring Security", "6.2.1") { + library("Spring Security", "6.2.2-SNAPSHOT") { considerSnapshots() group("org.springframework.security") { imports = [ From 1f7a9837014fcd3ede90421b2c73de2342c7ded1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 14:18:08 +0000 Subject: [PATCH 1168/1215] Switch to jersey-micrometer for Jersey metrics Closes gh-39502 --- .../build.gradle | 1 + .../JerseyServerMetricsAutoConfiguration.java | 46 +++++++++++++--- ...eyServerMetricsAutoConfigurationTests.java | 54 +++++++++++++++++-- 3 files changed, 89 insertions(+), 12 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index 272e36bbc9f9..de4db46bda7d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -102,6 +102,7 @@ dependencies { optional("org.flywaydb:flyway-core") optional("org.glassfish.jersey.core:jersey-server") optional("org.glassfish.jersey.containers:jersey-container-servlet-core") + optional("org.glassfish.jersey.ext:jersey-micrometer") optional("org.hibernate.orm:hibernate-core") optional("org.hibernate.orm:hibernate-micrometer") optional("org.hibernate.validator:hibernate-validator") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfiguration.java index 27d9729cc80f..6615a69c900c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfiguration.java @@ -20,13 +20,16 @@ import java.lang.reflect.AnnotatedElement; import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.binder.jersey.server.AnnotationFinder; -import io.micrometer.core.instrument.binder.jersey.server.DefaultJerseyTagsProvider; -import io.micrometer.core.instrument.binder.jersey.server.JerseyTagsProvider; -import io.micrometer.core.instrument.binder.jersey.server.MetricsApplicationEventListener; +import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.config.MeterFilter; +import org.glassfish.jersey.micrometer.server.AnnotationFinder; +import org.glassfish.jersey.micrometer.server.DefaultJerseyTagsProvider; +import org.glassfish.jersey.micrometer.server.JerseyTagsProvider; +import org.glassfish.jersey.micrometer.server.MetricsApplicationEventListener; import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties; import org.springframework.boot.actuate.autoconfigure.metrics.OnlyOnceLoggingDenyMeterFilter; @@ -66,17 +69,22 @@ public JerseyServerMetricsAutoConfiguration(ObservationProperties observationPro } @Bean - @ConditionalOnMissingBean(JerseyTagsProvider.class) + @SuppressWarnings("deprecation") + @ConditionalOnMissingBean({ JerseyTagsProvider.class, + io.micrometer.core.instrument.binder.jersey.server.JerseyTagsProvider.class }) public DefaultJerseyTagsProvider jerseyTagsProvider() { return new DefaultJerseyTagsProvider(); } @Bean + @SuppressWarnings("deprecation") public ResourceConfigCustomizer jerseyServerMetricsResourceConfigCustomizer(MeterRegistry meterRegistry, - JerseyTagsProvider tagsProvider) { + ObjectProvider tagsProvider, + ObjectProvider micrometerTagsProvider) { String metricName = this.observationProperties.getHttp().getServer().getRequests().getName(); - return (config) -> config.register(new MetricsApplicationEventListener(meterRegistry, tagsProvider, metricName, - true, new AnnotationUtilsAnnotationFinder())); + return (config) -> config.register(new MetricsApplicationEventListener(meterRegistry, + tagsProvider.getIfAvailable(() -> new JerseyTagsProviderAdapter(micrometerTagsProvider.getObject())), + metricName, true, new AnnotationUtilsAnnotationFinder())); } @Bean @@ -101,4 +109,26 @@ public A findAnnotation(AnnotatedElement annotatedElement } + @SuppressWarnings("deprecation") + static final class JerseyTagsProviderAdapter implements JerseyTagsProvider { + + private final io.micrometer.core.instrument.binder.jersey.server.JerseyTagsProvider delegate; + + private JerseyTagsProviderAdapter( + io.micrometer.core.instrument.binder.jersey.server.JerseyTagsProvider delegate) { + this.delegate = delegate; + } + + @Override + public Iterable httpRequestTags(RequestEvent event) { + return this.delegate.httpRequestTags(event); + } + + @Override + public Iterable httpLongRequestTags(RequestEvent event) { + return this.delegate.httpLongRequestTags(event); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfigurationTests.java index a7b1a6326ea9..612db1b2b514 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -17,22 +17,25 @@ package org.springframework.boot.actuate.autoconfigure.metrics.jersey; import java.net.URI; +import java.util.Set; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Timer; -import io.micrometer.core.instrument.binder.jersey.server.DefaultJerseyTagsProvider; -import io.micrometer.core.instrument.binder.jersey.server.JerseyTagsProvider; -import io.micrometer.core.instrument.binder.jersey.server.MetricsApplicationEventListener; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.glassfish.jersey.micrometer.server.DefaultJerseyTagsProvider; +import org.glassfish.jersey.micrometer.server.JerseyTagsProvider; +import org.glassfish.jersey.micrometer.server.MetricsApplicationEventListener; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.server.monitoring.RequestEvent; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.jersey.JerseyServerMetricsAutoConfiguration.JerseyTagsProviderAdapter; import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -87,6 +90,22 @@ void shouldHonorExistingTagProvider() { .run((context) -> assertThat(context).hasSingleBean(CustomJerseyTagsProvider.class)); } + @Test + @Deprecated(since = "3.3.0", forRemoval = true) + void shouldHonorExistingMicrometerTagProvider() { + this.webContextRunner.withUserConfiguration(CustomMicrometerJerseyTagsProviderConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(CustomMicrometerJerseyTagsProvider.class); + ResourceConfig config = new ResourceConfig(); + context.getBean(ResourceConfigCustomizer.class).customize(config); + Set instances = config.getInstances(); + assertThat(instances).hasSize(1) + .first(InstanceOfAssertFactories.type(MetricsApplicationEventListener.class)) + .satisfies((listener) -> assertThat(listener).extracting("tagsProvider") + .isInstanceOf(JerseyTagsProviderAdapter.class)); + }); + } + @Test void httpRequestsAreTimed() { this.webContextRunner.run((context) -> { @@ -161,4 +180,31 @@ public Iterable httpLongRequestTags(RequestEvent event) { } + @SuppressWarnings("deprecation") + @Configuration(proxyBeanMethods = false) + static class CustomMicrometerJerseyTagsProviderConfiguration { + + @Bean + io.micrometer.core.instrument.binder.jersey.server.JerseyTagsProvider customJerseyTagsProvider() { + return new CustomMicrometerJerseyTagsProvider(); + } + + } + + @SuppressWarnings("deprecation") + static class CustomMicrometerJerseyTagsProvider + implements io.micrometer.core.instrument.binder.jersey.server.JerseyTagsProvider { + + @Override + public Iterable httpRequestTags(RequestEvent event) { + return null; + } + + @Override + public Iterable httpLongRequestTags(RequestEvent event) { + return null; + } + + } + } From 4d81645a09aca9cfe5d6f4f562eab108bc325432 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 12:03:53 +0000 Subject: [PATCH 1169/1215] Start building against Micrometer 1.13.0-M1 snapshots See gh-38984 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 34a40aa769b9..67923992ecf3 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -996,7 +996,7 @@ bom { ] } } - library("Micrometer", "1.12.2") { + library("Micrometer", "1.13.0-SNAPSHOT") { considerSnapshots() group("io.micrometer") { modules = [ From f5f5c1150d373f659fc360aef12f2c28e6420f24 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 12:03:54 +0000 Subject: [PATCH 1170/1215] Start building against Micrometer Tracing 1.3.0-M1 snapshots See gh-38985 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 67923992ecf3..dc6d2d09d9e6 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1009,7 +1009,7 @@ bom { ] } } - library("Micrometer Tracing", "1.2.2") { + library("Micrometer Tracing", "1.3.0-SNAPSHOT") { considerSnapshots() calendarName = "Tracing" group("io.micrometer") { From 63e9a7d20e37b7bc9adc8bb3a5c18deaa0a869db Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 12:03:59 +0000 Subject: [PATCH 1171/1215] Start building against Reactor Bom 2023.0.3 snapshots See gh-39489 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index dc6d2d09d9e6..e32228eb6035 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1256,7 +1256,7 @@ bom { ] } } - library("Reactor Bom", "2023.0.2") { + library("Reactor Bom", "2023.0.3-SNAPSHOT") { considerSnapshots() calendarName = "Reactor" group("io.projectreactor") { From 325e390d37395a2dc9a29feb3ecdca9f2351247e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 12:04:03 +0000 Subject: [PATCH 1172/1215] Start building against Spring AMQP 3.1.2 snapshots See gh-39490 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e32228eb6035..1b40a7a6976a 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1424,7 +1424,7 @@ bom { ] } } - library("Spring AMQP", "3.1.1") { + library("Spring AMQP", "3.1.2-SNAPSHOT") { considerSnapshots() group("org.springframework.amqp") { imports = [ From cbbdb4ea72b2563ab666e968c04b2b8a88c3a9d7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 12:04:08 +0000 Subject: [PATCH 1173/1215] Start building against Spring Authorization Server 1.3.0-M2 snapshots See gh-39491 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 1b40a7a6976a..7b3f258dfece 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1432,7 +1432,7 @@ bom { ] } } - library("Spring Authorization Server", "1.3.0-M1") { + library("Spring Authorization Server", "1.3.0-SNAPSHOT") { considerSnapshots() group("org.springframework.security") { modules = [ From dab3be6857da282969fbe65118ca4f860c44205c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 12:04:13 +0000 Subject: [PATCH 1174/1215] Start building against Spring Batch 5.1.1 snapshots See gh-39492 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 7b3f258dfece..20687473e9d5 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1440,7 +1440,7 @@ bom { ] } } - library("Spring Batch", "5.1.0") { + library("Spring Batch", "5.1.1-SNAPSHOT") { considerSnapshots() group("org.springframework.batch") { imports = [ From d64c9c56fa30f004537c5ae850eb76cda8050992 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 12:04:17 +0000 Subject: [PATCH 1175/1215] Start building against Spring Data Bom 2023.1.3 snapshots See gh-39493 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 20687473e9d5..418c678dfc2e 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1448,7 +1448,7 @@ bom { ] } } - library("Spring Data Bom", "2023.1.2") { + library("Spring Data Bom", "2023.1.3-SNAPSHOT") { considerSnapshots() calendarName = "Spring Data Release" group("org.springframework.data") { From 84b9ebe39e14cd6192706a3511f3c4a2269b6959 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 12:04:22 +0000 Subject: [PATCH 1176/1215] Start building against Spring Framework 6.1.4 snapshots See gh-39494 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index aae7d3d3dfc9..e0c0a57feba6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,7 +12,7 @@ junitJupiterVersion=5.10.1 kotlinVersion=1.9.22 mavenVersion=3.9.4 nativeBuildToolsVersion=0.10.0 -springFrameworkVersion=6.1.3 +springFrameworkVersion=6.1.4-SNAPSHOT tomcatVersion=10.1.18 kotlin.stdlib.default.dependency=false From 33b48786a8adf89824e68b447e2f27d716323f91 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 12:04:26 +0000 Subject: [PATCH 1177/1215] Start building against Spring GraphQL 1.3.0-M1 snapshots See gh-39495 --- ...raphQlWebMvcSecurityAutoConfigurationTests.java | 12 +++--------- .../GraphQlWebMvcAutoConfigurationTests.java | 14 ++++---------- .../spring-boot-dependencies/build.gradle | 2 +- 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebMvcSecurityAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebMvcSecurityAutoConfigurationTests.java index a7deb9a3afdb..45ea3fdcc818 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebMvcSecurityAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/security/GraphQlWebMvcSecurityAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-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. @@ -46,14 +46,12 @@ import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.security.config.Customizer.withDefaults; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -84,8 +82,7 @@ void contributesSecurityComponents() { void anonymousUserShouldBeUnauthorized() { testWith((mockMvc) -> { String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author }}"; - MvcResult result = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")).andReturn(); - mockMvc.perform(asyncDispatch(result)) + mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("data.bookById.name").doesNotExist()) @@ -97,10 +94,7 @@ void anonymousUserShouldBeUnauthorized() { void authenticatedUserShouldGetData() { testWith((mockMvc) -> { String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author }}"; - MvcResult result = mockMvc - .perform(post("/graphql").content("{\"query\": \"" + query + "\"}").with(user("rob"))) - .andReturn(); - mockMvc.perform(asyncDispatch(result)) + mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}").with(user("rob"))) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("data.bookById.name").value("GraphQL for beginners")) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfigurationTests.java index 6df0fdff742a..cd082e39ac6b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/graphql/servlet/GraphQlWebMvcAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -43,7 +43,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.function.RouterFunction; @@ -52,7 +51,6 @@ import org.springframework.web.socket.server.support.WebSocketHandlerMapping; import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; @@ -88,8 +86,7 @@ void shouldContributeDefaultBeans() { void simpleQueryShouldWork() { testWith((mockMvc) -> { String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }"; - MvcResult result = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")).andReturn(); - mockMvc.perform(asyncDispatch(result)) + mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_GRAPHQL_RESPONSE)) .andExpect(jsonPath("data.bookById.name").value("GraphQL for beginners")); @@ -120,8 +117,7 @@ void shouldRejectQueryWithInvalidJson() { void shouldConfigureWebInterceptors() { testWith((mockMvc) -> { String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }"; - MvcResult result = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")).andReturn(); - mockMvc.perform(asyncDispatch(result)) + mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")) .andExpect(status().isOk()) .andExpect(header().string("X-Custom-Header", "42")); }); @@ -152,12 +148,10 @@ void shouldSupportCors() { testWith((mockMvc) -> { String query = "{" + " bookById(id: \\\"book-1\\\"){ " + " id" + " name" + " pageCount" + " author" + " }" + "}"; - MvcResult result = mockMvc + mockMvc .perform(post("/graphql").header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST") .header(HttpHeaders.ORIGIN, "https://example.com") .content("{\"query\": \"" + query + "\"}")) - .andReturn(); - mockMvc.perform(asyncDispatch(result)) .andExpect(status().isOk()) .andExpect(header().stringValues(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "https://example.com")) .andExpect(header().stringValues(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true")); diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 418c678dfc2e..bcb26086b501 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1466,7 +1466,7 @@ bom { ] } } - library("Spring GraphQL", "1.2.4") { + library("Spring GraphQL", "1.3.0-SNAPSHOT") { considerSnapshots() group("org.springframework.graphql") { modules = [ From b80570f506d7aba3fc36737fcc22c42bd87ee6a3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 12:04:31 +0000 Subject: [PATCH 1178/1215] Start building against Spring Integration 6.3.0-M1 snapshots See gh-39496 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index bcb26086b501..ae413df9a721 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1483,7 +1483,7 @@ bom { ] } } - library("Spring Integration", "6.2.1") { + library("Spring Integration", "6.3.0-SNAPSHOT") { considerSnapshots() group("org.springframework.integration") { imports = [ From bc91b3460ce62d8aa8ac9e3fa9ad55791eaac2c0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 12:04:36 +0000 Subject: [PATCH 1179/1215] Start building against Spring Kafka 3.2.0-M1 snapshots See gh-39497 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index ae413df9a721..cf74ce1de7f6 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1491,7 +1491,7 @@ bom { ] } } - library("Spring Kafka", "3.1.1") { + library("Spring Kafka", "3.2.0-SNAPSHOT") { considerSnapshots() group("org.springframework.kafka") { modules = [ From 1b28901ae54716c04ef03c7f8fdef65379f50b2b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 12:04:40 +0000 Subject: [PATCH 1180/1215] Start building against Spring LDAP 3.2.2 snapshots See gh-39498 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index cf74ce1de7f6..0ab49714227c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1500,7 +1500,7 @@ bom { ] } } - library("Spring LDAP", "3.2.1") { + library("Spring LDAP", "3.2.2-SNAPSHOT") { considerSnapshots() group("org.springframework.ldap") { modules = [ From 3aad954443bb1675bc350feebbc9b7ab246b7a23 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 12:04:45 +0000 Subject: [PATCH 1181/1215] Start building against Spring Pulsar 1.1.0-M1 snapshots See gh-39499 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 0ab49714227c..6799eb8a7a8e 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1511,7 +1511,7 @@ bom { ] } } - library("Spring Pulsar", "1.0.2") { + library("Spring Pulsar", "1.1.0-SNAPSHOT") { considerSnapshots() group("org.springframework.pulsar") { imports = [ From 8744cfb800c47e0f664bc1aaf7e1f2b6b96a29d6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 12:04:50 +0000 Subject: [PATCH 1182/1215] Start building against Spring Security 6.3.0-M2 snapshots See gh-39500 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 6799eb8a7a8e..f0f4eeabadbc 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1535,7 +1535,7 @@ bom { ] } } - library("Spring Security", "6.3.0-M1") { + library("Spring Security", "6.3.0-SNAPSHOT") { considerSnapshots() group("org.springframework.security") { imports = [ From 533f6c3bb1dc30838b8f506c49d53b716ce7144c Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Thu, 1 Feb 2024 14:57:47 -0600 Subject: [PATCH 1183/1215] Refactor TestcontainersPropertySource to use MapPropertySource Closes gh-39330 --- .../TestcontainersPropertySource.java | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java index d2df1e65b101..ee84759fe5b1 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/properties/TestcontainersPropertySource.java @@ -36,10 +36,11 @@ import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.Environment; +import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.PropertySource; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; +import org.springframework.util.function.SupplierUtils; /** * {@link EnumerablePropertySource} backed by a map with values supplied from one or more @@ -48,7 +49,7 @@ * @author Phillip Webb * @since 3.1.0 */ -public class TestcontainersPropertySource extends EnumerablePropertySource>> { +public class TestcontainersPropertySource extends MapPropertySource { static final String NAME = "testcontainersPropertySource"; @@ -75,24 +76,14 @@ private void addEventPublisher(ApplicationEventPublisher eventPublisher) { @Override public Object getProperty(String name) { - Supplier valueSupplier = this.source.get(name); + Object valueSupplier = this.source.get(name); return (valueSupplier != null) ? getProperty(name, valueSupplier) : null; } - private Object getProperty(String name, Supplier valueSupplier) { + private Object getProperty(String name, Object valueSupplier) { BeforeTestcontainersPropertySuppliedEvent event = new BeforeTestcontainersPropertySuppliedEvent(this, name); this.eventPublishers.forEach((eventPublisher) -> eventPublisher.publishEvent(event)); - return valueSupplier.get(); - } - - @Override - public boolean containsProperty(String name) { - return this.source.containsKey(name); - } - - @Override - public String[] getPropertyNames() { - return StringUtils.toStringArray(this.source.keySet()); + return SupplierUtils.resolve(valueSupplier); } public static DynamicPropertyRegistry attach(Environment environment) { From 09a6ae51cc83ba9e07da4922cb5d12d080ae57fd Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Fri, 17 Nov 2023 15:56:30 -0600 Subject: [PATCH 1184/1215] Add support for Bitnami container images with Docker Compose Closes gh-35759 --- ...DockerComposeConnectionDetailsFactory.java | 6 +- .../cassandra/CassandraEnvironment.java | 4 +- ...DockerComposeConnectionDetailsFactory.java | 7 +- .../mariadb/MariaDbEnvironment.java | 5 +- ...DockerComposeConnectionDetailsFactory.java | 7 +- ...DockerComposeConnectionDetailsFactory.java | 7 +- ...DockerComposeConnectionDetailsFactory.java | 6 +- .../connection/mongo/MongoEnvironment.java | 7 +- .../connection/mysql/MySqlEnvironment.java | 5 +- ...DockerComposeConnectionDetailsFactory.java | 7 +- ...DockerComposeConnectionDetailsFactory.java | 7 +- ...DockerComposeConnectionDetailsFactory.java | 7 +- .../connection/neo4j/Neo4jEnvironment.java | 9 +- .../postgres/PostgresEnvironment.java | 11 +- ...DockerComposeConnectionDetailsFactory.java | 7 +- ...DockerComposeConnectionDetailsFactory.java | 7 +- ...DockerComposeConnectionDetailsFactory.java | 7 +- .../connection/rabbit/RabbitEnvironment.java | 7 +- ...DockerComposeConnectionDetailsFactory.java | 7 +- ...nectionDetailsFactoryIntegrationTests.java | 55 ++++++++ ...nectionDetailsFactoryIntegrationTests.java | 4 +- .../cassandra/CassandraEnvironmentTests.java | 12 +- ...nectionDetailsFactoryIntegrationTests.java | 56 ++++++++ ...nectionDetailsFactoryIntegrationTests.java | 47 +++++++ ...nectionDetailsFactoryIntegrationTests.java | 49 +++++++ .../mariadb/MariaDbEnvironmentTests.java | 10 +- ...nectionDetailsFactoryIntegrationTests.java | 50 +++++++ ...nectionDetailsFactoryIntegrationTests.java | 47 +++++++ ...nectionDetailsFactoryIntegrationTests.java | 49 +++++++ .../mysql/MySqlEnvironmentTests.java | 10 +- ...nectionDetailsFactoryIntegrationTests.java | 51 ++++++++ .../neo4j/Neo4jEnvironmentTests.java | 11 +- ...nectionDetailsFactoryIntegrationTests.java | 47 +++++++ ...nectionDetailsFactoryIntegrationTests.java | 49 +++++++ .../postgres/PostgresEnvironmentTests.java | 44 ++++++- ...nectionDetailsFactoryIntegrationTests.java | 51 ++++++++ .../rabbit/RabbitEnvironmentTests.java | 15 ++- ...nectionDetailsFactoryIntegrationTests.java | 53 ++++++++ .../cassandra/cassandra-bitnami-compose.yaml | 8 ++ .../cassandra/cassandra-compose.yaml | 2 +- .../elasticsearch-bitnami-compose.yaml | 9 ++ .../mariadb/mariadb-bitnami-compose.yaml | 10 ++ .../mongo/mongo-bitnami-compose.yaml | 8 ++ .../mysql/mysql-bitnami-compose.yaml | 10 ++ .../neo4j/neo4j-bitnami-compose.yaml | 7 + .../postgres/postgres-bitnami-compose.yaml | 9 ++ .../rabbit/rabbit-bitnami-compose.yaml | 8 ++ .../redis/redis-bitnami-compose.yaml | 7 + .../asciidoc/features/docker-compose.adoc | 18 +-- .../testcontainers/BitnamiImageNames.java | 122 ++++++++++++++++++ 50 files changed, 993 insertions(+), 65 deletions(-) create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbBitnamiJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbBitnamiR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlBitnamiJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlBitnamiR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresBitnamiJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresBitnamiR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/redis/RedisBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/cassandra/cassandra-bitnami-compose.yaml create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/elasticsearch/elasticsearch-bitnami-compose.yaml create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mariadb/mariadb-bitnami-compose.yaml create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mongo/mongo-bitnami-compose.yaml create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mysql/mysql-bitnami-compose.yaml create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/neo4j/neo4j-bitnami-compose.yaml create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/postgres/postgres-bitnami-compose.yaml create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/rabbit/rabbit-bitnami-compose.yaml create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/redis/redis-bitnami-compose.yaml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/BitnamiImageNames.java diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraDockerComposeConnectionDetailsFactory.java index aa5d86d8b899..7065297869e6 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraDockerComposeConnectionDetailsFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -32,10 +32,12 @@ class CassandraDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { + private static final String[] CASSANDRA_CONTAINER_NAMES = { "cassandra", "bitnami/cassandra" }; + private static final int CASSANDRA_PORT = 9042; CassandraDockerComposeConnectionDetailsFactory() { - super("cassandra"); + super(CASSANDRA_CONTAINER_NAMES); } @Override diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraEnvironment.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraEnvironment.java index 84ed5f970444..e49f7c40e680 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraEnvironment.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -28,7 +28,7 @@ class CassandraEnvironment { private final String datacenter; CassandraEnvironment(Map env) { - this.datacenter = env.getOrDefault("CASSANDRA_DC", "datacenter1"); + this.datacenter = env.getOrDefault("CASSANDRA_DC", env.getOrDefault("CASSANDRA_DATACENTER", "datacenter1")); } String getDatacenter() { diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchDockerComposeConnectionDetailsFactory.java index 1304bef2b2c6..e2e27630be9d 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchDockerComposeConnectionDetailsFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -31,14 +31,17 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ class ElasticsearchDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { + private static final String[] ELASTICSEARCH_CONTAINER_NAMES = { "elasticsearch", "bitnami/elasticsearch" }; + private static final int ELASTICSEARCH_PORT = 9200; protected ElasticsearchDockerComposeConnectionDetailsFactory() { - super("elasticsearch"); + super(ELASTICSEARCH_CONTAINER_NAMES); } @Override diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbEnvironment.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbEnvironment.java index 388d77d95281..7da0a5e71630 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbEnvironment.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -27,6 +27,7 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ class MariaDbEnvironment { @@ -52,7 +53,7 @@ private String extractPassword(Map env) { Assert.state(!env.containsKey("MYSQL_RANDOM_ROOT_PASSWORD"), "MYSQL_RANDOM_ROOT_PASSWORD is not supported"); Assert.state(!env.containsKey("MARIADB_ROOT_PASSWORD_HASH"), "MARIADB_ROOT_PASSWORD_HASH is not supported"); boolean allowEmpty = env.containsKey("MARIADB_ALLOW_EMPTY_PASSWORD") - || env.containsKey("MYSQL_ALLOW_EMPTY_PASSWORD"); + || env.containsKey("MYSQL_ALLOW_EMPTY_PASSWORD") || env.containsKey("ALLOW_EMPTY_PASSWORD"); String password = env.get("MARIADB_PASSWORD"); password = (password != null) ? password : env.get("MYSQL_PASSWORD"); password = (password != null) ? password : env.get("MARIADB_ROOT_PASSWORD"); diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbJdbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbJdbcDockerComposeConnectionDetailsFactory.java index f5c2e85a5511..c0759a02f628 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbJdbcDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbJdbcDockerComposeConnectionDetailsFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -29,12 +29,15 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ class MariaDbJdbcDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { + private static final String[] MARIADB_CONTAINER_NAMES = { "mariadb", "bitnami/mariadb" }; + protected MariaDbJdbcDockerComposeConnectionDetailsFactory() { - super("mariadb"); + super(MARIADB_CONTAINER_NAMES); } @Override diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbR2dbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbR2dbcDockerComposeConnectionDetailsFactory.java index 06b2f1a74245..75f22d156733 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbR2dbcDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbR2dbcDockerComposeConnectionDetailsFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -31,12 +31,15 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ class MariaDbR2dbcDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { + private static final String[] MARIADB_CONTAINER_NAMES = { "mariadb", "bitnami/mariadb" }; + MariaDbR2dbcDockerComposeConnectionDetailsFactory() { - super("mariadb", "io.r2dbc.spi.ConnectionFactoryOptions"); + super(MARIADB_CONTAINER_NAMES, "io.r2dbc.spi.ConnectionFactoryOptions"); } @Override diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoDockerComposeConnectionDetailsFactory.java index 34e748c143e7..4fdba16f69f2 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoDockerComposeConnectionDetailsFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -34,10 +34,12 @@ */ class MongoDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { + private static final String[] MONGODB_CONTAINER_NAMES = { "mongo", "bitnami/mongodb" }; + private static final int MONGODB_PORT = 27017; protected MongoDockerComposeConnectionDetailsFactory() { - super("mongo", "com.mongodb.ConnectionString"); + super(MONGODB_CONTAINER_NAMES, "com.mongodb.ConnectionString"); } @Override diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoEnvironment.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoEnvironment.java index 9b881fdab6ca..7bdb2e407b0a 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoEnvironment.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -26,6 +26,7 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ class MongoEnvironment { @@ -40,8 +41,8 @@ class MongoEnvironment { "MONGO_INITDB_ROOT_USERNAME_FILE is not supported"); Assert.state(!env.containsKey("MONGO_INITDB_ROOT_PASSWORD_FILE"), "MONGO_INITDB_ROOT_PASSWORD_FILE is not supported"); - this.username = env.get("MONGO_INITDB_ROOT_USERNAME"); - this.password = env.get("MONGO_INITDB_ROOT_PASSWORD"); + this.username = env.getOrDefault("MONGO_INITDB_ROOT_USERNAME", env.get("MONGO_ROOT_USERNAME")); + this.password = env.getOrDefault("MONGO_INITDB_ROOT_PASSWORD", env.get("MONGO_ROOT_PASSWORD")); this.database = env.get("MONGO_INITDB_DATABASE"); } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlEnvironment.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlEnvironment.java index ceb8afe5aa0d..d0c7fe4450b1 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlEnvironment.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -27,6 +27,7 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ class MySqlEnvironment { @@ -44,7 +45,7 @@ class MySqlEnvironment { private String extractPassword(Map env) { Assert.state(!env.containsKey("MYSQL_RANDOM_ROOT_PASSWORD"), "MYSQL_RANDOM_ROOT_PASSWORD is not supported"); - boolean allowEmpty = env.containsKey("MYSQL_ALLOW_EMPTY_PASSWORD"); + boolean allowEmpty = env.containsKey("MYSQL_ALLOW_EMPTY_PASSWORD") || env.containsKey("ALLOW_EMPTY_PASSWORD"); String password = env.get("MYSQL_PASSWORD"); password = (password != null) ? password : env.get("MYSQL_ROOT_PASSWORD"); Assert.state(StringUtils.hasLength(password) || allowEmpty, "No MySQL password found"); diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlJdbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlJdbcDockerComposeConnectionDetailsFactory.java index 5977911879d8..d2fb07bf449d 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlJdbcDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlJdbcDockerComposeConnectionDetailsFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -29,12 +29,15 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ class MySqlJdbcDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { + private static final String[] MYSQL_CONTAINER_NAMES = { "mysql", "bitnami/mysql" }; + protected MySqlJdbcDockerComposeConnectionDetailsFactory() { - super("mysql"); + super(MYSQL_CONTAINER_NAMES); } @Override diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlR2dbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlR2dbcDockerComposeConnectionDetailsFactory.java index 6869007a6e84..1b25705b4902 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlR2dbcDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlR2dbcDockerComposeConnectionDetailsFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -31,12 +31,15 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ class MySqlR2dbcDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { + private static final String[] MYSQL_CONTAINER_NAMES = { "mysql", "bitnami/mysql" }; + MySqlR2dbcDockerComposeConnectionDetailsFactory() { - super("mysql", "io.r2dbc.spi.ConnectionFactoryOptions"); + super(MYSQL_CONTAINER_NAMES, "io.r2dbc.spi.ConnectionFactoryOptions"); } @Override diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jDockerComposeConnectionDetailsFactory.java index 33bd622e23ab..5b58d6d1d0df 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jDockerComposeConnectionDetailsFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -30,11 +30,14 @@ * for a {@code Neo4j} service. * * @author Andy Wilkinson + * @author Scott Frederick */ class Neo4jDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { + private static final String[] NEO4J_CONTAINER_NAMES = { "neo4j", "bitnami/neo4j" }; + Neo4jDockerComposeConnectionDetailsFactory() { - super("neo4j"); + super(NEO4J_CONTAINER_NAMES); } @Override diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jEnvironment.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jEnvironment.java index 59e5a90e9230..eaad04475acb 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jEnvironment.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -25,13 +25,18 @@ * Neo4j environment details. * * @author Andy Wilkinson + * @author Scott Frederick */ class Neo4jEnvironment { private final AuthToken authToken; Neo4jEnvironment(Map env) { - this.authToken = parse(env.get("NEO4J_AUTH")); + AuthToken authToken = parse(env.get("NEO4J_AUTH")); + if (authToken == null && env.containsKey("NEO4J_PASSWORD")) { + authToken = parse("neo4j/" + env.get("NEO4J_PASSWORD")); + } + this.authToken = authToken; } private AuthToken parse(String neo4jAuth) { diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresEnvironment.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresEnvironment.java index 435a936e51c0..4a63bbc0b68b 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresEnvironment.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -27,6 +27,7 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ class PostgresEnvironment { @@ -37,14 +38,14 @@ class PostgresEnvironment { private final String database; PostgresEnvironment(Map env) { - this.username = env.getOrDefault("POSTGRES_USER", "postgres"); + this.username = env.getOrDefault("POSTGRES_USER", env.getOrDefault("POSTGRESQL_USER", "postgres")); this.password = extractPassword(env); - this.database = env.getOrDefault("POSTGRES_DB", this.username); + this.database = env.getOrDefault("POSTGRES_DB", env.getOrDefault("POSTGRESQL_DB", this.username)); } private String extractPassword(Map env) { - String password = env.get("POSTGRES_PASSWORD"); - Assert.state(StringUtils.hasLength(password), "No POSTGRES_PASSWORD defined"); + String password = env.getOrDefault("POSTGRES_PASSWORD", env.get("POSTGRESQL_PASSWORD")); + Assert.state(StringUtils.hasLength(password), "PostgreSQL password must be provided"); return password; } diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactory.java index 2330a3dc81cf..3f4ad8653d26 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresJdbcDockerComposeConnectionDetailsFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -29,12 +29,15 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ class PostgresJdbcDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { + private static final String[] POSTGRES_CONTAINER_NAMES = { "postgres", "bitnami/postgresql" }; + protected PostgresJdbcDockerComposeConnectionDetailsFactory() { - super("postgres"); + super(POSTGRES_CONTAINER_NAMES); } @Override diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactory.java index 835bd1fe63fa..cb1c66ded50b 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresR2dbcDockerComposeConnectionDetailsFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -31,12 +31,15 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ class PostgresR2dbcDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { + private static final String[] POSTGRES_CONTAINER_NAMES = { "postgres", "bitnami/postgresql" }; + PostgresR2dbcDockerComposeConnectionDetailsFactory() { - super("postgres", "io.r2dbc.spi.ConnectionFactoryOptions"); + super(POSTGRES_CONTAINER_NAMES, "io.r2dbc.spi.ConnectionFactoryOptions"); } @Override diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitDockerComposeConnectionDetailsFactory.java index 7bc79b1d129f..c4e85853dc66 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitDockerComposeConnectionDetailsFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -30,14 +30,17 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ class RabbitDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { + private static final String[] RABBITMQ_CONTAINER_NAMES = { "rabbitmq", "bitnami/rabbitmq" }; + private static final int RABBITMQ_PORT = 5672; protected RabbitDockerComposeConnectionDetailsFactory() { - super("rabbitmq"); + super(RABBITMQ_CONTAINER_NAMES); } @Override diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitEnvironment.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitEnvironment.java index 4cc471149f4e..7d6beb70f2e8 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitEnvironment.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -24,6 +24,7 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ class RabbitEnvironment { @@ -32,8 +33,8 @@ class RabbitEnvironment { private final String password; RabbitEnvironment(Map env) { - this.username = env.getOrDefault("RABBITMQ_DEFAULT_USER", "guest"); - this.password = env.getOrDefault("RABBITMQ_DEFAULT_PASS", "guest"); + this.username = env.getOrDefault("RABBITMQ_DEFAULT_USER", env.getOrDefault("RABBITMQ_USERNAME", "guest")); + this.password = env.getOrDefault("RABBITMQ_DEFAULT_PASS", env.getOrDefault("RABBITMQ_PASSWORD", "guest")); } String getUsername() { diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/redis/RedisDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/redis/RedisDockerComposeConnectionDetailsFactory.java index 9503bde010ac..c41ba25ccbc4 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/redis/RedisDockerComposeConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/redis/RedisDockerComposeConnectionDetailsFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -28,13 +28,16 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ class RedisDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory { + private static final String[] REDIS_CONTAINER_NAMES = { "redis", "bitnami/redis" }; + private static final int REDIS_PORT = 6379; RedisDockerComposeConnectionDetailsFactory() { - super("redis"); + super(REDIS_CONTAINER_NAMES); } @Override diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..28cc16ec33ad --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,55 @@ +/* + * 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.docker.compose.service.connection.cassandra; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.cassandra.CassandraConnectionDetails; +import org.springframework.boot.autoconfigure.cassandra.CassandraConnectionDetails.Node; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.testsupport.testcontainers.BitnamiImageNames; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test for {@link CassandraDockerComposeConnectionDetailsFactory}. + * + * @author Scott Frederick + */ +class CassandraBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests + extends AbstractDockerComposeIntegrationTests { + + CassandraBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("cassandra-bitnami-compose.yaml", BitnamiImageNames.cassandra()); + } + + @Test + void runCreatesConnectionDetails() { + CassandraConnectionDetails connectionDetails = run(CassandraConnectionDetails.class); + List contactPoints = connectionDetails.getContactPoints(); + assertThat(contactPoints).hasSize(1); + Node node = contactPoints.get(0); + assertThat(node.host()).isNotNull(); + assertThat(node.port()).isGreaterThan(0); + assertThat(connectionDetails.getUsername()).isNull(); + assertThat(connectionDetails.getPassword()).isNull(); + assertThat(connectionDetails.getLocalDatacenter()).isEqualTo("testdc1"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraDockerComposeConnectionDetailsFactoryIntegrationTests.java index 8df562e23111..d9d79aeb471f 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -48,7 +48,7 @@ void runCreatesConnectionDetails() { assertThat(node.port()).isGreaterThan(0); assertThat(connectionDetails.getUsername()).isNull(); assertThat(connectionDetails.getPassword()).isNull(); - assertThat(connectionDetails.getLocalDatacenter()).isEqualTo("dc1"); + assertThat(connectionDetails.getLocalDatacenter()).isEqualTo("testdc1"); } } diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraEnvironmentTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraEnvironmentTests.java index 9344df66548f..f4f7bbd7ca39 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraEnvironmentTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/cassandra/CassandraEnvironmentTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -36,10 +36,16 @@ void getDatacenterWhenDatacenterIsNotSet() { assertThat(environment.getDatacenter()).isEqualTo("datacenter1"); } + @Test + void getDatacenterWhenDcIsSet() { + CassandraEnvironment environment = new CassandraEnvironment(Map.of("CASSANDRA_DC", "testdc1")); + assertThat(environment.getDatacenter()).isEqualTo("testdc1"); + } + @Test void getDatacenterWhenDatacenterIsSet() { - CassandraEnvironment environment = new CassandraEnvironment(Map.of("CASSANDRA_DC", "dc1")); - assertThat(environment.getDatacenter()).isEqualTo("dc1"); + CassandraEnvironment environment = new CassandraEnvironment(Map.of("CASSANDRA_DATACENTER", "testdc1")); + assertThat(environment.getDatacenter()).isEqualTo("testdc1"); } } diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..88256b596cf5 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/elasticsearch/ElasticsearchBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.elasticsearch; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node; +import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node.Protocol; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.testsupport.testcontainers.BitnamiImageNames; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link ElasticsearchDockerComposeConnectionDetailsFactory}. + * + * @author Scott Frederick + */ +class ElasticsearchBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests + extends AbstractDockerComposeIntegrationTests { + + ElasticsearchBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("elasticsearch-bitnami-compose.yaml", BitnamiImageNames.elasticsearch()); + } + + @Test + void runCreatesConnectionDetails() { + ElasticsearchConnectionDetails connectionDetails = run(ElasticsearchConnectionDetails.class); + assertThat(connectionDetails.getUsername()).isEqualTo("elastic"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + assertThat(connectionDetails.getPathPrefix()).isNull(); + assertThat(connectionDetails.getNodes()).hasSize(1); + Node node = connectionDetails.getNodes().get(0); + assertThat(node.hostname()).isNotNull(); + assertThat(node.port()).isGreaterThan(0); + assertThat(node.protocol()).isEqualTo(Protocol.HTTP); + assertThat(node.username()).isEqualTo("elastic"); + assertThat(node.password()).isEqualTo("secret"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbBitnamiJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbBitnamiJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..0032fa449855 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbBitnamiJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,47 @@ +/* + * 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.docker.compose.service.connection.mariadb; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.testsupport.testcontainers.BitnamiImageNames; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link MariaDbJdbcDockerComposeConnectionDetailsFactory} + * + * @author Scott Frederick + */ +class MariaDbBitnamiJdbcDockerComposeConnectionDetailsFactoryIntegrationTests + extends AbstractDockerComposeIntegrationTests { + + MariaDbBitnamiJdbcDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("mariadb-bitnami-compose.yaml", BitnamiImageNames.mariadb()); + } + + @Test + void runCreatesConnectionDetails() { + JdbcConnectionDetails connectionDetails = run(JdbcConnectionDetails.class); + assertThat(connectionDetails.getUsername()).isEqualTo("myuser"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:mariadb://").endsWith("/mydatabase"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbBitnamiR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbBitnamiR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..2d68b18af356 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbBitnamiR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.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.docker.compose.service.connection.mariadb; + +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.testsupport.testcontainers.BitnamiImageNames; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link MariaDbR2dbcDockerComposeConnectionDetailsFactory} + * + * @author Scott Frederick + */ +class MariaDbBitnamiR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests + extends AbstractDockerComposeIntegrationTests { + + MariaDbBitnamiR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("mariadb-bitnami-compose.yaml", BitnamiImageNames.mariadb()); + } + + @Test + void runCreatesConnectionDetails() { + R2dbcConnectionDetails connectionDetails = run(R2dbcConnectionDetails.class); + ConnectionFactoryOptions connectionFactoryOptions = connectionDetails.getConnectionFactoryOptions(); + assertThat(connectionFactoryOptions.toString()).contains("database=mydatabase", "driver=mariadb", + "password=REDACTED", "user=myuser"); + assertThat(connectionFactoryOptions.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("secret"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbEnvironmentTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbEnvironmentTests.java index d291bfab1d82..1aeb244e68c5 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbEnvironmentTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mariadb/MariaDbEnvironmentTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -31,6 +31,7 @@ * @author Andy Wilkinson * @author Phillip Webb * @author Jinseong Hwang + * @author Scott Frederick */ class MariaDbEnvironmentTests { @@ -130,6 +131,13 @@ void getPasswordWhenHasMariadbPasswordAndMysqlRootPassword() { assertThat(environment.getPassword()).isEqualTo("secret"); } + @Test + void getPasswordWhenHasNoPasswordAndAllowEmptyPassword() { + MariaDbEnvironment environment = new MariaDbEnvironment( + Map.of("ALLOW_EMPTY_PASSWORD", "true", "MARIADB_DATABASE", "db")); + assertThat(environment.getPassword()).isEmpty(); + } + @Test void getPasswordWhenHasNoPasswordAndMariadbAllowEmptyPassword() { MariaDbEnvironment environment = new MariaDbEnvironment( diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..9c93a1ab5970 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mongo/MongoBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,50 @@ +/* + * 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.docker.compose.service.connection.mongo; + +import com.mongodb.ConnectionString; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.testsupport.testcontainers.BitnamiImageNames; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link MongoDockerComposeConnectionDetailsFactory}. + * + * @author Scott Frederick + */ +class MongoBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { + + MongoBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("mongo-bitnami-compose.yaml", BitnamiImageNames.mongo()); + } + + @Test + void runCreatesConnectionDetails() { + MongoConnectionDetails connectionDetails = run(MongoConnectionDetails.class); + ConnectionString connectionString = connectionDetails.getConnectionString(); + assertThat(connectionString.getCredential().getUserName()).isEqualTo("root"); + assertThat(connectionString.getCredential().getPassword()).isEqualTo("secret".toCharArray()); + assertThat(connectionString.getCredential().getSource()).isEqualTo("admin"); + assertThat(connectionString.getDatabase()).isEqualTo("test"); + assertThat(connectionDetails.getGridFs()).isNull(); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlBitnamiJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlBitnamiJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..bea96f9a834a --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlBitnamiJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,47 @@ +/* + * 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.docker.compose.service.connection.mysql; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.testsupport.testcontainers.BitnamiImageNames; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link MySqlJdbcDockerComposeConnectionDetailsFactory} + * + * @author Scott Frederick + */ +class MySqlBitnamiJdbcDockerComposeConnectionDetailsFactoryIntegrationTests + extends AbstractDockerComposeIntegrationTests { + + MySqlBitnamiJdbcDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("mysql-bitnami-compose.yaml", BitnamiImageNames.mysql()); + } + + @Test + void runCreatesConnectionDetails() { + JdbcConnectionDetails connectionDetails = run(JdbcConnectionDetails.class); + assertThat(connectionDetails.getUsername()).isEqualTo("myuser"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:mysql://").endsWith("/mydatabase"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlBitnamiR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlBitnamiR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..18ead62fcdd9 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlBitnamiR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.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.docker.compose.service.connection.mysql; + +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.testsupport.testcontainers.BitnamiImageNames; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link MySqlR2dbcDockerComposeConnectionDetailsFactory} + * + * @author Scott Frederick + */ +class MySqlBitnamiR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests + extends AbstractDockerComposeIntegrationTests { + + MySqlBitnamiR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("mysql-bitnami-compose.yaml", BitnamiImageNames.mysql()); + } + + @Test + void runCreatesConnectionDetails() { + R2dbcConnectionDetails connectionDetails = run(R2dbcConnectionDetails.class); + ConnectionFactoryOptions connectionFactoryOptions = connectionDetails.getConnectionFactoryOptions(); + assertThat(connectionFactoryOptions.toString()).contains("database=mydatabase", "driver=mysql", + "password=REDACTED", "user=myuser"); + assertThat(connectionFactoryOptions.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("secret"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlEnvironmentTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlEnvironmentTests.java index 819d2ebd1359..e01b4a8ebc70 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlEnvironmentTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/mysql/MySqlEnvironmentTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -31,6 +31,7 @@ * @author Andy Wilkinson * @author Phillip Webb * @author Jinseong Hwang + * @author Scott Frederick */ class MySqlEnvironmentTests { @@ -86,6 +87,13 @@ void getPasswordWhenHasNoPasswordAndMysqlAllowEmptyPassword() { assertThat(environment.getPassword()).isEmpty(); } + @Test + void getPasswordWhenHasNoPasswordAndAllowEmptyPassword() { + MySqlEnvironment environment = new MySqlEnvironment( + Map.of("ALLOW_EMPTY_PASSWORD", "true", "MYSQL_DATABASE", "db")); + assertThat(environment.getPassword()).isEmpty(); + } + @Test void getDatabaseWhenHasMysqlDatabase() { MySqlEnvironment environment = new MySqlEnvironment( diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..6750c2363d31 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2023 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.docker.compose.service.connection.neo4j; + +import org.junit.jupiter.api.Test; +import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.Driver; +import org.neo4j.driver.GraphDatabase; + +import org.springframework.boot.autoconfigure.neo4j.Neo4jConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.testsupport.testcontainers.BitnamiImageNames; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; + +/** + * Integration tests for {@link Neo4jDockerComposeConnectionDetailsFactory}. + * + * @author Scott Frederick + */ +class Neo4jBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { + + Neo4jBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("neo4j-bitnami-compose.yaml", BitnamiImageNames.neo4j()); + } + + @Test + void runCreatesConnectionDetailsThatCanAccessNeo4j() { + Neo4jConnectionDetails connectionDetails = run(Neo4jConnectionDetails.class); + assertThat(connectionDetails.getAuthToken()).isEqualTo(AuthTokens.basic("neo4j", "bitnami2")); + try (Driver driver = GraphDatabase.driver(connectionDetails.getUri(), connectionDetails.getAuthToken())) { + assertThatNoException().isThrownBy(driver::verifyConnectivity); + } + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jEnvironmentTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jEnvironmentTests.java index 4cbb02d0b608..528682e773e1 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jEnvironmentTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/neo4j/Neo4jEnvironmentTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -29,11 +29,12 @@ * Tests for {@link Neo4jEnvironment}. * * @author Andy Wilkinson + * @author Scott Frederick */ class Neo4jEnvironmentTests { @Test - void whenNeo4jAuthIsNullThenAuthTokenIsNull() { + void whenNeo4jAuthAndPasswordAreNullThenAuthTokenIsNull() { Neo4jEnvironment environment = new Neo4jEnvironment(Collections.emptyMap()); assertThat(environment.getAuthToken()).isNull(); } @@ -56,4 +57,10 @@ void whenNeo4jAuthIsNeitherNoneNorNeo4jSlashPasswordEnvironmentCreationThrows() .isThrownBy(() -> new Neo4jEnvironment(Map.of("NEO4J_AUTH", "graphdb/custom-password"))); } + @Test + void whenNeo4jPasswordIsProvidedThenAuthTokenIsBasic() { + Neo4jEnvironment environment = new Neo4jEnvironment(Map.of("NEO4J_PASSWORD", "custom-password")); + assertThat(environment.getAuthToken()).isEqualTo(AuthTokens.basic("neo4j", "custom-password")); + } + } diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresBitnamiJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresBitnamiJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..b057a37b20e3 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresBitnamiJdbcDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,47 @@ +/* + * 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.docker.compose.service.connection.postgres; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.jdbc.JdbcConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.testsupport.testcontainers.BitnamiImageNames; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link PostgresJdbcDockerComposeConnectionDetailsFactory}. + * + * @author Scott Frederick + */ +class PostgresBitnamiJdbcDockerComposeConnectionDetailsFactoryIntegrationTests + extends AbstractDockerComposeIntegrationTests { + + PostgresBitnamiJdbcDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("postgres-bitnami-compose.yaml", BitnamiImageNames.postgresql()); + } + + @Test + void runCreatesConnectionDetails() { + JdbcConnectionDetails connectionDetails = run(JdbcConnectionDetails.class); + assertThat(connectionDetails.getUsername()).isEqualTo("myuser"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + assertThat(connectionDetails.getJdbcUrl()).startsWith("jdbc:postgresql://").endsWith("/mydatabase"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresBitnamiR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresBitnamiR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..2f5590f5ec11 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresBitnamiR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests.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.docker.compose.service.connection.postgres; + +import io.r2dbc.spi.ConnectionFactoryOptions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.r2dbc.R2dbcConnectionDetails; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.testsupport.testcontainers.BitnamiImageNames; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link PostgresR2dbcDockerComposeConnectionDetailsFactory}. + * + * @author Scott Frederick + */ +class PostgresBitnamiR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests + extends AbstractDockerComposeIntegrationTests { + + PostgresBitnamiR2dbcDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("postgres-bitnami-compose.yaml", BitnamiImageNames.postgresql()); + } + + @Test + void runCreatesConnectionDetails() { + R2dbcConnectionDetails connectionDetails = run(R2dbcConnectionDetails.class); + ConnectionFactoryOptions connectionFactoryOptions = connectionDetails.getConnectionFactoryOptions(); + assertThat(connectionFactoryOptions.toString()).contains("database=mydatabase", "driver=postgresql", + "password=REDACTED", "user=myuser"); + assertThat(connectionFactoryOptions.getRequiredValue(ConnectionFactoryOptions.PASSWORD)).isEqualTo("secret"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresEnvironmentTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresEnvironmentTests.java index 58d590de53ed..9058c1f11d70 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresEnvironmentTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/postgres/PostgresEnvironmentTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -30,13 +30,14 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ class PostgresEnvironmentTests { @Test void createWhenNoPostgresPasswordThrowsException() { assertThatIllegalStateException().isThrownBy(() -> new PostgresEnvironment(Collections.emptyMap())) - .withMessage("No POSTGRES_PASSWORD defined"); + .withMessage("PostgreSQL password must be provided"); } @Test @@ -45,6 +46,12 @@ void getUsernameWhenNoPostgresUser() { assertThat(environment.getUsername()).isEqualTo("postgres"); } + @Test + void getUsernameWhenNoPostgresqlUser() { + PostgresEnvironment environment = new PostgresEnvironment(Map.of("POSTGRESQL_PASSWORD", "secret")); + assertThat(environment.getUsername()).isEqualTo("postgres"); + } + @Test void getUsernameWhenHasPostgresUser() { PostgresEnvironment environment = new PostgresEnvironment( @@ -52,18 +59,37 @@ void getUsernameWhenHasPostgresUser() { assertThat(environment.getUsername()).isEqualTo("me"); } + @Test + void getUsernameWhenHasPostgresqlUser() { + PostgresEnvironment environment = new PostgresEnvironment( + Map.of("POSTGRESQL_USER", "me", "POSTGRESQL_PASSWORD", "secret")); + assertThat(environment.getUsername()).isEqualTo("me"); + } + @Test void getPasswordWhenHasPostgresPassword() { PostgresEnvironment environment = new PostgresEnvironment(Map.of("POSTGRES_PASSWORD", "secret")); assertThat(environment.getPassword()).isEqualTo("secret"); } + @Test + void getPasswordWhenHasPostgresqlPassword() { + PostgresEnvironment environment = new PostgresEnvironment(Map.of("POSTGRESQL_PASSWORD", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + @Test void getDatabaseWhenNoPostgresDbOrPostgresUser() { PostgresEnvironment environment = new PostgresEnvironment(Map.of("POSTGRES_PASSWORD", "secret")); assertThat(environment.getDatabase()).isEqualTo("postgres"); } + @Test + void getDatabaseWhenNoPostgresqlDbOrPostgresUser() { + PostgresEnvironment environment = new PostgresEnvironment(Map.of("POSTGRESQL_PASSWORD", "secret")); + assertThat(environment.getDatabase()).isEqualTo("postgres"); + } + @Test void getDatabaseWhenNoPostgresDbAndPostgresUser() { PostgresEnvironment environment = new PostgresEnvironment( @@ -71,6 +97,13 @@ void getDatabaseWhenNoPostgresDbAndPostgresUser() { assertThat(environment.getDatabase()).isEqualTo("me"); } + @Test + void getDatabaseWhenNoPostgresqlDbAndPostgresUser() { + PostgresEnvironment environment = new PostgresEnvironment( + Map.of("POSTGRESQL_USER", "me", "POSTGRESQL_PASSWORD", "secret")); + assertThat(environment.getDatabase()).isEqualTo("me"); + } + @Test void getDatabaseWhenHasPostgresDb() { PostgresEnvironment environment = new PostgresEnvironment( @@ -78,4 +111,11 @@ void getDatabaseWhenHasPostgresDb() { assertThat(environment.getDatabase()).isEqualTo("db"); } + @Test + void getDatabaseWhenHasPostgresqlDb() { + PostgresEnvironment environment = new PostgresEnvironment( + Map.of("POSTGRESQL_DB", "db", "POSTGRESQL_PASSWORD", "secret")); + assertThat(environment.getDatabase()).isEqualTo("db"); + } + } diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..abfb1ac59c96 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,51 @@ +/* + * 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.docker.compose.service.connection.rabbit; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails; +import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails.Address; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.testsupport.testcontainers.BitnamiImageNames; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link RabbitDockerComposeConnectionDetailsFactory}. + * + * @author Scott Frederick + */ +class RabbitBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { + + RabbitBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("rabbit-bitnami-compose.yaml", BitnamiImageNames.rabbit()); + } + + @Test + void runCreatesConnectionDetails() { + RabbitConnectionDetails connectionDetails = run(RabbitConnectionDetails.class); + assertThat(connectionDetails.getUsername()).isEqualTo("myuser"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + assertThat(connectionDetails.getVirtualHost()).isEqualTo("/"); + assertThat(connectionDetails.getAddresses()).hasSize(1); + Address address = connectionDetails.getFirstAddress(); + assertThat(address.host()).isNotNull(); + assertThat(address.port()).isGreaterThan(0); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitEnvironmentTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitEnvironmentTests.java index fdac67e5cedc..95d07335395e 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitEnvironmentTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/rabbit/RabbitEnvironmentTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -29,6 +29,7 @@ * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ class RabbitEnvironmentTests { @@ -44,6 +45,12 @@ void getUsernameWhenHasRabbitmqDefaultUser() { assertThat(environment.getUsername()).isEqualTo("me"); } + @Test + void getUsernameWhenHasRabbitmqUsername() { + RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_USERNAME", "me")); + assertThat(environment.getUsername()).isEqualTo("me"); + } + @Test void getUsernameWhenNoRabbitmqDefaultPass() { RabbitEnvironment environment = new RabbitEnvironment(Collections.emptyMap()); @@ -56,4 +63,10 @@ void getUsernameWhenHasRabbitmqDefaultPass() { assertThat(environment.getPassword()).isEqualTo("secret"); } + @Test + void getUsernameWhenHasRabbitmqPassword() { + RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_PASSWORD", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + } diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/redis/RedisBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/redis/RedisBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 000000000000..47feef6dedf4 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/redis/RedisBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,53 @@ +/* + * 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.docker.compose.service.connection.redis; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.data.redis.RedisConnectionDetails; +import org.springframework.boot.autoconfigure.data.redis.RedisConnectionDetails.Standalone; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.testsupport.testcontainers.BitnamiImageNames; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test for {@link RedisDockerComposeConnectionDetailsFactory}. + * + * @author Scott Frederick + */ +class RedisBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { + + RedisBitnamiDockerComposeConnectionDetailsFactoryIntegrationTests() { + super("redis-bitnami-compose.yaml", BitnamiImageNames.redis()); + } + + @Test + void runCreatesConnectionDetails() { + RedisConnectionDetails connectionDetails = run(RedisConnectionDetails.class); + Standalone standalone = connectionDetails.getStandalone(); + assertThat(connectionDetails.getUsername()).isNull(); + assertThat(connectionDetails.getPassword()).isNull(); + assertThat(connectionDetails.getCluster()).isNull(); + assertThat(connectionDetails.getSentinel()).isNull(); + assertThat(standalone).isNotNull(); + assertThat(standalone.getDatabase()).isZero(); + assertThat(standalone.getPort()).isGreaterThan(0); + assertThat(standalone.getHost()).isNotNull(); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/cassandra/cassandra-bitnami-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/cassandra/cassandra-bitnami-compose.yaml new file mode 100644 index 000000000000..fc9c3d6d8ff2 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/cassandra/cassandra-bitnami-compose.yaml @@ -0,0 +1,8 @@ +services: + cassandra: + image: '{imageName}' + ports: + - '9042' + environment: + - 'CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch' + - 'CASSANDRA_DATACENTER=testdc1' diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/cassandra/cassandra-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/cassandra/cassandra-compose.yaml index b8d5ffd528e4..946fd4fd3590 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/cassandra/cassandra-compose.yaml +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/cassandra/cassandra-compose.yaml @@ -9,4 +9,4 @@ services: - 'HEAP_NEWSIZE=128M' - 'MAX_HEAP_SIZE=1024M' - 'CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch' - - 'CASSANDRA_DC=dc1' + - 'CASSANDRA_DC=testdc1' diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/elasticsearch/elasticsearch-bitnami-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/elasticsearch/elasticsearch-bitnami-compose.yaml new file mode 100644 index 000000000000..b68a393757fd --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/elasticsearch/elasticsearch-bitnami-compose.yaml @@ -0,0 +1,9 @@ +services: + elasticsearch: + image: '{imageName}' + environment: + - 'ELASTIC_PASSWORD=secret' + - 'ES_JAVA_OPTS=-Xmx1024m' + ports: + - '9200' + - '9300' diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mariadb/mariadb-bitnami-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mariadb/mariadb-bitnami-compose.yaml new file mode 100644 index 000000000000..64406055b950 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mariadb/mariadb-bitnami-compose.yaml @@ -0,0 +1,10 @@ +services: + database: + image: '{imageName}' + ports: + - '3306' + environment: + - 'MARIADB_ROOT_PASSWORD=verysecret' + - 'MARIADB_USER=myuser' + - 'MARIADB_PASSWORD=secret' + - 'MARIADB_DATABASE=mydatabase' diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mongo/mongo-bitnami-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mongo/mongo-bitnami-compose.yaml new file mode 100644 index 000000000000..0a09b5ced4f6 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mongo/mongo-bitnami-compose.yaml @@ -0,0 +1,8 @@ +services: + mongo: + image: '{imageName}' + ports: + - '27017' + environment: + - 'MONGO_ROOT_USERNAME=root' + - 'MONGO_ROOT_PASSWORD=secret' diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mysql/mysql-bitnami-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mysql/mysql-bitnami-compose.yaml new file mode 100644 index 000000000000..b0340ed3ed48 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/mysql/mysql-bitnami-compose.yaml @@ -0,0 +1,10 @@ +services: + database: + image: '{imageName}' + ports: + - '3306' + environment: + - 'MYSQL_ROOT_PASSWORD=verysecret' + - 'MYSQL_USER=myuser' + - 'MYSQL_PASSWORD=secret' + - 'MYSQL_DATABASE=mydatabase' diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/neo4j/neo4j-bitnami-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/neo4j/neo4j-bitnami-compose.yaml new file mode 100644 index 000000000000..f60e53329a7c --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/neo4j/neo4j-bitnami-compose.yaml @@ -0,0 +1,7 @@ +services: + neo4j: + image: 'bitnami/neo4j:5.16.0' + ports: + - '7687' + environment: + - 'NEO4J_PASSWORD=bitnami2' diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/postgres/postgres-bitnami-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/postgres/postgres-bitnami-compose.yaml new file mode 100644 index 000000000000..cb34245ec140 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/postgres/postgres-bitnami-compose.yaml @@ -0,0 +1,9 @@ +services: + database: + image: '{imageName}' + ports: + - '5432' + environment: + - 'POSTGRESQL_USER=myuser' + - 'POSTGRESQL_DB=mydatabase' + - 'POSTGRESQL_PASSWORD=secret' diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/rabbit/rabbit-bitnami-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/rabbit/rabbit-bitnami-compose.yaml new file mode 100644 index 000000000000..1951fba4bb08 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/rabbit/rabbit-bitnami-compose.yaml @@ -0,0 +1,8 @@ +services: + rabbitmq: + image: '{imageName}' + environment: + - 'RABBITMQ_DEFAULT_USER=myuser' + - 'RABBITMQ_DEFAULT_PASS=secret' + ports: + - '5672' diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/redis/redis-bitnami-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/redis/redis-bitnami-compose.yaml new file mode 100644 index 000000000000..c4d6aeb291f8 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/redis/redis-bitnami-compose.yaml @@ -0,0 +1,7 @@ +services: + redis: + image: '{imageName}' + ports: + - '6379' + environment: + - 'ALLOW_EMPTY_PASSWORD=yes' diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc index 9b6f4688d199..bb5f2058197d 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/docker-compose.adoc @@ -70,28 +70,28 @@ The following service connections are currently supported: | Connection Details | Matched on | `ActiveMQConnectionDetails` -| Containers named "symptoma/activemq", "apache/activemq-classic" +| Containers named "symptoma/activemq" or "apache/activemq-classic" | `ArtemisConnectionDetails` | Containers named "apache/activemq-artemis" | `CassandraConnectionDetails` -| Containers named "cassandra" +| Containers named "cassandra" or "bitnami/cassandra" | `ElasticsearchConnectionDetails` -| Containers named "elasticsearch" +| Containers named "elasticsearch" or "bitnami/elasticsearch" | `JdbcConnectionDetails` -| Containers named "gvenzl/oracle-free", "gvenzl/oracle-xe", "mariadb", "mssql/server", "mysql", or "postgres" +| Containers named "gvenzl/oracle-free", "gvenzl/oracle-xe", "mariadb", "bitnami/mariadb", "mssql/server", "mysql", "bitnami/mysql", "postgres", or "bitnami/postgresql" | `LdapConnectionDetails` | Containers named "osixia/openldap" | `MongoConnectionDetails` -| Containers named "mongo" +| Containers named "mongo" or "bitnami/mongodb" | `Neo4jConnectionDetails` -| Containers named "neo4j" +| Containers named "neo4j" or "bitnami/neo4j" | `OtlpMetricsConnectionDetails` | Containers named "otel/opentelemetry-collector-contrib" @@ -103,13 +103,13 @@ The following service connections are currently supported: | Containers named "apachepulsar/pulsar" | `R2dbcConnectionDetails` -| Containers named "gvenzl/oracle-free", "gvenzl/oracle-xe", "mariadb", "mssql/server", "mysql", or "postgres" +| Containers named "gvenzl/oracle-free", "gvenzl/oracle-xe", "mariadb", "bitnami/mariadb", "mssql/server", "mysql", "bitnami/mysql", "postgres", or "bitnami/postgresql" | `RabbitConnectionDetails` -| Containers named "rabbitmq" +| Containers named "rabbitmq" or "bitnami/rabbitmq" | `RedisConnectionDetails` -| Containers named "redis" +| Containers named "redis" or "bitnami/redis" | `ZipkinConnectionDetails` | Containers named "openzipkin/zipkin". diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/BitnamiImageNames.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/BitnamiImageNames.java new file mode 100644 index 000000000000..c5a6700955ab --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/BitnamiImageNames.java @@ -0,0 +1,122 @@ +/* + * 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.testsupport.testcontainers; + +import org.testcontainers.utility.DockerImageName; + +/** + * Create {@link DockerImageName} for Bitnami instances for services used in integration + * tests. + * + * @author Scott Frederick + */ +public final class BitnamiImageNames { + + private static final String CASSANDRA_VERSION = "4.1.3"; + + private static final String ELASTICSEARCH_VERSION = "8.12.1"; + + private static final String MARIADB_VERSION = "11.2.3"; + + private static final String MONGO_VERSION = "7.0.5"; + + private static final String MYSQL_VERSION = "8.0.36"; + + private static final String NEO4J_VERSION = "5.16.0"; + + private static final String POSTGRESQL_VERSION = "16.2.0"; + + private static final String RABBIT_VERSION = "3.11.28"; + + private static final String REDIS_VERSION = "7.2.4"; + + private BitnamiImageNames() { + } + + /** + * Return a {@link DockerImageName} suitable for running Cassandra. + * @return a docker image name for running cassandra + */ + public static DockerImageName cassandra() { + return DockerImageName.parse("bitnami/cassandra").withTag(CASSANDRA_VERSION); + } + + /** + * Return a {@link DockerImageName} suitable for running Elasticsearch 7. + * @return a docker image name for running elasticsearch + */ + public static DockerImageName elasticsearch() { + return DockerImageName.parse("bitnami/elasticsearch").withTag(ELASTICSEARCH_VERSION); + } + + /** + * Return a {@link DockerImageName} suitable for running MariaDB. + * @return a docker image name for running MariaDB + */ + public static DockerImageName mariadb() { + return DockerImageName.parse("bitnami/mariadb").withTag(MARIADB_VERSION); + } + + /** + * Return a {@link DockerImageName} suitable for running Mongo. + * @return a docker image name for running mongo + */ + public static DockerImageName mongo() { + return DockerImageName.parse("bitnami/mongodb").withTag(MONGO_VERSION); + } + + /** + * Return a {@link DockerImageName} suitable for running MySQL. + * @return a docker image name for running MySQL + */ + public static DockerImageName mysql() { + return DockerImageName.parse("bitnami/mysql").withTag(MYSQL_VERSION); + } + + /** + * Return a {@link DockerImageName} suitable for running Neo4j. + * @return a docker image name for running neo4j + */ + public static DockerImageName neo4j() { + return DockerImageName.parse("bitnami/neo4j").withTag(NEO4J_VERSION); + } + + /** + * Return a {@link DockerImageName} suitable for running PostgreSQL. + * @return a docker image name for running postgresql + */ + public static DockerImageName postgresql() { + return DockerImageName.parse("bitnami/postgresql").withTag(POSTGRESQL_VERSION); + } + + /** + * Return a {@link DockerImageName} suitable for running RabbitMQ. + * @return a docker image name for running RabbitMQ + */ + public static DockerImageName rabbit() { + return DockerImageName.parse("bitnami/rabbitmq").withTag(RABBIT_VERSION); + } + + /** + * Return a {@link DockerImageName} suitable for running Redis. + * @return a docker image name for running redis + */ + public static DockerImageName redis() { + return DockerImageName.parse("bitnami/redis").withTag(REDIS_VERSION); + } + +} From 02765bc9f0b21b963a3f29e774a45c9558148a32 Mon Sep 17 00:00:00 2001 From: Jakob Wanger Date: Sat, 10 Feb 2024 15:35:59 -0500 Subject: [PATCH 1185/1215] Clarify that auto-configured OpenTelemetry Resource behaviour The documentation does not describe that exposing a Resource bean, will prevent the property from being able to provide attributes (unless the newly exposed Resource bean, implements it). Signed-off-by: Jakob Wanger See gh-39509 --- .../src/docs/asciidoc/actuator/observability.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc index e0b02262c7b6..efff390d2067 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc @@ -80,7 +80,7 @@ Spring Boot's actuator module includes basic support for https://opentelemetry.i It provides a bean of type `OpenTelemetry`, and if there are beans of type `SdkTracerProvider`, `ContextPropagators`, `SdkLoggerProvider` or `SdkMeterProvider` in the application context, they automatically get registered. Additionally, it provides a `Resource` bean. -The attributes of the `Resource` can be configured via the configprop:management.opentelemetry.resource-attributes[] configuration property. +The attributes of the `Resource` can be configured via the configprop:management.opentelemetry.resource-attributes[] configuration property if you have not defined and exposed your own Resource bean. NOTE: Spring Boot does not provide auto-configuration for OpenTelemetry metrics or logging. OpenTelemetry tracing is only auto-configured when used together with <>. From 6163308fbc24f3f6cbc8f73ca65be5b9d9f85dab Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Mon, 12 Feb 2024 08:20:24 +0100 Subject: [PATCH 1186/1215] Polish "Clarify that auto-configured OpenTelemetry Resource behaviour" See gh-39509 --- .../src/docs/asciidoc/actuator/observability.adoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc index efff390d2067..54d2e5ba4c96 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc @@ -80,7 +80,8 @@ Spring Boot's actuator module includes basic support for https://opentelemetry.i It provides a bean of type `OpenTelemetry`, and if there are beans of type `SdkTracerProvider`, `ContextPropagators`, `SdkLoggerProvider` or `SdkMeterProvider` in the application context, they automatically get registered. Additionally, it provides a `Resource` bean. -The attributes of the `Resource` can be configured via the configprop:management.opentelemetry.resource-attributes[] configuration property if you have not defined and exposed your own Resource bean. +The attributes of the auto-configured `Resource` can be configured via the configprop:management.opentelemetry.resource-attributes[] configuration property. +If you have defined your own `Resource` bean, this will no longer be the case. NOTE: Spring Boot does not provide auto-configuration for OpenTelemetry metrics or logging. OpenTelemetry tracing is only auto-configured when used together with <>. From 2b7fcd271cff5a37da9f1b6aad8408dab505b613 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Sat, 10 Feb 2024 12:03:37 +0900 Subject: [PATCH 1187/1215] Add Javadoc for ServerProperties.mimeMappings See gh-39503 --- .../boot/autoconfigure/web/ServerProperties.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index 7704eb283a41..b8b352be8288 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -117,6 +117,9 @@ public class ServerProperties { @NestedConfigurationProperty private final Compression compression = new Compression(); + /** + * Custom MIME mappings in addition to the default MIME mappings. + */ private final MimeMappings mimeMappings = MimeMappings.lazyCopy(MimeMappings.DEFAULT); @NestedConfigurationProperty From d597a4d56b8e87c6cd72a9dd02213e563b54a416 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 12 Feb 2024 12:35:47 +0000 Subject: [PATCH 1188/1215] Correct handling of disable-html-escaping See gh-39504 --- .../boot/autoconfigure/gson/GsonAutoConfiguration.java | 2 +- .../boot/autoconfigure/gson/GsonAutoConfigurationTests.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfiguration.java index f81ecacf007f..94ed69c74517 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfiguration.java @@ -93,7 +93,7 @@ public void customize(GsonBuilder builder) { map.from(properties::getFieldNamingPolicy).to(builder::setFieldNamingPolicy); map.from(properties::getPrettyPrinting).whenTrue().toCall(builder::setPrettyPrinting); map.from(properties::getLenient).whenTrue().toCall(builder::setLenient); - map.from(properties::getDisableHtmlEscaping).whenFalse().toCall(builder::disableHtmlEscaping); + map.from(properties::getDisableHtmlEscaping).whenTrue().toCall(builder::disableHtmlEscaping); map.from(properties::getDateFormat).to(builder::setDateFormat); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfigurationTests.java index e53b013aefaf..179280d8d180 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/gson/GsonAutoConfigurationTests.java @@ -244,7 +244,7 @@ void withoutDisableHtmlEscaping() { void withDisableHtmlEscapingTrue() { this.contextRunner.withPropertyValues("spring.gson.disable-html-escaping:true").run((context) -> { Gson gson = context.getBean(Gson.class); - assertThat(gson.htmlSafe()).isTrue(); + assertThat(gson.htmlSafe()).isFalse(); }); } @@ -252,7 +252,7 @@ void withDisableHtmlEscapingTrue() { void withDisableHtmlEscapingFalse() { this.contextRunner.withPropertyValues("spring.gson.disable-html-escaping:false").run((context) -> { Gson gson = context.getBean(Gson.class); - assertThat(gson.htmlSafe()).isFalse(); + assertThat(gson.htmlSafe()).isTrue(); }); } From b6467ed826818db5d18061496768db3f6758c916 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 12 Feb 2024 14:10:03 +0000 Subject: [PATCH 1189/1215] Adapt to Spring Data Neo4j now requiring a transaction manager See gh-39493 --- .../neo4j/Neo4jDataAutoConfiguration.java | 30 +---------- .../Neo4jReactiveDataAutoConfiguration.java | 10 +++- .../Neo4jRepositoriesAutoConfiguration.java | 3 ++ .../Neo4jTransactionManagerConfiguration.java | 48 +++++++++++++++++ ...jTransactionalComponentsConfiguration.java | 54 +++++++++++++++++++ 5 files changed, 116 insertions(+), 29 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jTransactionManagerConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jTransactionalComponentsConfiguration.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataAutoConfiguration.java index 38cad56487c8..1970e5449725 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jDataAutoConfiguration.java @@ -20,7 +20,6 @@ import org.neo4j.driver.Driver; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -30,23 +29,18 @@ import org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration; import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizationAutoConfiguration; -import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; import org.springframework.data.neo4j.aot.Neo4jManagedTypes; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; -import org.springframework.data.neo4j.core.Neo4jClient; -import org.springframework.data.neo4j.core.Neo4jOperations; -import org.springframework.data.neo4j.core.Neo4jTemplate; import org.springframework.data.neo4j.core.convert.Neo4jConversions; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.schema.Node; import org.springframework.data.neo4j.core.schema.RelationshipProperties; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; -import org.springframework.data.neo4j.repository.config.Neo4jRepositoryConfigurationExtension; import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.TransactionManager; /** * {@link EnableAutoConfiguration Auto-configuration} for Spring Data Neo4j. @@ -64,6 +58,7 @@ @ConditionalOnClass({ Driver.class, Neo4jTransactionManager.class, PlatformTransactionManager.class }) @EnableConfigurationProperties(Neo4jDataProperties.class) @ConditionalOnBean(Driver.class) +@Import({ Neo4jTransactionManagerConfiguration.class, Neo4jTransactionalComponentsConfiguration.class }) public class Neo4jDataAutoConfiguration { @Bean @@ -96,25 +91,4 @@ public DatabaseSelectionProvider databaseSelectionProvider(Neo4jDataProperties p : DatabaseSelectionProvider.getDefaultSelectionProvider(); } - @Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_CLIENT_BEAN_NAME) - @ConditionalOnMissingBean - public Neo4jClient neo4jClient(Driver driver, DatabaseSelectionProvider databaseNameProvider) { - return Neo4jClient.create(driver, databaseNameProvider); - } - - @Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_TEMPLATE_BEAN_NAME) - @ConditionalOnMissingBean(Neo4jOperations.class) - public Neo4jTemplate neo4jTemplate(Neo4jClient neo4jClient, Neo4jMappingContext neo4jMappingContext) { - return new Neo4jTemplate(neo4jClient, neo4jMappingContext); - } - - @Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME) - @ConditionalOnMissingBean(TransactionManager.class) - public Neo4jTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider, - ObjectProvider optionalCustomizers) { - Neo4jTransactionManager transactionManager = new Neo4jTransactionManager(driver, databaseNameProvider); - optionalCustomizers.ifAvailable((customizer) -> customizer.customize((TransactionManager) transactionManager)); - return transactionManager; - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jReactiveDataAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jReactiveDataAutoConfiguration.java index 94fa7836d314..b3c4752c0faf 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jReactiveDataAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jReactiveDataAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * 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. @@ -30,6 +30,7 @@ import org.springframework.data.neo4j.core.ReactiveNeo4jOperations; import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; import org.springframework.data.neo4j.repository.config.ReactiveNeo4jRepositoryConfigurationExtension; import org.springframework.transaction.ReactiveTransactionManager; @@ -68,4 +69,11 @@ public ReactiveNeo4jTemplate reactiveNeo4jTemplate(ReactiveNeo4jClient neo4jClie return new ReactiveNeo4jTemplate(neo4jClient, neo4jMappingContext); } + @Bean(ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME) + @ConditionalOnMissingBean(ReactiveTransactionManager.class) + ReactiveNeo4jTransactionManager rectiveNeo4jTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseNameProvider) { + return new ReactiveNeo4jTransactionManager(driver, databaseNameProvider); + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jRepositoriesAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jRepositoriesAutoConfiguration.java index 05f2c7729e14..321407c2c6ff 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jRepositoriesAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jRepositoriesAutoConfiguration.java @@ -20,11 +20,13 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.data.ConditionalOnRepositoryType; import org.springframework.boot.autoconfigure.data.RepositoryType; import org.springframework.context.annotation.Import; +import org.springframework.data.neo4j.core.Neo4jTemplate; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.data.neo4j.repository.config.Neo4jRepositoryConfigurationExtension; @@ -52,6 +54,7 @@ @AutoConfiguration(after = Neo4jDataAutoConfiguration.class) @ConditionalOnClass({ Driver.class, Neo4jRepository.class }) @ConditionalOnMissingBean({ Neo4jRepositoryFactoryBean.class, Neo4jRepositoryConfigurationExtension.class }) +@ConditionalOnBean(Neo4jTemplate.class) @ConditionalOnRepositoryType(store = "neo4j", type = RepositoryType.IMPERATIVE) @Import(Neo4jRepositoriesRegistrar.class) public class Neo4jRepositoriesAutoConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jTransactionManagerConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jTransactionManagerConfiguration.java new file mode 100644 index 000000000000..53fb41d9954c --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jTransactionManagerConfiguration.java @@ -0,0 +1,48 @@ +/* + * 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.data.neo4j; + +import org.neo4j.driver.Driver; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.core.DatabaseSelectionProvider; +import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; +import org.springframework.data.neo4j.repository.config.Neo4jRepositoryConfigurationExtension; +import org.springframework.transaction.TransactionManager; + +/** + * Configuration for a Neo4j-backed {@link TransactionManager}. + * + * @author Andy Wilkinson + */ +@Configuration(proxyBeanMethods = false) +class Neo4jTransactionManagerConfiguration { + + @Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME) + @ConditionalOnMissingBean(TransactionManager.class) + Neo4jTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider, + ObjectProvider optionalCustomizers) { + Neo4jTransactionManager transactionManager = new Neo4jTransactionManager(driver, databaseNameProvider); + optionalCustomizers.ifAvailable((customizer) -> customizer.customize((TransactionManager) transactionManager)); + return transactionManager; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jTransactionalComponentsConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jTransactionalComponentsConfiguration.java new file mode 100644 index 000000000000..d225e3cdbef3 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/neo4j/Neo4jTransactionalComponentsConfiguration.java @@ -0,0 +1,54 @@ +/* + * 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.data.neo4j; + +import org.neo4j.driver.Driver; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.core.DatabaseSelectionProvider; +import org.springframework.data.neo4j.core.Neo4jClient; +import org.springframework.data.neo4j.core.Neo4jOperations; +import org.springframework.data.neo4j.core.Neo4jTemplate; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; +import org.springframework.data.neo4j.repository.config.Neo4jRepositoryConfigurationExtension; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * Neo4j components that require a {@link PlatformTransactionManager}. + * + * @author Andy Wilkinson + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnBean(PlatformTransactionManager.class) +class Neo4jTransactionalComponentsConfiguration { + + @Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_CLIENT_BEAN_NAME) + @ConditionalOnMissingBean + Neo4jClient neo4jClient(Driver driver, DatabaseSelectionProvider databaseNameProvider) { + return Neo4jClient.create(driver, databaseNameProvider); + } + + @Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_NEO4J_TEMPLATE_BEAN_NAME) + @ConditionalOnMissingBean(Neo4jOperations.class) + Neo4jTemplate neo4jTemplate(Neo4jClient neo4jClient, Neo4jMappingContext neo4jMappingContext) { + return new Neo4jTemplate(neo4jClient, neo4jMappingContext); + } + +} From a0cb2bdeaf6cb3c3cbef4a8aea37cc4ac0ae3b82 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 13 Feb 2024 17:34:47 +0000 Subject: [PATCH 1190/1215] Upgrade to Neo4j Java Driver 5.17.0 Closes gh-39534 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 3a02e99f5d40..865e03e9f786 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1070,7 +1070,7 @@ bom { ] } } - library("Neo4j Java Driver", "5.15.0") { + library("Neo4j Java Driver", "5.17.0") { alignWithVersion { from "org.springframework.data:spring-data-neo4j" managedBy "Spring Data Bom" From b2c98a80b6f64b595395ddb93b81cc68f3d61f84 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 13 Feb 2024 17:36:46 +0000 Subject: [PATCH 1191/1215] Upgrade to Neo4j Java Driver 5.17.0 Closes gh-39535 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f0f4eeabadbc..21de4316c4dc 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1071,7 +1071,7 @@ bom { ] } } - library("Neo4j Java Driver", "5.15.0") { + library("Neo4j Java Driver", "5.17.0") { alignWithVersion { from "org.springframework.data:spring-data-neo4j" managedBy "Spring Data Bom" From 720e9cef162ab406e5d49648a55eb44fd212397e Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Tue, 13 Feb 2024 13:10:55 -0600 Subject: [PATCH 1192/1215] Make RestTemplateBuilder more compatible with RestClient.Builder When Spring Framework builds a `RestClient` from a `RestTemplate`, it will use any `UriTemplateHandler` that has been set on the `RestTemplate` if the provided `UriTemplateHandler` is also a `UriBuilderFactory`. Prior to this commit, Spring Boot's `RestTemplateBuilder#rootUri` set a `UriTemplateHandler` on the created `RestTemplate`, but it was not a `UriBuilderFactory` so `RestClient` would not consider it. With this commit, `RestTemplateBuilder#rootUri` sets a `UriTemplateHandler` that is also a `UriBuilderFactory` so that any root URI that is set on the `RestTemplateBuilder` will be applied to a `RestClient` also. Fixes gh-39317 --- ...estClientWithRestTemplateBuilderTests.java | 68 ++++++++++++++++++ .../RestClientWithRestTemplateTests.java | 72 +++++++++++++++++++ .../test/web/client/TestRestTemplate.java | 4 +- .../boot/web/client/RestTemplateBuilder.java | 10 ++- .../web/client/RootUriBuilderFactory.java | 65 +++++++++++++++++ .../web/client/RootUriTemplateHandler.java | 15 +++- .../web/client/RestTemplateBuilderTests.java | 6 +- .../client/RootUriBuilderFactoryTests.java | 43 +++++++++++ .../client/RootUriTemplateHandlerTests.java | 3 +- 9 files changed, 271 insertions(+), 15 deletions(-) create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientWithRestTemplateBuilderTests.java create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientWithRestTemplateTests.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RootUriBuilderFactory.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RootUriBuilderFactoryTests.java diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientWithRestTemplateBuilderTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientWithRestTemplateBuilderTests.java new file mode 100644 index 000000000000..6a216e114e81 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientWithRestTemplateBuilderTests.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.test.autoconfigure.web.client; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.Builder; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * Tests for building a {@link RestClient} from a {@link RestTemplateBuilder}. + * + * @author Scott Frederick + */ +class RestClientWithRestTemplateBuilderTests { + + @Test + void buildUsingRestTemplateBuilderRootUri() { + RestTemplate restTemplate = new RestTemplateBuilder().rootUri("https://resttemplate.example.com").build(); + RestClient.Builder builder = RestClient.builder(restTemplate); + RestClient client = buildMockedClient(builder, "https://resttemplate.example.com/test"); + assertThat(client.get().uri("/test").retrieve().toBodilessEntity().getStatusCode().is2xxSuccessful()).isTrue(); + } + + @Test + void buildUsingRestClientBuilderBaseUrl() { + RestTemplate restTemplate = new RestTemplateBuilder().build(); + RestClient.Builder builder = RestClient.builder(restTemplate).baseUrl("https://restclient.example.com"); + RestClient client = buildMockedClient(builder, "https://restclient.example.com/test"); + assertThat(client.get().uri("/test").retrieve().toBodilessEntity().getStatusCode().is2xxSuccessful()).isTrue(); + } + + @Test + void buildRestTemplateBuilderRootUriAndRestClientBuilderBaseUrl() { + RestTemplate restTemplate = new RestTemplateBuilder().rootUri("https://resttemplate.example.com").build(); + RestClient.Builder builder = RestClient.builder(restTemplate).baseUrl("https://restclient.example.com"); + RestClient client = buildMockedClient(builder, "https://resttemplate.example.com/test"); + assertThat(client.get().uri("/test").retrieve().toBodilessEntity().getStatusCode().is2xxSuccessful()).isTrue(); + } + + private RestClient buildMockedClient(Builder builder, String url) { + MockRestServiceServer server = MockRestServiceServer.bindTo(builder).build(); + server.expect(requestTo(url)).andRespond(withSuccess()); + return builder.build(); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientWithRestTemplateTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientWithRestTemplateTests.java new file mode 100644 index 000000000000..556991d9347f --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/web/client/RestClientWithRestTemplateTests.java @@ -0,0 +1,72 @@ +/* + * 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.test.autoconfigure.web.client; + +import org.junit.jupiter.api.Test; + +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.Builder; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.DefaultUriBuilderFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * Tests for building a {@link RestClient} from a {@link RestTemplate}. + * + * @author Scott Frederick + */ +class RestClientWithRestTemplateTests { + + @Test + void buildUsingRestTemplateUriTemplateHandler() { + RestTemplate restTemplate = new RestTemplate(); + DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("https://resttemplate.example.com"); + restTemplate.setUriTemplateHandler(uriBuilderFactory); + Builder builder = RestClient.builder(restTemplate); + RestClient client = buildMockedClient(builder, "https://resttemplate.example.com/test"); + assertThat(client.get().uri("/test").retrieve().toBodilessEntity().getStatusCode().is2xxSuccessful()).isTrue(); + } + + @Test + void buildUsingRestClientBuilderBaseUrl() { + RestTemplate restTemplate = new RestTemplate(); + Builder builder = RestClient.builder(restTemplate).baseUrl("https://restclient.example.com"); + RestClient client = buildMockedClient(builder, "https://restclient.example.com/test"); + assertThat(client.get().uri("/test").retrieve().toBodilessEntity().getStatusCode().is2xxSuccessful()).isTrue(); + } + + @Test + void buildUsingRestTemplateUriTemplateHandlerAndRestClientBuilderBaseUrl() { + RestTemplate restTemplate = new RestTemplate(); + DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory("https://resttemplate.example.com"); + restTemplate.setUriTemplateHandler(uriBuilderFactory); + Builder builder = RestClient.builder(restTemplate).baseUrl("https://restclient.example.com"); + RestClient client = buildMockedClient(builder, "https://resttemplate.example.com/test"); + assertThat(client.get().uri("/test").retrieve().toBodilessEntity().getStatusCode().is2xxSuccessful()).isTrue(); + } + + private RestClient buildMockedClient(Builder builder, String url) { + MockRestServiceServer server = MockRestServiceServer.bindTo(builder).build(); + server.expect(requestTo(url)).andRespond(withSuccess()); + return builder.build(); + } + +} diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplate.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplate.java index 901b999bbf37..5f44bac53b47 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplate.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplate.java @@ -171,8 +171,8 @@ public void setUriTemplateHandler(UriTemplateHandler handler) { } /** - * Returns the root URI applied by a {@link RootUriTemplateHandler} or {@code ""} if - * the root URI is not available. + * Returns the root URI applied by {@link RestTemplateBuilder#rootUri(String)} or + * {@code ""} if the root URI has not been applied. * @return the root URI */ public String getRootUri() { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilder.java index a10a9ce10022..49d8ac59f60b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -150,10 +150,8 @@ public RestTemplateBuilder detectRequestFactory(boolean detectRequestFactory) { /** * Set a root URL that should be applied to each request that starts with {@code '/'}. - * Since this works by adding a {@link UriTemplateHandler} to the - * {@link RestTemplate}, the root URL will only apply when {@code String} variants of - * the {@link RestTemplate} methods are used for specifying the request URL. See - * {@link RootUriTemplateHandler} for details. + * The root URL will only apply when {@code String} variants of the + * {@link RestTemplate} methods are used for specifying the request URL. * @param rootUri the root URI or {@code null} * @return a new builder instance */ @@ -639,7 +637,7 @@ public T configure(T restTemplate) { restTemplate.setErrorHandler(this.errorHandler); } if (this.rootUri != null) { - RootUriTemplateHandler.addTo(restTemplate, this.rootUri); + RootUriBuilderFactory.applyTo(restTemplate, this.rootUri); } restTemplate.getInterceptors().addAll(this.interceptors); if (!CollectionUtils.isEmpty(this.customizers)) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RootUriBuilderFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RootUriBuilderFactory.java new file mode 100644 index 000000000000..da2945a7f475 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RootUriBuilderFactory.java @@ -0,0 +1,65 @@ +/* + * 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.web.client; + +import org.springframework.util.Assert; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriBuilder; +import org.springframework.web.util.UriBuilderFactory; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriTemplateHandler; + +/** + * {@link UriBuilderFactory} to set the root for URI that starts with {@code '/'}. + * + * @author Scott Frederick + * @since 3.2.3 + */ +public class RootUriBuilderFactory extends RootUriTemplateHandler implements UriBuilderFactory { + + @SuppressWarnings("removal") + RootUriBuilderFactory(String rootUri) { + super(rootUri); + } + + @SuppressWarnings("removal") + RootUriBuilderFactory(String rootUri, UriTemplateHandler delegate) { + super(rootUri, delegate); + } + + @Override + public UriBuilder uriString(String uriTemplate) { + return UriComponentsBuilder.fromUriString(apply(uriTemplate)); + } + + @Override + public UriBuilder builder() { + return UriComponentsBuilder.newInstance(); + } + + /** + * Apply a {@link RootUriBuilderFactory} instance to the given {@link RestTemplate}. + * @param restTemplate the {@link RestTemplate} to add the builder factory to + * @param rootUri the root URI + */ + static void applyTo(RestTemplate restTemplate, String rootUri) { + Assert.notNull(restTemplate, "RestTemplate must not be null"); + RootUriBuilderFactory handler = new RootUriBuilderFactory(rootUri, restTemplate.getUriTemplateHandler()); + restTemplate.setUriTemplateHandler(handler); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RootUriTemplateHandler.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RootUriTemplateHandler.java index e52c59b95fe6..4a007a6fcaa7 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RootUriTemplateHandler.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RootUriTemplateHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * 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. @@ -30,6 +30,7 @@ * {@link UriTemplateHandler} to set the root for URI that starts with {@code '/'}. * * @author Phillip Webb + * @author Scott Frederick * @since 1.4.0 */ public class RootUriTemplateHandler implements UriTemplateHandler { @@ -47,7 +48,9 @@ protected RootUriTemplateHandler(UriTemplateHandler handler) { /** * Create a new {@link RootUriTemplateHandler} instance. * @param rootUri the root URI to be used to prefix relative URLs + * @deprecated since 3.2.3 for removal in 3.4.0, with no replacement */ + @Deprecated(since = "3.2.3", forRemoval = true) public RootUriTemplateHandler(String rootUri) { this(rootUri, new DefaultUriBuilderFactory()); } @@ -55,8 +58,10 @@ public RootUriTemplateHandler(String rootUri) { /** * Create a new {@link RootUriTemplateHandler} instance. * @param rootUri the root URI to be used to prefix relative URLs - * @param handler the delegate handler + * @param handler the handler handler + * @deprecated since 3.2.3 for removal in 3.4.0, with no replacement */ + @Deprecated(since = "3.2.3", forRemoval = true) public RootUriTemplateHandler(String rootUri, UriTemplateHandler handler) { Assert.notNull(rootUri, "RootUri must not be null"); Assert.notNull(handler, "Handler must not be null"); @@ -74,7 +79,7 @@ public URI expand(String uriTemplate, Object... uriVariables) { return this.handler.expand(apply(uriTemplate), uriVariables); } - private String apply(String uriTemplate) { + String apply(String uriTemplate) { if (StringUtils.startsWithIgnoreCase(uriTemplate, "/")) { return getRootUri() + uriTemplate; } @@ -91,7 +96,9 @@ public String getRootUri() { * @param wrapper the wrapper to apply to the delegate URI template handler * @return the new handler * @since 2.3.10 + * @deprecated since 3.2.3 for removal in 3.4.0, with no replacement */ + @Deprecated(since = "3.2.3", forRemoval = true) public RootUriTemplateHandler withHandlerWrapper(Function wrapper) { return new RootUriTemplateHandler(getRootUri(), wrapper.apply(this.handler)); } @@ -101,7 +108,9 @@ public RootUriTemplateHandler withHandlerWrapper(Function { assertThat(restTemplate.getInterceptors()).hasSize(1); assertThat(restTemplate.getMessageConverters()).contains(this.messageConverter); - assertThat(restTemplate.getUriTemplateHandler()).isInstanceOf(RootUriTemplateHandler.class); + assertThat(restTemplate.getUriTemplateHandler()).isInstanceOf(RootUriBuilderFactory.class); assertThat(restTemplate.getErrorHandler()).isEqualTo(errorHandler); ClientHttpRequestFactory actualRequestFactory = restTemplate.getRequestFactory(); assertThat(actualRequestFactory).isInstanceOf(InterceptingClientHttpRequestFactory.class); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RootUriBuilderFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RootUriBuilderFactoryTests.java new file mode 100644 index 000000000000..1a77773cd742 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RootUriBuilderFactoryTests.java @@ -0,0 +1,43 @@ +/* + * 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.web.client; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.junit.jupiter.api.Test; + +import org.springframework.web.util.UriBuilder; +import org.springframework.web.util.UriBuilderFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link RootUriBuilderFactory}. + * + * @author Scott Frederick + */ +class RootUriBuilderFactoryTests { + + @Test + void uriStringPrefixesRoot() throws URISyntaxException { + UriBuilderFactory builderFactory = new RootUriBuilderFactory("https://example.com"); + UriBuilder builder = builderFactory.uriString("/hello"); + assertThat(builder.build()).isEqualTo(new URI("https://example.com/hello")); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RootUriTemplateHandlerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RootUriTemplateHandlerTests.java index 2e8140008920..56288dc3d71f 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RootUriTemplateHandlerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/RootUriTemplateHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -43,6 +43,7 @@ * @author Phillip Webb */ @ExtendWith(MockitoExtension.class) +@SuppressWarnings("removal") class RootUriTemplateHandlerTests { private URI uri; From bb87faf237b21bfa6fe36e00227bd1f88e522d58 Mon Sep 17 00:00:00 2001 From: BenchmarkingBuffalo <46448799+benchmarkingbuffalo@users.noreply.github.com> Date: Fri, 9 Feb 2024 09:37:36 +0100 Subject: [PATCH 1193/1215] Add BatchTransactionManager annotation Add a new @BatchTransactionManager annotation for marking a PlatformTransactionManager that should be used in batch processing. See gh-39473 --- .../batch/BatchAutoConfiguration.java | 7 ++- .../batch/BatchTransactionManager.java | 44 +++++++++++++++++++ .../batch/BatchAutoConfigurationTests.java | 36 +++++++++++++++ .../src/docs/asciidoc/howto/batch.adoc | 8 ++++ 4 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchTransactionManager.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java index 22d3ffb19b34..7a2e4e2a5eb3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfiguration.java @@ -65,6 +65,7 @@ * @author Kazuki Shimizu * @author Mahmoud Ben Hassine * @author Lars Uffmann + * @author Lasse Wulff * @since 1.0.0 */ @AutoConfiguration(after = { HibernateJpaAutoConfiguration.class, TransactionAutoConfiguration.class }) @@ -108,11 +109,13 @@ static class SpringBootBatchConfiguration extends DefaultBatchConfiguration { private final ExecutionContextSerializer executionContextSerializer; SpringBootBatchConfiguration(DataSource dataSource, @BatchDataSource ObjectProvider batchDataSource, - PlatformTransactionManager transactionManager, BatchProperties properties, + PlatformTransactionManager transactionManager, + @BatchTransactionManager ObjectProvider batchTransactionManager, + BatchProperties properties, ObjectProvider batchConversionServiceCustomizers, ObjectProvider executionContextSerializer) { this.dataSource = batchDataSource.getIfAvailable(() -> dataSource); - this.transactionManager = transactionManager; + this.transactionManager = batchTransactionManager.getIfAvailable(() -> transactionManager); this.properties = properties; this.batchConversionServiceCustomizers = batchConversionServiceCustomizers.orderedStream().toList(); this.executionContextSerializer = executionContextSerializer diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchTransactionManager.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchTransactionManager.java new file mode 100644 index 000000000000..a2ec189a10ea --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchTransactionManager.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2014 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.batch; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Primary; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * Qualifier annotation for a {@link PlatformTransactionManager + * PlatformTransactionManager} to be injected into Batch auto-configuration. Can be used + * on a secondary {@link PlatformTransactionManager PlatformTransactionManager}, if there + * is another one marked as {@link Primary @Primary}. + * + * @author Lasse Wulff + * @since 3.3.0 + */ +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Qualifier +public @interface BatchTransactionManager { + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java index baab590b90d1..0f55506d5ed7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java @@ -80,6 +80,7 @@ import org.springframework.context.annotation.Primary; import org.springframework.core.annotation.Order; import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.integration.transaction.PseudoTransactionManager; import org.springframework.jdbc.BadSqlGrammarException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; @@ -102,6 +103,7 @@ * @author Kazuki Shimizu * @author Mahmoud Ben Hassine * @author Lars Uffmann + * @author Lasse Wulff */ @ExtendWith(OutputCaptureExtension.class) class BatchAutoConfigurationTests { @@ -351,6 +353,18 @@ void testBatchDataSource() { }); } + @Test + void testBatchTransactionManager() { + this.contextRunner.withUserConfiguration(TestConfiguration.class, BatchTransactionManagerConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(SpringBootBatchConfiguration.class); + PlatformTransactionManager batchTransactionManager = context.getBean("batchTransactionManager", + PlatformTransactionManager.class); + assertThat(context.getBean(SpringBootBatchConfiguration.class).getTransactionManager()) + .isEqualTo(batchTransactionManager); + }); + } + @Test void jobRepositoryBeansDependOnBatchDataSourceInitializer() { this.contextRunner.withUserConfiguration(TestConfiguration.class, EmbeddedDataSourceConfiguration.class) @@ -519,6 +533,28 @@ public DataSource batchDataSource() { } + @Configuration(proxyBeanMethods = false) + protected static class BatchTransactionManagerConfiguration { + + @Bean + public DataSource dataSource() { + return DataSourceBuilder.create().url("jdbc:hsqldb:mem:database").username("sa").build(); + } + + @Bean + @Primary + public PlatformTransactionManager normalTransactionManager() { + return new PseudoTransactionManager(); + } + + @BatchTransactionManager + @Bean + public PlatformTransactionManager batchTransactionManager() { + return new PseudoTransactionManager(); + } + + } + @Configuration(proxyBeanMethods = false) static class EmptyConfiguration { diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc index c03c81520509..4bdf2930e2f3 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc @@ -19,6 +19,14 @@ For more info about Spring Batch, see the {spring-batch}[Spring Batch project pa +[[howto.batch.specifying-a-transaction-manager]] +=== Specifying a Batch Transaction Manager +Similar to <> you can also define a `PlatformTransactionManager` +for use in the batch processing by marking it as `@BatchTransactionManager`. +If you do so and want two transaction managers, remember to mark the other one as `@Primary`. + + + [[howto.batch.running-jobs-on-startup]] === Running Spring Batch Jobs on Startup Spring Batch auto-configuration is enabled by adding `spring-boot-starter-batch` to your application's classpath. From 22952c30577443aa23f2f801a8569c25149cad15 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Tue, 13 Feb 2024 15:37:32 -0600 Subject: [PATCH 1194/1215] Polish "Add BatchTransactionManager annotation" See gh-39473 --- .../batch/BatchTransactionManager.java | 7 ++--- .../batch/BatchAutoConfigurationTests.java | 30 +++++++++++++++++-- .../src/docs/asciidoc/howto/batch.adoc | 3 +- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchTransactionManager.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchTransactionManager.java index a2ec189a10ea..ee1360c54183 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchTransactionManager.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchTransactionManager.java @@ -27,10 +27,9 @@ import org.springframework.transaction.PlatformTransactionManager; /** - * Qualifier annotation for a {@link PlatformTransactionManager - * PlatformTransactionManager} to be injected into Batch auto-configuration. Can be used - * on a secondary {@link PlatformTransactionManager PlatformTransactionManager}, if there - * is another one marked as {@link Primary @Primary}. + * Qualifier annotation for a {@link PlatformTransactionManager} to be injected into Batch + * auto-configuration. Can be used on a secondary {@link PlatformTransactionManager}, if + * there is another one marked as {@link Primary @Primary}. * * @author Lasse Wulff * @since 3.3.0 diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java index 0f55506d5ed7..d2a28accc725 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java @@ -80,13 +80,16 @@ import org.springframework.context.annotation.Primary; import org.springframework.core.annotation.Order; import org.springframework.core.convert.support.ConfigurableConversionService; -import org.springframework.integration.transaction.PseudoTransactionManager; import org.springframework.jdbc.BadSqlGrammarException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.jdbc.datasource.init.DatabasePopulator; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -544,13 +547,34 @@ public DataSource dataSource() { @Bean @Primary public PlatformTransactionManager normalTransactionManager() { - return new PseudoTransactionManager(); + return new TestTransactionManager(); } @BatchTransactionManager @Bean public PlatformTransactionManager batchTransactionManager() { - return new PseudoTransactionManager(); + return new TestTransactionManager(); + } + + } + + static class TestTransactionManager extends AbstractPlatformTransactionManager { + + @Override + protected Object doGetTransaction() throws TransactionException { + return null; + } + + @Override + protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { + } + + @Override + protected void doCommit(DefaultTransactionStatus status) throws TransactionException { + } + + @Override + protected void doRollback(DefaultTransactionStatus status) throws TransactionException { } } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc index 4bdf2930e2f3..ef4e8b90f710 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/batch.adoc @@ -21,8 +21,7 @@ For more info about Spring Batch, see the {spring-batch}[Spring Batch project pa [[howto.batch.specifying-a-transaction-manager]] === Specifying a Batch Transaction Manager -Similar to <> you can also define a `PlatformTransactionManager` -for use in the batch processing by marking it as `@BatchTransactionManager`. +Similar to <>, you can define a `PlatformTransactionManager` for use in the batch processing by marking it as `@BatchTransactionManager`. If you do so and want two transaction managers, remember to mark the other one as `@Primary`. From 256f9fe83af13066725802dd692de6dfe8dd046f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Feb 2024 18:25:51 +0000 Subject: [PATCH 1195/1215] Only configure WebFlux blocking executor when using virtual threads Fixes gh-39469 --- .../reactive/WebFluxAutoConfiguration.java | 12 ++++-- .../WebFluxAutoConfigurationTests.java | 38 ++++++++++++++----- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java index e9ebef362499..cab4dd72b436 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -34,6 +34,7 @@ import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration; import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders; +import org.springframework.boot.autoconfigure.thread.Threading; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain; @@ -55,6 +56,7 @@ import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.format.FormatterRegistry; import org.springframework.format.support.FormattingConversionService; @@ -149,6 +151,8 @@ public static class WebFluxConfig implements WebFluxConfigurer { private static final Log logger = LogFactory.getLog(WebFluxConfig.class); + private final Environment environment; + private final Resources resourceProperties; private final WebFluxProperties webFluxProperties; @@ -163,11 +167,12 @@ public static class WebFluxConfig implements WebFluxConfigurer { private final ObjectProvider viewResolvers; - public WebFluxConfig(WebProperties webProperties, WebFluxProperties webFluxProperties, + public WebFluxConfig(Environment environment, WebProperties webProperties, WebFluxProperties webFluxProperties, ListableBeanFactory beanFactory, ObjectProvider resolvers, ObjectProvider codecCustomizers, ObjectProvider resourceHandlerRegistrationCustomizer, ObjectProvider viewResolvers) { + this.environment = environment; this.resourceProperties = webProperties.getResources(); this.webFluxProperties = webFluxProperties; this.beanFactory = beanFactory; @@ -189,7 +194,8 @@ public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { @Override public void configureBlockingExecution(BlockingExecutionConfigurer configurer) { - if (this.beanFactory.containsBean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)) { + if (Threading.VIRTUAL.isActive(this.environment) && this.beanFactory + .containsBean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)) { Object taskExecutor = this.beanFactory .getBean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME); if (taskExecutor instanceof AsyncTaskExecutor asyncTaskExecutor) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java index cf40c58a52c9..53731f72fcfb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -39,6 +39,8 @@ import org.assertj.core.api.Assertions; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -687,8 +689,20 @@ void problemDetailsExceptionHandlerIsOrderedAt0() { } @Test - void asyncTaskExecutorWithApplicationTaskExecutor() { + void asyncTaskExecutorWithPlatformThreadsAndApplicationTaskExecutor() { this.contextRunner.withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class)) + .run((context) -> { + assertThat(context).hasSingleBean(AsyncTaskExecutor.class); + assertThat(context.getBean(RequestMappingHandlerAdapter.class)).extracting("scheduler.executor") + .isNull(); + }); + } + + @Test + @EnabledForJreRange(min = JRE.JAVA_21) + void asyncTaskExecutorWithVirtualThreadsAndApplicationTaskExecutor() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true") + .withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class)) .run((context) -> { assertThat(context).hasSingleBean(AsyncTaskExecutor.class); assertThat(context.getBean(RequestMappingHandlerAdapter.class)).extracting("scheduler.executor") @@ -697,8 +711,10 @@ void asyncTaskExecutorWithApplicationTaskExecutor() { } @Test - void asyncTaskExecutorWithNonMatchApplicationTaskExecutorBean() { - this.contextRunner.withUserConfiguration(CustomApplicationTaskExecutorConfig.class) + @EnabledForJreRange(min = JRE.JAVA_21) + void asyncTaskExecutorWithVirtualThreadsAndNonMatchApplicationTaskExecutorBean() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true") + .withUserConfiguration(CustomApplicationTaskExecutorConfig.class) .withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class)) .run((context) -> { assertThat(context).doesNotHaveBean(AsyncTaskExecutor.class); @@ -708,8 +724,10 @@ void asyncTaskExecutorWithNonMatchApplicationTaskExecutorBean() { } @Test - void asyncTaskExecutorWithWebFluxConfigurerCanOverrideExecutor() { - this.contextRunner.withUserConfiguration(CustomAsyncTaskExecutorConfigurer.class) + @EnabledForJreRange(min = JRE.JAVA_21) + void asyncTaskExecutorWithVirtualThreadsAndWebFluxConfigurerCanOverrideExecutor() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true") + .withUserConfiguration(CustomAsyncTaskExecutorConfigurer.class) .withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class)) .run((context) -> assertThat(context.getBean(RequestMappingHandlerAdapter.class)) .extracting("scheduler.executor") @@ -717,13 +735,15 @@ void asyncTaskExecutorWithWebFluxConfigurerCanOverrideExecutor() { } @Test - void asyncTaskExecutorWithCustomNonApplicationTaskExecutor() { - this.contextRunner.withUserConfiguration(CustomAsyncTaskExecutorConfig.class) + @EnabledForJreRange(min = JRE.JAVA_21) + void asyncTaskExecutorWithVirtualThreadsAndCustomNonApplicationTaskExecutor() { + this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true") + .withUserConfiguration(CustomAsyncTaskExecutorConfig.class) .withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class)) .run((context) -> { assertThat(context).hasSingleBean(AsyncTaskExecutor.class); assertThat(context.getBean(RequestMappingHandlerAdapter.class)).extracting("scheduler.executor") - .isNotSameAs(context.getBean("customTaskExecutor")); + .isNull(); }); } From 0ba1496f61c418abb9008d14c3c0bfa4adc2024b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 14 Feb 2024 17:58:24 +0000 Subject: [PATCH 1196/1215] Upgrade to Commons Codec 1.16.1 Closes gh-39566 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d1786b540b68..d1f14de5e3dc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ org.gradle.parallel=true org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 assertjVersion=3.24.2 -commonsCodecVersion=1.16.0 +commonsCodecVersion=1.16.1 commonsCompressVersion=1.21 hamcrestVersion=2.2 jacksonVersion=2.15.3 From ec5b259381daf282a163f468a90717bc007d259d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 14 Feb 2024 17:58:29 +0000 Subject: [PATCH 1197/1215] Upgrade to Dropwizard Metrics 4.2.25 Closes gh-39567 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 865e03e9f786..c498d93bcb76 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -247,7 +247,7 @@ bom { ] } } - library("Dropwizard Metrics", "4.2.23") { + library("Dropwizard Metrics", "4.2.25") { group("io.dropwizard.metrics") { imports = [ "metrics-bom" From c0ec714e0a290923ce8503bd6a246240763f9500 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 14 Feb 2024 17:58:33 +0000 Subject: [PATCH 1198/1215] Upgrade to Groovy 4.0.18 Closes gh-39568 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index c498d93bcb76..f675f1149cad 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -339,7 +339,7 @@ bom { ] } } - library("Groovy", "4.0.17") { + library("Groovy", "4.0.18") { group("org.apache.groovy") { imports = [ "groovy-bom" From 7dc24370c6538e242aa0eaca59372ccacdd1af50 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 14 Feb 2024 17:58:38 +0000 Subject: [PATCH 1199/1215] Upgrade to Hibernate 6.4.4.Final Closes gh-39569 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f675f1149cad..aa43949e9c54 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -377,7 +377,7 @@ bom { ] } } - library("Hibernate", "6.4.1.Final") { + library("Hibernate", "6.4.4.Final") { group("org.hibernate.orm") { modules = [ "hibernate-agroal", From 4ee4215c8b49e0d40fe97ee86f77d1fa20c290fd Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 14 Feb 2024 17:58:43 +0000 Subject: [PATCH 1200/1215] Upgrade to Infinispan 14.0.24.Final Closes gh-39570 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index aa43949e9c54..a98b6069ead9 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -465,7 +465,7 @@ bom { ] } } - library("Infinispan", "14.0.21.Final") { + library("Infinispan", "14.0.24.Final") { group("org.infinispan") { imports = [ "infinispan-bom" From 6ea6aa42f7b3ecae4f634e79b344632f0ee5d7ed Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 14 Feb 2024 17:58:47 +0000 Subject: [PATCH 1201/1215] Upgrade to Janino 3.1.12 Closes gh-39571 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index a98b6069ead9..f33dc42d82f0 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -606,7 +606,7 @@ bom { ] } } - library("Janino", "3.1.11") { + library("Janino", "3.1.12") { group("org.codehaus.janino") { modules = [ "commons-compiler", From 3a9987371cc60fd13cea98d7c2c2e412362db9ab Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 14 Feb 2024 17:58:52 +0000 Subject: [PATCH 1202/1215] Upgrade to Jetty Reactive HTTPClient 4.0.3 Closes gh-39572 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index f33dc42d82f0..a834df1674ac 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -671,7 +671,7 @@ bom { ] } } - library("Jetty Reactive HTTPClient", "4.0.1") { + library("Jetty Reactive HTTPClient", "4.0.3") { group("org.eclipse.jetty") { modules = [ "jetty-reactive-httpclient" From 1d4f0e78e6a7e2296bd07330ea737de579489cea Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 14 Feb 2024 17:58:57 +0000 Subject: [PATCH 1203/1215] Upgrade to Jetty 12.0.6 Closes gh-39573 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index a834df1674ac..e59982595701 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -678,7 +678,7 @@ bom { ] } } - library("Jetty", "12.0.5") { + library("Jetty", "12.0.6") { group("org.eclipse.jetty.ee10") { imports = [ "jetty-ee10-bom" From 5c6f200b85a44deff1117a8f75bc877041f5e7ed Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 14 Feb 2024 17:59:01 +0000 Subject: [PATCH 1204/1215] Upgrade to jOOQ 3.18.10 Closes gh-39574 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index e59982595701..4516c4ae8075 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -697,7 +697,7 @@ bom { ] } } - library("jOOQ", "3.18.9") { + library("jOOQ", "3.18.10") { group("org.jooq") { modules = [ "jooq", From d16c3db912be922d0714afbe9cf868f3481c3ac3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 14 Feb 2024 17:59:06 +0000 Subject: [PATCH 1205/1215] Upgrade to JUnit Jupiter 5.10.2 Closes gh-39575 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d1f14de5e3dc..7121b3e74eec 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ commonsCodecVersion=1.16.1 commonsCompressVersion=1.21 hamcrestVersion=2.2 jacksonVersion=2.15.3 -junitJupiterVersion=5.10.1 +junitJupiterVersion=5.10.2 kotlinVersion=1.9.22 mavenVersion=3.9.4 nativeBuildToolsVersion=0.9.28 From a7d78aee8bbe9241375ddffb5f840ae7a6c20a50 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 14 Feb 2024 17:59:06 +0000 Subject: [PATCH 1206/1215] Upgrade to Micrometer 1.12.3 Closes gh-39474 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 4516c4ae8075..0bee836f9532 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -995,7 +995,7 @@ bom { ] } } - library("Micrometer", "1.12.3-SNAPSHOT") { + library("Micrometer", "1.12.3") { considerSnapshots() group("io.micrometer") { modules = [ From 6536ee973a5a8d59c23348667df46bfbdcc893db Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 14 Feb 2024 17:59:07 +0000 Subject: [PATCH 1207/1215] Upgrade to Micrometer Tracing 1.2.3 Closes gh-39475 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 0bee836f9532..89f6485d43e5 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1008,7 +1008,7 @@ bom { ] } } - library("Micrometer Tracing", "1.2.3-SNAPSHOT") { + library("Micrometer Tracing", "1.2.3") { considerSnapshots() calendarName = "Tracing" group("io.micrometer") { From a61a7b9d169ce7324ee20ee64ae7eb11aded8d3a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 14 Feb 2024 17:59:12 +0000 Subject: [PATCH 1208/1215] Upgrade to Netty 4.1.107.Final Closes gh-39576 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 89f6485d43e5..30b3ddba0efd 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1081,7 +1081,7 @@ bom { ] } } - library("Netty", "4.1.105.Final") { + library("Netty", "4.1.107.Final") { group("io.netty") { imports = [ "netty-bom" From 24572e46c9854b6e6a5e731c648a9d3b2ab701fc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 14 Feb 2024 17:59:16 +0000 Subject: [PATCH 1209/1215] Upgrade to SLF4J 2.0.12 Closes gh-39577 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 30b3ddba0efd..1382fdb0f76c 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1471,7 +1471,7 @@ bom { ] } } - library("SLF4J", "2.0.11") { + library("SLF4J", "2.0.12") { group("org.slf4j") { modules = [ "jcl-over-slf4j", From 90e46b941536d1d09cb95cecb0ee15917a59b7c6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 14 Feb 2024 17:59:23 +0000 Subject: [PATCH 1210/1215] Upgrade to Testcontainers 1.19.5 Closes gh-39578 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 1382fdb0f76c..4b38cf066121 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1641,7 +1641,7 @@ bom { ] } } - library("Testcontainers", "1.19.4") { + library("Testcontainers", "1.19.5") { group("org.testcontainers") { imports = [ "testcontainers-bom" From 6be9fdaeec3c4584d5a438cb6833513e3d83a9f8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 14 Feb 2024 17:59:28 +0000 Subject: [PATCH 1211/1215] Upgrade to Undertow 2.3.11.Final Closes gh-39579 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 4b38cf066121..67314a4265cb 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1701,7 +1701,7 @@ bom { ] } } - library("Undertow", "2.3.10.Final") { + library("Undertow", "2.3.11.Final") { group("io.undertow") { modules = [ "undertow-core", From 71abc9d6b7e60d1e67fdea4d8ebfb5c095c1118b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 14 Feb 2024 18:58:19 +0000 Subject: [PATCH 1212/1215] fixup! Upgrade to Testcontainers 1.19.5 --- .../boot/image/assertions/ImageAssert.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/assertions/ImageAssert.java b/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/assertions/ImageAssert.java index fc5f9509412b..ce326c1702ba 100644 --- a/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/assertions/ImageAssert.java +++ b/spring-boot-system-tests/spring-boot-image-tests/src/systemTest/java/org/springframework/boot/image/assertions/ImageAssert.java @@ -80,12 +80,12 @@ public ListAssert entries() { this.actual.writeTo(out); try (TarArchiveInputStream in = new TarArchiveInputStream( new ByteArrayInputStream(out.toByteArray()))) { - TarArchiveEntry entry = in.getNextEntry(); + TarArchiveEntry entry = in.getNextTarEntry(); while (entry != null) { if (!entry.isDirectory()) { entryNames.add(entry.getName().replaceFirst("^/workspace/", "")); } - entry = in.getNextEntry(); + entry = in.getNextTarEntry(); } } } @@ -101,7 +101,7 @@ public void jsonEntry(String name, Consumer assertConsumer) { this.actual.writeTo(out); try (TarArchiveInputStream in = new TarArchiveInputStream( new ByteArrayInputStream(out.toByteArray()))) { - TarArchiveEntry entry = in.getNextEntry(); + TarArchiveEntry entry = in.getNextTarEntry(); while (entry != null) { if (entry.getName().equals(name)) { ByteArrayOutputStream entryOut = new ByteArrayOutputStream(); @@ -109,7 +109,7 @@ public void jsonEntry(String name, Consumer assertConsumer) { assertConsumer.accept(new JsonContentAssert(LayerContentAssert.class, entryOut.toString())); return; } - entry = in.getNextEntry(); + entry = in.getNextTarEntry(); } } failWithMessage("Expected JSON entry '%s' in layer with digest '%s'", name, this.actual.getId()); From d13c7e013ceb9a64db07c53bd0d6d59bef4b22a5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 14 Feb 2024 19:44:21 +0000 Subject: [PATCH 1213/1215] Upgrade to Micrometer 1.13.0-M1 Closes gh-38984 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 21de4316c4dc..122cfb3ec44e 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -996,7 +996,7 @@ bom { ] } } - library("Micrometer", "1.13.0-SNAPSHOT") { + library("Micrometer", "1.13.0-M1") { considerSnapshots() group("io.micrometer") { modules = [ From 224254b3003e4af4344dbc76afd4d9bac877b9a5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 14 Feb 2024 19:44:22 +0000 Subject: [PATCH 1214/1215] Upgrade to Micrometer Tracing 1.3.0-M1 Closes gh-38985 --- spring-boot-project/spring-boot-dependencies/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 122cfb3ec44e..b3244bf633bd 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1009,7 +1009,7 @@ bom { ] } } - library("Micrometer Tracing", "1.3.0-SNAPSHOT") { + library("Micrometer Tracing", "1.3.0-M1") { considerSnapshots() calendarName = "Tracing" group("io.micrometer") { From b8c8642b5213c41a40cd00c762240de641866027 Mon Sep 17 00:00:00 2001 From: Jake Smolka <30796382+jakesmolka@users.noreply.github.com> Date: Thu, 15 Feb 2024 21:25:02 +0100 Subject: [PATCH 1215/1215] Document configuration property default for the show-values properties #39565 --- .../additional-spring-configuration-metadata.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index aa2900f90990..123dd6ca23b7 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -30,6 +30,14 @@ "description": "Whether to enable default metrics exporters.", "defaultValue": true }, + { + "name": "management.endpoint.configprops.show-values", + "defaultValue": "never" + }, + { + "name": "management.endpoint.env.show-values", + "defaultValue": "never" + }, { "name": "management.endpoint.health.probes.add-additional-paths", "type": "java.lang.Boolean", @@ -61,6 +69,10 @@ "description": "Whether to validate health group membership on startup. Validation fails if a group includes or excludes a health contributor that does not exist.", "defaultValue": true }, + { + "name": "management.endpoint.quartz.show-values", + "defaultValue": "never" + }, { "name": "management.endpoints.enabled-by-default", "type": "java.lang.Boolean",